-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
128 lines (128 loc) · 190 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[Vue实现流程]]></title>
<url>%2Fblog%2F2018%2F10%2F10%2FVue%E5%AE%9E%E7%8E%B0%E6%B5%81%E7%A8%8B%2F</url>
<content type="text"><![CDATA[1.解析模板成render函数 render函数返回的是vnode节点,解析的模板即是字符串,在开发环境下编译打包出js代码 用到with(this),改变作用域 模板中的所有信息都会被render函数所包含 模板中用到的data的属性,都变成js变量 模板中的v-model,v-if,v-for,v-on逻辑都变成js逻辑 render函数最后返回vnode 2.响应式监听 Object.defineProrerty(obj,name,{})进行数据劫持,使得属性变为可观察的对象 data的属性代理到vm实例上 3.首次渲染,显示页面,且绑定依赖12345678910111213vm._update(vnode){ Const preVonde = vm._vnode Vm._vnode = vnode If(!preVnode) { Vm.$el = vm._patch_(vm.$el, vnode) } else { Vm.$el = vm._patch_(preVnode, vnode) }}function updateComponent() { vm._update(vm.render())} 初次渲染执行updateComponent 执行render函数,访问到模板指令数据属性 会被响应式的get方法监听到(不监听set,get作为依赖收集,走get监听来收集data用到的属性,未走到get的属性,set时也无需关心以避免不必要的渲染) 执行updateComponent走到vdom的patch方法 patch将vnode渲染成dom,初次渲染完成 4.data属性变化,触发rerender render函数执行及vdom的patch方法 修改data属性,会被响应式set监听到 set中执行updateComponent update重新执行render函数 生成vnode和preVnode,通过patch进行对比变化 结果渲染到html中 基于es6实现简易mvvm框架总体框架融合实现类12345678910111213141516171819202122232425262728293031323334mvvm.jsclass MVVM { constructor(options) { //属性挂载在实例 this.$el = options.el; this.$data = options.data; if(this.$el) { //数据劫持,将对象所有属性 改为get和set方法 new Observer(this.$data); //代理属性 vm.msg = vm.$data.msg this.proxyData(this.$data) //用数据和元素编译 new Compile(this.$el, this); } } //跟mvvm融合没关系 proxyData(data) { Object.keys(data).forEach((key)=>{ Object.defineProperty(this,key,{ get: function() { return data[key]; }, set: function(newVal) { data[key] = newVal; } }) }) }}实现数据劫持 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107observer.js文件class Observer{ constructor(data) { // this.$data = data; this.observe(data); } observe(data) { //将data属性改为get和set形式 if(!data || typeof data !== 'object') { return } //数据属性一一劫持 Object.keys(data).forEach(key=>{ this.defineReactive(data,key,data[key]); this.observe(data[key]);// 递归劫持 }) } //定义响应式 defineReactive(obj,key,value) { let that = this; let dep = new Dep();//每个变化数据对应一个数组 存放所有更新操作 Object.defineProperty(obj, key, { enumerable:true, configurable:true, get: function() { Dep.target && dep.addSub(Dep.target); return value; }, set: function(newVal) { if(newVal != value){ that.observe(newVal); // 如果是对象仍劫持 value = newVal; dep.notify(); //通知所有人 数据更新 } } }); }}//发布-订阅 数组class Dep{ constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(watcher=>watcher.update()); }}// 给需要变化的元素(input)增加观察者,数据变化后执行对应方法class Watcher{ constructor(vm, expr, fn) { this.vm = vm; this.expr = expr; this.cb = fn; //获取旧值 this.oldvalue = this.getOld(); } getVal(vm,expr) {//获取实例上对应数据 expr = expr.split('.'); // [a,b,c] return expr.reduce((prev,next)=>{ //vm.$data.a return prev[next]; },vm.$data); } getOld() { Dep.target = this; let oldVal = this.getVal(this.vm,this.expr); Dep.target = null; return oldVal; } //对外暴露方法 update() { let newVal = this.getVal(this.vm,this.expr); let oldVal = this.oldvalue; if(newVal != oldVal) { this.cb(newVal) //调用watch的callback } }//比对新值和旧值,发生变化则执行更新方法} 模板编译123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165compile.js文件class Compile { constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; document.querySelector(el); if(this.el) { //获取到元素才开始编译 //1.将dom加载到内存,fragment let fragment = this.nodetoFragment(this.el); //2.编译 查出v-model {{}} this.compile(fragment); //fragment加入页面 this.el.appendChild(fragment) } } //辅助方法 isElementNode(node) { return node.nodeType === 1; } //核心方法 compile(fragment) { let childNodes = fragment.childNodes; Array.from(childNodes).forEach(node=>{ if(this.isElementNode(node)) { //是元素节点,递归 this.compile(node); this.compileElement(node); console.log('element:',node) } else{ this.compileText(node); console.log('test:' +node) } }) } //编译元素 compileElement(node) { let attrs = node.attributes; Array.from(attrs).forEach(attr=>{ console.log(attr.name) let attrName = attr.name; if(this.isDirective(attrName)) { //取到对应值放到节点 let expr = attr.value; // let type = attrName.slice(2); let [,type] = attrName.split('-'); //node this.vm.$data expr CompileUtil[type](node, this.vm, expr) } }) } compileText(node) { let expr = node.textContent; //取文本内容 let reg = /\{\{([^}]+)\}\}/g; if(reg.test(expr)) { //this.vm.$data text CompileUtil['text'](node, this.vm, expr) } } //判断是否指令 isDirective(name) { return name.includes('v-'); } nodetoFragment(el) { let fragment = document.createDocumentFragment(); let firstChild; while (firstChild = el.firstChild){ fragment.appendChild(firstChild); } return fragment; //内存中节点 }}CompileUtil = { getVal(vm,expr) {//获取实例上对应数据 expr = expr.split('.'); // [a,b,c] return expr.reduce((prev,next)=>{ //vm.$data.a return prev[next]; },vm.$data); }, getTextVal(vm,expr) { return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments)=>{ return this.getVal(vm,arguments[1]); // return arguments[1]; }); }, text(node,vm,expr) { let updateFn = this.updater['textUpdate']; // console.log(expr) let value = this.getTextVal(vm,expr); /*let value = expr.replace(/\{\{([^}]+)\}\}/g, (...arguments)=>{ return this.getVal(vm,arguments[1]); return arguments[1]; });*/ //加入watch后添加 expr.replace(/\{\{([^}]+)\}\}/g, (...arguments)=>{ // return arguments[1]; new Watcher(vm,arguments[1],()=>{ //文本节点数据变化,重新获取依赖属性更新文本内容 updateFn && updateFn(node,this.getTextVal(vm,expr)); }); }); // new Watcher(vm,expr); updateFn && updateFn(node,value); }, setVal(vm,expr,value) { expr = expr.split('.'); // [a,b,c] return expr.reduce((prev,next,currentIndet)=>{ //vm.$data.a if(currentIndet == expr.length -1) { return prev[next] = value; } return prev[next]; //获取值 },vm.$data); }, model(node,vm,expr) { let updateFn = this.updater['modelUpdate']; //这里加一个监控 数据变化 调用watch的callback new Watcher(vm,expr,(newVal)=>{ //值变化调用cb 新值传入 调用watch里updata时调用 updateFn && updateFn(node,newVal); }); //数据输入框绑定 node.addEventListener('input',(e)=>{ let newVal = e.target.value; this.setVal(vm,expr,newVal); }) updateFn && updateFn(node,this.getVal(vm,expr)); }, updater:{ textUpdate(node, value) { node.textContent = value; }, modelUpdate(node, value) { node.value = value; } }}]]></content>
<tags>
<tag>vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[技术成长发展]]></title>
<url>%2Fblog%2F2018%2F09%2F21%2F%E6%8A%80%E6%9C%AF%E6%88%90%E9%95%BF%E5%8F%91%E5%B1%95%2F</url>
<content type="text"><![CDATA[成长模型大型公司→中型公司→创业公司,专注的事情从技术到团队再到业务 这里把整个职业生涯分三个阶段:第一阶段:一技之长,就是深耕你的专业技能,你的专业技术;第二阶段:独立做事,当你的一技之长达到一定深度的时候,需要开始思考如何独立做事;第三阶段:拥有事业,选择一份使命,带领团队实现它。 第一阶段:一技之长把它分成五个层次:栈内技术、栈外技术、工程经验、带人做事、业界发声。 所谓栈内技术,是指你的专业领域技术,对于前端工程师来说,就是那些我们熟悉的JS、CSS、HTML等基础,深入了解我们的程序所运行的宿主环境——浏览器,能掌握并灵活操纵在浏览器内发生的一切。前端没有秘密,所有可访问的页面都近似于开源,所以检验栈内技术的标准就是看你是否能最终形成技术上的“写轮眼”——那种看到任何前端产品都有能独立复刻和洞察它的自信。 栈内技术是安身立命的根本,不要轻易“换方向”。 虽然我现在在做技术管理,但始终不会放弃我作为前端这个一技之长的根本。遇到一些前端同学工作几年以后前端做得比较熟了,考虑转行到其他岗位。但是我想说,当你转行那一刻起,就把要转的领域变成了你新的“栈内技术”,然后又要重新走一遍技术沉淀的过程,匆匆几年,又过去了,所以不要轻易“换方向”,前端绝对是可以长时间坚持的领域,现在新型的软件生态,无论是小程序,快应用,甚至区块链,都会首先瞄准JS开发者,因为有庞大的开发者群体,工具链也比较完善,所以长期坚持从事前端工作,在可预见的未来都不会“过时”。 第二,栈外技术 是指栈内技术的上下游,领域外的相关专业知识,包括但不限于服务端技术、运维、CDN、测试,甚至UI设计、产品设计等等,扩展你栈内技术的周围领域,充分理解你的工作在整个技术研发体系中处于怎样的环节。工作之余多投入一份精力,把其他栈外技术不断纳入到你的知识体系中来,建立栈外能力。前端想要做的深入,往往会涉及到缓存、模板渲染、用户体验等知识,没有相当的栈外技术积累,你很难为自己的团队争取到足够的话语权。 我个人非常推崇“大前端”的概念,有点类似“全栈工程师”,但不是要求一个人全面专家,而是以前端为根,展开上下游的栈外技术学习,一专多能。全民CTO角色就说他也是得益于早年在百度和UC积累的栈外技术经验,了解两家公司大部分产品线的运维和架构体系。 第三,工程经验。 工程经验才是技术水平的分水岭,工程经验是指建设专业技术体系的“解决方案”,通俗的说,就是做事的“套路”,掌握从0到1,1到60,甚至60到100分阶段建设专业技术体系的过程。工程经验涉及到技术选型、架构设计、性能优化,CI/CD,日志监控、系统测试等,这些是跟工程相关的方法论。 很多的工程师没有总结自己工程经验的能力,面试经常会问,你们觉得自己工作了两年、三年、四年和刚毕业的学生有什么差别?面试者说掌握了某某技术,但其实往往说的只是一些知识点。那些“今晚查完明天就会了的”不叫技术。决定你比别人更有专业价值的,其实是领域工程经验。你看过再多的文章也没有用,没真正实操过都不能称之为“掌握”。所以我建议要想掌握足够丰富的工程经验,需要在业务中多争取实践的机会。 第四,带人做事。 之前三项都是个人专业技能方面的深度要求,带人做事是对团队协作能力的要求。以前非常不理解一件事情,就是我自己明明做的很好,来了一个新人不能get到我的要求,怎么办?非常着急的时候,我说你别做了,我来吧。更有甚者,早年间在百度工作的时候,有同事白天辛辛苦苦写好的代码,晚上code review之后直接给重构掉了,没过几天那位同学就提了离职。带人做事,是把自己擅长的事情,良好的习惯传递给他人,实现1+1>2的生产力提升,让整个团队的产出高于自己。另外,这个阶段大家要特别注意『管』与『带』的区别。所谓『管』是”我不懂某个领域,但我知道你懂,你必须给我做好”,而『带』则是”我特别懂这个领域,我知道你不懂,我要教你做得更好”。有点授之以渔,甘当人梯,成就他人的意味。带好一个人或者带起一支有战斗力的团队,是做人做事成熟的表现。 提问的深度, 最能体现一个人的能力水平。 对技术有洁癖的人,往往缺乏耐心,带人容易走极端,如何引导新人成长,激发他人的能力,也是我在职业发展过程中要寻求的问题。管理上有一个特别好的方法,就是善于启发式的提问。提问的深度特别能体现一个人的能力水平,任何用于提要求的陈述句,都能转换成疑问句,在启发萌新的过程中植入对结果的约束。举一个例子,我想要老板给我加薪,如果直接说:『老板,我要加薪』,估计十之八九会被搪塞过去,但是转换一下,先问问『老板,我最近工作怎么样?』启发上司思考你的工作成果,引导他得出你应该被嘉奖的结论,或许是个不错的话术。当你让一个人做A的时候,他提出了方案B。你不要强行扭转对方的思路提出A,因为对于新人来讲,或许确实不能一步到位理解A方案,在他的能力约束下,只能想到B。你带人呢,要尽量尝试把A和B之间有差异的地方转换成提问,你问他遇到这个问题怎么解决,遇到那个问题怎么解决,一直问到形成A,他会带着思考去做事情。如果没有这个过程,没有让他思维演化的过程,虽然他收到了A的指令,但是他不理解,他会用别的方式做出来,最后得出来一个C,然后你又重构一遍,陷入一个怪圈不能自拔,这就是我以前的误区,所以我现在特别注重提问的艺术。 第五,业界发声。 如果你前面做的非常好,基本上到了可以对外发声的程度。把自己的工作经验总结对外发布,与他人交流,碰撞思想,看到更高的山峰,然后修正自己的想法,日臻完善,是能走得更远的一种方式。有的时候真的要把自己的思想放到业界的层面验证它,自嗨有毒,大家好才是真的好。如果别人不认可你的这套思路,你的想法,基本上你也可以判定为自己没有达到一个更高的水平。不过通过业界发声获得认可,往往需要机会,可遇不可求,得之我幸,失之我命,这也和所在的公司平台有关。在技术声誉成长最快的地方就是在百度那几年,刚好一个大平台,做出一点成绩,但是小型创业公司出来分享,基本上没有人理你。 永远不要放弃一技之长,没有所谓的转行或者转型,永远坚持你最初的领域,扩充你的外延,最终达成比较全面的能力,所以一技之长是值得你长期信仰的。 第二阶段:独立做事独立做事也有几个层次: 独立Coding:给你一个目标自己做完,不让别人给你擦屁股就可以了。 独立带人:要注重提问的水平,帮助他演进他的想法。 独立带团队:进入到这个阶段,要关注的更多,整个团队的氛围、工作效率,运用你一技之长的工程经验带领团队高效优质的产出成果,实现1+1>2。 独立做业务:团队稳定之后,开始关注所做的业务,行业的发展,理解你的用户,他们是谁,他们在哪,他们为什么使用你的产品,为团队指引下一步的产研方向。 独立生存:独立做事的最高水平,就是能带领一群人养活自己,独立生存下来。互联网的商业模型只有有限的那么几种,深谙其中的玩法,寻找擅长的合适的模型养活一群小伙伴,意味着一次更大的蜕变。 独立做事每个阶段,都是一次比较大的跨越,需要思想和多种软素质发生较大的变化,抛开技术人的身份不讲,独立做事的几个阶段,也是一个人逐渐成熟的过程。 职业问题思考CTO平时都做些什么? 一个是招聘,招聘面试是我平时要做的事情,有专门的HR团队,把自己的帐号给人事的同学去登录,勾搭别人,有一些专业的高端人才,会一直保持联系,寸志老师就是这样被我勾搭过来的(:逃。其次就是考虑技术团队的组织架构,我要参照曾经的经验设计一家公司的架构,一家公司的发展你要在不同的阶段做不同的调整,比如大数据,运维这样的团队,早期我会统一管理,随着公司发展,会考虑独立出来做特色的东西,不要受业务的干扰,这是组织架构。还有就是经常跟CEO聊天,游湖散步,把他的战略目标拆解成技术目标落地实现。另外就是跟合作厂商的沟通协作,创业公司需要借助各方力量,我可以通过置换商业资源的方式寻求行业其他公司的技术帮助,加快产研速度。 怎么决定一个人的薪资、奖金、期权? 薪资是一个人的未来能带来的收益的考量,它是一个市场价值,一个人不管来这家公司做出多少业绩,他达到这个水平,就要给他薪资;奖金是做出业绩的回报,是一个人过去做过的事情的价值衡量;最后一个是期权,如果这个人这家公司充满希望,你可以用一部分的薪资奖金兑换成期权,降低公司的成本。这是三者的关系。充分理解薪资、奖金、期权的关系,是一个CTO要关注的事,有助于建设良好团队氛围。 如何建设好团队工作氛围? 除了前面说到的待遇问题,还有很多举措可以用于建设良好的团队氛围。我是偏理性的管理者,我基本上不太会用个人关系,比如天天吃饭来去维护这个,我比较认可通过完善的机制,完善的规范来建设公平的环境,带领好的氛围。另外,好的氛围的根本,取决于业务,业务不向上,基本上你怎么做都是不好的氛围。 是不是技术做不下去的就要转管理? 我觉得没有所谓的转管理,这个过程对我来说,只是在寻找独立做事的能力,最后怀揣着使命感,要达成梦想,是这样的过程。 技术型人才是不是都不适合做管理? 我曾经也有这样的怀疑,后来自己带团队,经历了一些事情,慢慢了解到,每个人都会形成自己的管理风格,没有适不适合的问题。管理上不是有个『五型管理风格』吗?老虎型、考拉型、孔雀型、猫头鹰型、变色龙型,这五种,没有谁好谁坏,只是不同的风格会带来不同的团队氛围。我估计很多技术同学刚开始带团队的时候都是老虎型。 再就是刚毕业的人应该去大公司还是去创业公司。我觉得经历大公司很有必要,因为去到大公司,你会了解到『好是什么好』,我现在能做到这个位置,一定程度上得益于曾经在B和A的工作经历,大概知道一个团队如何从1发展到60,再从60到100。经历过大公司,知道了好是什么好,才有足够的视野和高度指引团队走下去。 这一点我发现创业的时候一定要先做后学。2年多前,我一个朋友有一天问我,『云龙,我要做个易企秀那样的平台需要多少成本?』,我说认识一个朋友,他们大概需要二三十人的团队做两三个月,可能需要两三百万的资金吧,他说这么高的成本,就不理我了。过了两天来找我,说搞定了,我很诧异,问他怎么解决的,他说在淘宝上买的,一套代码,100多人民币,还带大量模板。这件事给我很大冲击,当你做技术决策的时候,尤其是创业公司,千万要先想到行业里面有什么可以直接用的,用公司的资源置换也好,商务采购也好,拿过来先用,用完以后再组团队去学,学完以后形成自己的一套东西,是这样的过程。 最后一个问题就是如何体现技术团队的价值。如果一个公司都是产品设计运营做的好,我们怎么知道是技术做的好,这是一个技术管理者要非常清楚的要点,才能让团队有方向可以遵循。 这里大概总结了3个要点:技术产品质量: 稳定是基本要求开发效率: 越快越好体验性能: 核心业务指标 高可用,稳定性,扩展性业务开发方向和基础支持组件开发方向 当线上服务一切正常时,老板回想『一切风平浪静,真不知道我要雇这群研发干什么』,而当线上出现事故,老板又会想『成天出问题,真不知道雇你们这群研发干什么』,这种现象其实很普遍,技术产品质量是基础,稳定压倒一切。其次能体现技术价值的就是在稳定的基础上,提升开发效率,快速迭代,快速上线,小步快跑。但做到以上两点,只是基本满足业务要求而已,不被诟病,但要进一步体现技术的价值,就要在性能上做文章了。 亚马逊提出的一个概念,网站打开速度每降低0.1秒,网站就能多增加1%的收益,优秀的团队需要在业务核心性能指标上下功夫,突破秒级的打开速度。]]></content>
<categories>
<category>感悟思考</category>
</categories>
<tags>
<tag>skill thinking</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端代码素养]]></title>
<url>%2Fblog%2F2018%2F09%2F09%2F%E5%89%8D%E7%AB%AF%E4%BB%A3%E7%A0%81%E7%B4%A0%E5%85%BB%2F</url>
<content type="text"><![CDATA[破窗理论破窗理论,原义指窗户破损了,建筑无人照管,人们放任窗户继续破损,最终自己也参与破坏活动,在外墙上涂鸦,任垃圾堆积,最后走向倾颓。破窗理论在实际中非常容易出现,往往第一个人的代码写的不好,第二个人就会有类似“反正他已经写成这样了,那我也只能这样了”的思想,导致代码越维护越冗杂,最后一刻轰然坍塌,变成无人想去维护的垃圾。 整洁的代码整洁的代码如同优美的散文,试想读过的一本好书,能够随着作者的笔锋跌宕起伏,充满了画面感,调动了自己的喜怒哀乐。代码虽然没有那样的高潮迭起,但整洁的代码应当充满张力,能够在某一时刻利用这种张力将情节推向高潮。写代码类比于写文章讲故事,写代码是创作的过程,作者需要将自己想表达的东西通过代码的形式展现出来,而整洁的代码如同讲故事一般,娓娓道来,引人入胜,不好的代码则让人感觉毫无头绪,通篇不知道在讲什么。 整洁代码原则在现代化的前端开发中,有很多自动化工具可以帮助我们写出规范的代码,如eslint,tslint等各种辅助校验工具,知名的规范如google规范、airbnb规范等等也从各个细节方面约束,帮助我们形成合理规范的代码风格。根据实际重构项目,总结出一系列开发过程中需要时刻注意的原则,按照重要程度优先级排列。 1. DRY(Don’t Repeat Yourself)相信大家都听说过最基本的DRY原则,很多设计模式,包括面向对象本身,都是在这条原则上做努力。DRY顾名思义,是指“不要重复自己”,它实际上强调了一个抽象性原则,如果同样或类似的代码片段出现了两次以上,那么应该将它抽象成一个通用方法或文件,在需要使用的地方去依赖引入,确保在改动的时候,只需调整一处,所有的地方都改变过来,而不是到每个地方去找到相应的代码来修改。在实际工作中,我见过两种在这条原则上各自走向极端的代码: 一种是完全没有抽象概念,重复的代码散落在各处,更奇葩的是,有一部分的抽象,但更多的是重复,比如在common下抽取了一个data.js的数据处理文件,部分页面中引用了该文件,而更多页面完全拷贝了该文件中的几个不同方法代码。而作者的意图则是令人啼笑皆非——只用到小部分代码,没必要引入那么整个文件。且不论现代化的前端构建层面可以解决这个问题,即使是引入了整个大文件,这部分多余的代码在gzip之后也不会损失多少性能,但这种到处copy的行为带来后续的维护成本是翻倍的。 对于这种行为还遇到另外一个理由,就是工期时间短,改不动之前的代码,怕造成外网问题,那就拷贝一份相同的逻辑来修改。比如支付逻辑,原有的逻辑为单独的UI浮层+单个支付购买,现在产品提出“打包购买”需求,原有的代码逻辑又比较复杂,出现了“改不动”的现象,于是把UI层和购买逻辑的几个文件整个拷贝过来,修改几下,形成了新的“打包购买”模块,后来产品又提出“按条购买”,按照上述”改不动“原则,又拷贝了一份“按条购买”的模块。这样一来调用处的逻辑就会冗余重复,需要根据不同的购买方式引入不同UI组件和支付逻辑,另外如果新添需求,如支持“分期付款”,那么将改动的是非常多的文件,最可悲的是,最后想要把代码重构为一处统一调用的人,将会面对三份“改不动”的压力,需要众多逻辑中对比分析提取相同之处,工作量已经不能用翻倍来衡量,而这种工作量往往无法得到产品的认同和理解。 另一种极端是过度设计,在写每个逻辑的时候都去抽象,让代码的可读性大大下降,一个简单的for循环都要复用,甚至变量定义,这种代码维护起来也是比较有成本的,还有将迥然不同的逻辑过度抽象,使得抽象方法变得非常复杂,经常“牵一发而动全身”,这种行为也是不可取的。 这也是将该原则排在首位的原因,这种行为导致的重构工作量是最大的,保持良好的代码维护性是一种素养,更是一种责任,如果自己在这方面逃避或偷懒,将把这块工作量翻倍地加在将来别人或自己的身上。 2. SRP(Single Responsibility Principle)SRP也是一个比较著名的设计原则——单一职责,在面向对象的编程中,认为类应该具有单一职责,一个类的改变动机应当只有一个。对于前端开发来说,最需要贯彻的思想是函数应当保持单一职责,一个函数应当只做一件事,这样一来是保证函数的可复用性,更单一的函数有更强的复用性,二来可以让整体的代码框架更加清晰,细节都封装在一个个小函数中。另外一点也和单一职责有关,就是无副作用的函数,也称纯函数,我们应当尽量保证纯函数的数量,非纯函数是不可避免的,但应当尽量减少它。把SRP原则排在第二位,因为它非常的重要,没有人愿意看一团乱麻的逻辑,在维护代码时,如果没有一个清晰的逻辑结构,所有的数据定义、数据处理、DOM操作等等一系列细节的代码全部放在一个函数中,导致这个函数非常的冗长,让人本能地产生心理排斥,不愿去查看内部的逻辑。 3. LKP(Least Knowledge Principle)LKP原则是最小知识原则,又称“迪米特”法则,也就是说,一个对象应该对另一个对象有最少的了解,你内部如何复杂都没关系,我只关心调用的地方。 4. 可读性基本定理可读性基本定理——“代码的写法应当使别人理解它所需的时间最小化”。代码风格和原则不是一概而论的,我们经常需要对一些编码原则和方案进行取舍,例如对于三元表达式的取舍,当我们觉得两种方案都占理时,那么唯一的评判标准就是可读性基本定理,无论写法多么的高超炫技,最好的代码依旧是让人第一时间能够理解的代码。 5. 有意义的命名代码的可读性绝大部分依赖于变量和函数的命名,一个好的名称能够一针见血地帮助维护者理解逻辑,如同写文章中的“文笔”,文笔优异者总能将故事娓娓道来,引人入胜。不过要起好名称还是很难的,尤其是我们不是以英语为母语,更是添加了一层障碍,有些人认为纠结在名称上会导致效率变低,开发第一时间应该完成需求的开发。这样说并没有错,我们在开发过程中应当专注于功能逻辑,但不要完全忽视命名,所谓“文笔”是需要锻炼的,思考的越多,命名就会愈加的水到渠成,到后来也就不太会影响工作效率了。在这里推荐鲍勃大叔提到的童子军规,每一次看自己的代码,都进行一次重构,最简单的重构便是改名,也许一开始觉得命名还比较贴合,但逻辑越写越不符合初始的命名了,当回顾代码时,我们可以顺手对变量和方法进行重新命名,现代编辑工具也很容易做到这一点。 6. 适当的注释维护注释是一个比较有争议性的话题,有人认为可读的函数变量就很清晰,不需要额外的注释,且注释有不可维护性,适当的注释是有必要的,对于复杂的逻辑,如果有一个简练的注释,对于代码可读性的帮助是极大的,但有些不必要的注释可以去掉,注释的取舍关键在于可读性基本定理 小结这里提到的6个代码编写的原则,前三个偏向于代码维护性,后三个偏向于代码可读性,整个可维护性和可读性构成了代码的基本素养。作为一名前端开发工程师,想要拥有良好的代码素养,首先要让自己的代码可维护,不给别人的维护带来巨大的成本和工作量,其次尽量保证代码的美观可读,整洁的代码人见人爱,如同阅读一本好书,令人心情愉悦。]]></content>
<categories>
<category>感悟思考</category>
</categories>
<tags>
<tag>skill thinking</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript数组]]></title>
<url>%2Fblog%2F2018%2F08%2F11%2FJavaScript%E6%95%B0%E7%BB%84%2F</url>
<content type="text"><![CDATA[JavaScript数组的索引是基于零的32位数值,第一个元素索引为0,数组最大能容纳4294967295(即2^32-1)个元素。 JavaScript数组是动态的,根据需要它们会增长或缩减,并且在创建数组时无需声明一个固定的大小或者在数组大小变化时无需重新分配空间。 JavaScript数组可能是稀疏的,数组元素的索引不一定要连续的,它们之间可以有空缺。 每个JavaScript数组都有一个length属性,针对非稀疏数组,该属性就是数组元素的个数。针对稀疏数组,length比所有元素的索引都要大。 创建数组1、最简单的方法是使用数组直接量(字面量)创建数组。var empty = []; //没有元素的数组 var arr = [1.1, true, "a",]; //3个不同类型的元素和结尾的逗号 数组直接量中的值也不一定必须是常量,它们可以是任意的表达式: var number = 1; var list = [number, number+1, number+2]; 如果省略数组直接量中的某个值,省略的元素用empty表示(就是没有这个元素),访问的话会返回undefined。 var count = [1,,3]; // 数组打印出来是(3) [1, empty, 3], count[1]sh是undefined var undefs = [,,]; //数组直接量语法允许有可选的结尾的逗号,undefs.length是2 2、构造函数Array()创建数组调用时没有参数,等同于[],创建一个没有任何元素的空数组 var arr = new Array(); 调用时有一个数值参数,它指定长度 var arr = new Array(10) // (10) [empty × 10] 显式指定两个或者多个数组元素或者数组元素的一个非数值元素 var arr = new Array(1,2,3,"one"); 3、ES6的一些方法(1)Array.of() 返回由所有参数组成的数组,不考虑参数的数量或类型,如果没有参数就返回一个空数组 (ES6新增)参数:elementN 任意个参数,将按顺序成为返回数组中的元素。注意:of() 可以解决上述构造器因参数个数不同,导致的行为有差异的问题(参数只有一个数值时,构造函数会把它当成数组的长度)。 Array.of(1,2,3); // [1,2,3] Array.of(1,{a:1},null,undefined) // [1, {a:1}, null, undefined] // 只有一个数值参数时 let B = new Array(3); // (3) [empty × 3] let C = Array.of(3); // [3] 返回值: 新的 Array 实例。 (2)Array.from()从一个类数组或可迭代对象中创建一个新的数组 (ES6新增) 参数: 第一个参数:想要转换成数组的类数组或可迭代对象 第二个参数(可选):回调函数,类似数组的map方法,对每个元素进行处理,将处理后的值放入返回的数组。 第三个参数(可选):绑定回调函数的this对象 // 有length属性的类数组 Array.from({length:5},(v,i) => i) //[0, 1, 2, 3, 4] // 部署了Iterator接口的数据结构 比如:字符串、Set、NodeList对象 Array.from(‘hello’) // [‘h’,’e’,’l’,’l’,’o’] Array.from(new Set([‘a’,’b’])) // [‘a’,’b’] // 传入一个数组生成的是一个新的数组,引用不同,修改新数组不会改变原数组 let arr1 = [1,2,3] let arr2 = Array.from(arr); arr2[1] = 4; console.log(arr1,arr2) //[1, 2, 3] [1, 4, 3] 返回值: 新的 Array 实例。 知识点 //数组合并去重 function combine(){ //没有去重复的新数组,之后用Set数据结构的特性来去重 let arr = [].concat.apply([], arguments); return Array.from(new Set(arr)); } var m = [1, 2, 2], n = [2,3,3]; console.log(combine(m,n)); 数组方法 1、会改变原数组的方法 push() 方法在数组的尾部添加一个或多个元素,并返回数组的长度参数: item1, item2, …, itemX ,要添加到数组末尾的元素 let arr = [1,2,3]; let length = arr.push('末尾1','末尾2'); // 返回数组长度 console.log(arr,length) // [1, 2, 3, "末尾1", "末尾2"] 5 返回值: 数组的长度 pop() 方法删除数组的最后一个元素,减小数组长度并返回它删除的值。参数:无 //组合使用push()和pop()能够用JavaScript数组实现先进后出的栈 let stack = []; stack.push(1,2) // 返回长度2,这时stack的值是[1,2] stack.pop() // 返回删除的值2,这时stack的值是[1] 返回值: 从数组中删除的元素(当数组为空时返回undefined)。 unshift() 方法在数组的头部添加一个或多个元素,并将已存在的元素移动到更高索引的位置来获得足够的空间,最后返回数组新的长度。参数: item1, item2, …, itemX ,要添加到数组开头的元素 let arr = [3,4,5]; let length = arr.unshift(1,2); // 返回长度是5 console.log(arr, length) //[1, 2, 3, 4, 5] 5 注意: 当调用unshift()添加多个参数时,参数时一次性插入的,而非一次一个地插入。就像是上例添加1和2,他们插入到数组中的顺序跟参数列表中的顺序一致,而不是[2,1,3,4,5]。 返回值: 返回数组新的长度。 shift() 方法删除数组的第一个元素并将其返回,然后把所有随后的元素下移一个位置来填补数组头部的空缺,返回值是删除的元素参数: 无 let arr = [1,2,3]; let item = arr.shift(); // 返回删除的值1 console.log(arr, item) // [2, 3] 1 返回值: 从数组中删除的元素; 如果数组为空则返回undefined 。 splice() 方法是在数组中插入或删除元素的通用方法语法array.splice(start[, deleteCount[, item1[, item2[, …]]]])参数:start 指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1计数);若只使用start参数而不使用deleteCount、item,如:array.splice(start) ,表示删除[start,end]的元素。deleteCount (可选) 整数,表示要移除的数组元素的个数。如果 deleteCount 是 0,则不移除元素。这种情况下,至少应添加一个新元素。如果 deleteCount 大于start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。如果deleteCount被省略,则其相当于(arr.length - start)。item1, item2, … (可选)要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素。返回值: 由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。 // start不超过数组长度(以下操作是连续的) let arr = [1,2,3,4,5]; arr.splice(2) // arr是[1,2],返回值是[3,4,5] arr.splice(1,1) // arr是[1],返回值是[2] arr.splice(0,3) // arr是[],返回值是[1],因为此时数组从第0位开始不够3位, 所以是删除从0开始到最后的所有元素。 // start大于数组长度(以下操作是连续的) let arr = [1,2,3,4,5]; arr.splice(5) // arr是[1,2,3,4,5],返回值是[] arr.splice(5,3,6) // arr是[1,2,3,4,5,6],返回值是[] arr.splice(5,3,7) // arr是[1,2,3,4,5,7] 返回值是[6] // start是负数(以下操作是连续的) let arr = [1,2,3,4,5]; arr.splice(-3,2); // arr是[1,2,5], 返回值是[3,4] arr.splice(-4); // arr是[],返回值是[1,2,5] // 插入数组时,是插入数组本身,而不是数组元素 let arr = [1,4,5]; arr.splice(1,0,[2,3]) // arr是[1,[2,3],4,5],返回值是[] sort() 方法将数组中的元素排序并返回排序后的数组参数: compareFunction (可选) 用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序。 如果指明了 compareFunction ,那么数组会按照调用该函数的返回值排序。即 a 和 b 是两个将要被比较的元素: 如果 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 之前; 如果 compareFunction(a, b) 等于 0 , a 和 b 的相对位置不变。备注: ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla 在 2003 年之前的版本); 如果 compareFunction(a, b) 大于 0 , b 会被排列到 a 之前。 compareFunction(a, b) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。 var stringArray = ["Blue", "Humpback", "Beluga"]; var numberArray = [40, 1, 5, 200]; function compareNumbers(a, b){ return a - b; } console.log('stringArray:' + stringArray.join()); console.log('Sorted:' + stringArray.sort()); console.log('numberArray:' + numberArray.join()); // 没有使用比较函数时,数字并不会按照我们设想的那样排序 console.log('Sorted without a compare function:'+ numberArray.sort()); console.log('Sorted with compareNumbers:'+ numberArray.sort(compareNumbers)); //打印如下 // stringArray: Blue,Humpback,Beluga // Sorted: Beluga,Blue,Humpback // numberArray: 40,1,5,200 // Sorted without a compare function: 1,200,40,5 // Sorted with compareNumbers: 1,5,40,200 返回值: 返回排序后的数组。原数组已经被排序后的数组代替。 reverse() 方法将数组中的元素颠倒顺序,返回逆序的数组。参数: 无 let arr = [1,2,3]; arr.reverse() // arr是[3,2,1],返回值是[3,2,1] 返回值: 返回顺序颠倒后的数组。原数组已经被排序后的数组代替。 copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它,而不修改其大小。 (ES6新增)语法:arr.copyWithin(target[, start[, end]])参数: target 0 为基底的索引,复制序列到该位置。如果是负数,target 将从末尾开始计算。 如果 target 大于等于 arr.length,将会不发生拷贝。如果 target 在 start 之后,复制的序列将被修改以符合 arr.length。 start 0 为基底的索引,开始复制元素的起始位置。如果是负数,start 将从末尾开始计算。 如果 start 被忽略,copyWithin 将会从0开始复制。 end 0 为基底的索引,开始复制元素的结束位置。copyWithin 将会拷贝到该位置,但不包括 end 这个位置的元素。如果是负数, end 将从末尾开始计算。 如果 end 被忽略,copyWithin 将会复制到 arr.length。 返回值: 改变了的数组。 [1, 2, 3, 4, 5].copyWithin(-2); // [1, 2, 3, 1, 2] [1, 2, 3, 4, 5].copyWithin(0, 3); // [4, 5, 3, 4, 5] [1, 2, 3, 4, 5].copyWithin(0, 3, 4); // [4, 2, 3, 4, 5] [1, 2, 3, 4, 5].copyWithin(-2, -3, -1); // [1, 2, 3, 3, 4] // copyWithin 函数是设计为通用的,其不要求其 this 值必须是一个数组对象。 [].copyWithin.call({length: 5, 3: 1}, 0, 3); // {0: 1, 3: 1, length: 5} fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。 (ES6新增)语法:arr.fill(value[, start[, end]])参数: value 用来填充数组元素的值。 start (可选) 起始索引,默认值为0。 end (可选) 终止索引,默认值为 this.length。 如果 start 是个负数, 则开始索引会被自动计算成为 length+start, 其中 length 是 this 对象的 length 属性值. 如果 end 是个负数, 则结束索引会被自动计算成为 length+end。 返回值: 修改后的数组 [1, 2, 3].fill(4); // [4, 4, 4] [1, 2, 3].fill(4, 1); // [1, 4, 4] [1, 2, 3].fill(4, 1, 2); // [1, 4, 3] [1, 2, 3].fill(4, 1, 1); // [1, 2, 3] [1, 2, 3].fill(4, 3, 3); // [1, 2, 3] [1, 2, 3].fill(4, -3, -2); // [4, 2, 3] [1, 2, 3].fill(4, NaN, NaN); // [1, 2, 3] [1, 2, 3].fill(4, 3, 5); // [1, 2, 3] Array(3).fill(4); // [4, 4, 4] //fill 方法故意被设计成通用方法, 该方法不要求 this 是数组对象。 [].fill.call({ length: 3 }, 4); // {0: 4, 1: 4, 2: 4, length: 3} 2、不改变原数组的方法 slice() 方法返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象。且原始数组不会被修改。参数: begin (可选) 从该索引处开始提取原数组中的元素(从0开始)。 如果该参数为负数,则表示从原数组中的倒数第几个元素开始提取,slice(-2)表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。 如果省略 begin,则 slice 从索引 0 开始。 end (可选) 在该索引处结束提取原数组元素(从0开始)。 slice会提取原数组中索引从 begin 到 end 的所有元素(包含begin,但不包含end)。 slice(1,4) 提取原数组中的第二个元素开始直到第四个元素的所有元素 (索引为 1, 2, 3的元素)。 如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1)表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。 如果 end 被省略,则slice 会一直提取到原数组末尾。 如果 end 大于数组长度,slice 也会一直提取到原数组末尾。 返回值: 一个含有提取元素的新数组 let arr = [1,2,3,4,5]; let arr1 = arr.slice(1,3); // arr是[1,2,3,4,5], arr1是[2,3] let arr2 = arr.slice(-2,-1); // arr是[1,2,3,4,5], arr2是[4] // 开始位置在结束位置后面,得到的数组是空 let arr3 = arr.slice(-2, -3); // arr是[1,2,3,4,5], arr3是[] let arr4 = arr.slice(2, 1); // arr是[1,2,3,4,5], arr4是[] //如果元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。 let arr = [{name: 'xiaoming'}]; let arr1 = arr.slice(); // arr是[{name: xiaoming}],arr1是[{name: 'xiaoming'}] arr1[0].name = 'xiaogang'; // arr是[{name: 'xiaogang'}],arr1是[{name: 'xiaogang'}] // 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。 let arr = [1,2,3]; let arr1 = arr.slice(); // arr是[1,2,3],arr1是[1,2,3] arr1[1] = "two"; // arr是[1,2,3],arr1是[1,"tow",3] // 当然,如果向两个数组任一中添加了新元素(简单或者引用类型),则另一个不会受到影响。 join() 方法将数组(或一个类数组对象)中所有元素都转化为字符串并连接在一起,返回最后生成的字符串。参数: separator (可选) 指定一个字符串来分隔数组的每个元素。 如果有(separator),将分隔符转换为字符串。 如果省略(),数组元素用逗号分隔。默认为 “,”。 如果separator是空字符串(“”),则所有元素之间都没有任何字符。 返回值: 一个所有数组元素连接的字符串。如果 arr.length 为0,则返回空字符串 知识点 // 扁平化简单的二维数组 const arr = [11, [22, 33], [44, 55], 66]; const flatArr = arr.join().split(','); // ["11", "22", "33", "44", "55", "66"] toString() 方法将数组的每个元素转化为字符串(如有必要将调用元素的toString()方法)并且输出用逗号分割的字符串列表。返回一个字符串表示数组中的元素 toLocaleString() 数组中的元素将使用各自的 toLocaleString 方法转成字符串,这些字符串将使用一个特定语言环境的字符串(例如一个逗号 “,”)隔开。 concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。 isArray() 用于确定传递的值是否是一个 Array。 3、数组遍历、映射、过滤、检测、简化等方法介绍方法之前,先对这些数组方法做一个概述: 首先,大多数方法的第一个参数接收一个函数,并且对数组的每个元素(或一些元素)调用一次该函数。如果是稀疏数组,对不存在的元素不调用该函数。大多数情况下,调用提供的函数使用三个参数:数组元素、元素的索引和数组本身。通常,只需要第一个参数值,可以忽略后两个参数。 大多数方法,第二个参数是可选的。如果有第二个参数,则调用的第一个函数参数被看做是第二个参数的方法,即当执行第一个函数参数时用作this的值(参考对象)。 方法的返回值很重要,不同的方法处理返回值的方式也不一样。 下面这些方法运行时的规则: 对于空数组是不会执行回调函数的 对于已在迭代过程中删除的元素,或者空元素会跳过回调函数 遍历次数在第一次循环前就会确定,再添加到数组中的元素不会被遍历。 如果已经存在的值被改变,则传递给 callback 的值是遍历到他们那一刻的值。 已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了(例如使用 shift()) ,之后的元素将被跳过 forEach() 方法从头到尾遍历数组,为每个元素调用指定的函数。 map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个callback函数后返回的结果。 filter() 方法返回的数组元素是调用的数组的一个子集。传入的函数时用来逻辑判定的,该函数返回 true 或 false,如果返回值为true或能转化为true的值,那么传递给判断函数的元素就是这个子集的成员,它将被添加倒一个作为返回值的数组中。 every() 方法测试数组的所有元素是否都通过了指定函数的测试。当且仅当针对数组中的所有元素调用判定函数都返回true,它才返回true。 some() 方法测试数组中的某些元素是否通过由提供的函数实现的测试。当数组中至少有一个元素调用判定函数返回true,它就返回true,当且仅当数组中的所有元素调用判定函数都返回false,它才返回false。 reduce() 和 reduceRight() 这两个方法使用指定的函数将数组元素进行组合,生成单个值。这在函数式编程中是常见的操作,也可以称为“注入”和“折叠”。reduceRight() 和 reduce() 工作原理是一样的,不同的是reduceRight() 按照数组索引从高到低(从右到左)处理数组,而不是从低到高。参数: callback 执行数组中每个值的函数,包含四个参数: accumulator 累加器累加回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(如下所示)。 currentValue 数组中正在处理的元素。 currentIndex (可选) 数组中正在处理的当前元素的索引。 如果提供了initialValue,则索引号为0,否则为索引为1。 array (可选) 调用reduce的数组 initialValue (可选) 用作第一个调用 callback的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。 注意: reduce为数组中的每一个元素依次执行callback函数,不包括数组中被删除或从未被赋值的元素,回调函数第一次执行时,accumulator 和currentValue的取值有两种情况:调用reduce时提供initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;没有提供 initialValue,accumulator取数组中的第一个值,currentValue取数组中的第二个值。即:如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。 如果数组为空且没有提供initialValue,会抛出TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供initialValue, 或者有提供initialValue但是数组为空,那么此唯一值将被返回并且callback不会被执行。 indexof() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。 lastIndexOf() 跟indexOf()查找方向相反,方法返回指定元素在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始 includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。 ES7新增 find() 和 findIndex() find 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。findIndex 方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。(ES6新增) keys() 方法返回一个新的Array迭代器,它包含数组中每个索引的键。 (ES6新增) values() 方法返回一个新的Array迭代器,它包含数组中每个索引的值。 (ES6新增) @@iterator 属性和 values() 属性的初始值均为同一个函数对象。数组的 iterator 方法,默认情况下与 values() 返回值相同,调用语法是 arrSymbol.iterator (ES6新增) entries() 方法返回一个新的Array迭代器,该对象包含数组中每个索引的键/值对。 (ES6新增)11-14参数: 都是无。11-14都是一个新的 Array 迭代器对象。 扩展几个概念1、数组的索引和对象key有什么关系? 数组是对象的特殊形式,使用方括号访问数组元素和使用方括号访问对象属性一样。JavaScript将指定的数字索引值转换成字符串——索引1变成”1”——然后将其作为属性名来使用。数组的特别之处在于,当使用小于2^32的非负整数作为属性名时数组会自动维护其length属性。 // 索引到属性名的转化 let arr = [1,2,3]; console.log(arr[1]) // 2 console.log(arr["1"]) // 2 所有的数组都是对象,可以为其创建任意名字的属性,不过,只有在小于2^32的非负整数才是索引,数组才会根据需要更新length。事实上数组的索引仅仅是对象属性名的一种特殊类型,这意味着JavaScript数组没有“越界”错误的概念。当查询任何对象中不存在的属性时,不会报错,只会得到undefined let arr = []; arr["a"] = 1; console.log(arr,arr.length) // arr是[a:1] length是0 对于使用负数或非整数的情况,数值会转换为字符串,字符串作为属性名来用,当时只能当做常规的对象属性,而非数组的索引。 let arr = []; arr[-1.23] = 0; console.log(arr,arr.length) // arr是[-1.23: 0] length是0 使用非负整数的字符串或者一个跟整数相等的浮点数时,它就当做数组的索引而非对象属性。 let arr = []; arr["100"] = 'a'; console.log(arr,arr.length) // arr 是[empty × 100, "a"],length 是101 let arr1 = []; arr1[1.0000] = 'b'; console.log(arr1,arr1.length) // arr 是[empty, "b"],length 是2 2、稀疏数组 稀疏数组就是包含从0开始的不连续索引的数组。通常数组的length属性值代表数组中元素的个数。如果数组是稀疏的,length属性值大于元素的个数 足够稀疏的数组通常在实现上比稠密的数组更慢,更耗内存,在这样的数组中查找元素所用的时间就变得跟常规对象的查找时间一样长了,失去了性能的优势。 3、类数组对象 拥有一个数值length属性和对应非负整数属性的对象看做一种类型的数组 数组跟类数组相比有以下不同: 当有新元素添加到数组中时,自动更新length属性 设置length为一个较小值将截断数组 从Array.prototype中继承了一些方法 其类属性为'Array' JavaScript 数组有很多方法特意定义通用,因此他们不仅应用在真正的数组而且在类数组对象上都能正确工作,JavaScript权威指南一书说的是:ES5中所有的方法都是通用的,ES3中除了toString()和toLocaleString()意外所有方法也是通用的。类数组对象显然没有继承自Array.prototype,所以它们不能直接调用数组方法,不过可以间接地使用Function.call方法调用。 4、 JavaScript数组的进化——类型化数组的引入 先说一下普遍意义上的Array,数组是一串 连续 的内存位置,用来保存某些值。JavaScript 中的数组是哈希映射,可以使用不同的数据结构来实现,如链表,上一个元素包含下一个元素的引用。这样其他语言中数组的取值是根据内存位置进行数学计算就能找到,而在JavaScript中就需要遍历链表之类的结构,数组越长,遍历链表跟数据计算相比就越慢。 现代 JavaScript 引擎是会给数组分配连续内存的 —— 如果数组是同质的(所有元素类型相同)。所以在写代码时保证数组同质,以便 JIT(即时编译器)能够使用 c 编译器式的计算方法读取元素是一种优雅的方式。不过,一旦你想要在某个同质数组中插入一个其他类型的元素,JIT 将解构整个数组,并按照旧有的方式重新创建。 ES6 增加了 ArrayBuffer, 提供一块连续内存供我们随意操作。然而,直接操作内存还是太复杂、偏底层。于是便有了处理 ArrayBuffer 的视图(View)。 ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。语法: new ArrayBuffer(length) 参数length:要创建的 ArrayBuffer 的大小,单位为字节。 返回值:一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。复制代码类型数组对象 一个TypedArray 对象描述一个底层的二进制数据缓存区的一个类似数组(array-like)视图。事实上,没有名为 TypedArray的全局对象,也没有一个名为的 TypedArray构造函数。相反,有许多不同的全局对象,下面会列出这些针对特定元素类型的类型化数组的构造函数。 new TypedArray(); // ES2017中新增 new TypedArray(length); new TypedArray(typedArray); new TypedArray(object); new TypedArray(buffer [, byteOffset [, length]]); TypedArray()指的是以下的其中之一: Int8Array();//8位二进制带符号整数 -2^7~(2^7) - 1,大小1个字节 Uint8Array();//8位无符号整数 0~(2^8) - 1,大小1个字节 Int16Array();//16位二进制带符号整数 -2^15~(2^15)-1,大小2个字节 Uint16Array();//16位无符号整数 0~(2^16) - 1,大小2个字节 Int32Array();// 32位二进制带符号整数 -2^31~(2^31)-1,大小4个字节 Uint32Array();//32位无符号整数 0~(2^32) - 1,大小4个字节 Float32Array();//32位IEEE浮点数,大小4个字节 Float64Array(); //64位IEEE浮点数,大小8个字节 应用: var buffer = new ArrayBuffer(8); var view = new Int32Array(buffer); view[0] = 100; console.log(view)// [100,0],一个八个字节,Int32Array一个元素大小是 4个字节,所以只能放下两个元素 参考和链接 《JavaScript权威指南》数组部分详解JS遍历一次掌握 JavaScript ES5 到 ES8 数组内容【干货】js 数组详细操作方法及解析合集【译】 深入 JavaScript 数组:进化与性能 原文链接:https://juejin.im/post/5b684ef9e51d451964629ba1]]></content>
<categories>
<category>Javascript</category>
</categories>
<tags>
<tag>Array</tag>
</tags>
</entry>
<entry>
<title><![CDATA[git命令大全]]></title>
<url>%2Fblog%2F2018%2F06%2F06%2FGit%E5%91%BD%E4%BB%A4%E5%A4%A7%E5%85%A8%2F</url>
<content type="text"><![CDATA[目录 配置 修改项目中的个人信息 配置自动换行 常见使用场景 创建SSH密钥 多账号ssh配置 免密码登录远程服务器 https协议下提交代码免密码 文件推向3个git库 修改远程仓库地址 撤销远程记录 放弃本地的文件修改 最简单放弃本地修改内容 回滚到某个commit提交 回退到某一个版本 去掉某个commit 新建一个空分支 合并多个commit 修改远程Commit记录 添加忽略文件 利用commit关闭一个issue 同步fork的上游仓库 批量修改历史commit中的名字和邮箱 查看某个文件历史 查看git仓库中最近修改的分支 打造自己的git命令 中文乱码的解决方案 新建仓库 init status add commit remote push clone 本地 help add rm commit reset revert checkout diff stash merge cherry-pick rebase 分支branch 删除 提交 拉取 分支合并 重命名 查看 新建 连接 分支切换 远端 submodule 删除文件 remote 标签tag 日志log 重写历史 删除仓库 其它 报错问题解决 参考资料 配置首先是配置帐号信息 ssh -T git@github.com 测试。 修改项目中的个人信息1234git help config # 获取帮助信息,查看修改个人信息的参数 git config --global user.name "小弟调调" # 修改全局名字git config --global user.email "wowohoo@qq.com" # 修改全局邮箱git config --list # 查看配置的信息 配置自动换行自动转换坑太大,提交到git是自动将换行符转换为lf 1git config --global core.autocrlf input 常见使用场景创建SSH密钥这个密钥用来跟 github 通信,在本地终端里生成然后上传到 github 123ssh-keygen -t rsa -C 'wowohoo@qq.com' # 生成密钥 ssh-keygen -t rsa -C "wowohoo@qq.com" -f ~/.ssh/ww_rsa # 指定生成目录文件名字ssh -T git@github.com # 测试是否成功 多账号ssh配置1.生成指定名字的密钥 ssh-keygen -t rsa -C "邮箱地址" -f ~/.ssh/jslite_rsa会生成 jslite_rsa 和 jslite_rsa.pub 这两个文件 2.密钥复制到托管平台上 vim ~/.ssh/jslite_rsa.pub打开公钥文件 jslite_rsa.pub ,并把内容复制至代码托管平台上 3.修改config文件 vim ~/.ssh/config #修改config文件,如果没有创建 config 123456789101112Host jslite.github.com HostName github.com User git IdentityFile ~/.ssh/jslite_rsaHost work.github.com HostName github.com # Port 服务器open-ssh端口(默认:22,默认时一般不写此行) # PreferredAuthentications 配置登录时用什么权限认证 # publickey|password publickey|keyboard-interactive等 User git IdentityFile ~/.ssh/work_rsa Host 这里是个别名可以随便命名 HostName 一般是网站如:`git@ss.github.com:username/repo.git填写github.com` User 通常填写git IdentityFile 使用的公钥文件地址 4.测试 123ssh -T git@jslite.github.com # `@`后面跟上定义的Host ssh -T work.github.com # 通过别名测试ssh -i ~/公钥文件地址 Host别名 # 如 ssh -i ~/.ssh/work_rsa work.github.com 5.使用 12345# 原来的写法git clone git@github.com:<jslite的用户名>/learngit.git# 现在的写法git clone git@jslite.github.com:<jslite的用户名>/learngit.gitgit clone git@work.github.com:<work的用户名>/learngit.git 5.注意 如果你修改了id_rsa的名字,你需要将ssh key添加到SSH agent中,如: 1234ssh-add ~/.ssh/jslite_rsassh-add -l # 查看所有的keyssh-add -D # 删除所有的keyssh-add -d ~/.ssh/jslite_rsa # 删除指定的key 免密码登录远程服务器12$ ssh-keygen -t rsa -P '' -f ~/.ssh/aliyunserver.key$ ssh-copy-id -i ~/.ssh/aliyunserver.key.pub root@192.168.182.112 # 这里需要输入密码一次 编辑 ~/.ssh/config 12345Host aliyun1 HostName 192.168.182.112 User root PreferredAuthentications publickey IdentityFile ~/.ssh/aliyunserver.key 上面配置完了,可以通过命令登录,不需要输入IP地址和密码 ssh aliyun1 https协议下提交代码免密码1git clone https://github.com/username/rep.git 通过上面方式克隆可能需要密码,解决办法:进入当前克隆的项目 vi rep/.git/config 编辑 config, 按照下面方式修改,你就可以提交代码不用输入密码了。 1234567891011121314[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true[remote "origin"]- url = https://github.com/username/rep.git+ url = https://用户名:密码@github.com/username/rep.git fetch = +refs/heads/*:refs/remotes/origin/*[branch "master"] remote = origin merge = refs/heads/master 文件推向3个git库1. 增加3个远程库地址 123git remote add origin https://github.com/JSLite/JSLite.git git remote set-url --add origin https://gitlab.com/wang/JSLite.js.git git remote set-url --add origin https://oschina.net/wang/JSLite.js.git 2. 删除其中一个 set-url 地址 123usage: git remote set-url [--push] <name> <newurl> [<oldurl>] or: git remote set-url --add <name> <newurl> or: git remote set-url --delete <name> <url> git remote set-url --delete origin https://oschina.net/wang/JSLite.js.git 3.推送代码 12git push origin mastergit push -f origin master # 强制推送 4.拉代码 只能拉取 origin 里的一个url地址,这个fetch-url默认为你添加的到 origin的第一个地址 12345678git pull origin master git pull --all # 获取远程所有内容包括tag git pull origin next:master # 取回origin主机的next分支,与本地的master分支合并 git pull origin next # 远程分支是与当前分支合并 # 上面一条命令等同于下面两条命令 git fetch origin git merge origin/next 如果远程主机删除了某个分支,默认情况下,git pull 不会在拉取远程分支的时候,删除对应的本地分支。这是为了防止,由于其他人操作了远程主机,导致git pull不知不觉删除了本地分支。但是,你可以改变这个行为,加上参数 -p 就会在本地删除远程已经删除的分支。 1234$ git pull -p# 等同于下面的命令$ git fetch --prune origin $ git fetch -p 5.更改pull 只需要更改config文件里,那三个url的顺序即可,fetch-url会直接对应排行第一的那个utl连接。 修改远程仓库地址12git remote remove origin # 删除该远程路径 git remote add origin git@jslite.github.com:JSLite/JSLite.git # 添加远程路径 撤销远程记录12git reset --hard HEAD~1 # 撤销一条记录 git push -f origin HEAD:master # 同步到远程仓库 放弃本地的文件修改1git reset --hard FETCH_HEAD # FETCH_HEAD表示上一次成功git pull之后形成的commit点。然后git pull git reset --hard FETCH_HEAD 出现错误 123456git pullYou are not currently on a branch, so I cannot use any'branch.<branchname>.merge' in your configuration file.Please specify which remote branch you want to use on the commandline and try again (e.g. 'git pull <repository> <refspec>').See git-pull(1) FOR details. 解决方法: 12git checkout -b temp # 新建+切换到temp分支 git checkout master 最简单放弃本地修改内容123456# 如果有的修改以及加入暂存区的话git reset --hard # 还原所有修改,不会删除新增的文件git checkout . # 下面命令会删除新增的文件git clean -xdf 通过存储暂存区stash,在删除暂存区的方法放弃本地修改。 1git stash && git stash drop 回滚到某个commit提交12git revert HEAD~1 # 撤销一条记录 会弹出 commit 编辑git push # 提交回滚 回退到某一个版本1234git reset --hard <hash># 例如 git reset --hard a3hd73r# --hard代表丢弃工作区的修改,让工作区与版本代码一模一样,与之对应,# --soft参数代表保留工作区的修改。 去掉某个commit12# 实质是新建了一个与原来完全相反的commit,抵消了原来commit的效果git revert <commit-hash> 新建一个空分支123456# 这种方式新建的分支(gh-pages)是没有 commit 记录的git checkout --orphan gh-pages# 删除新建的gh-pages分支原本的内容,如果不删除,提交将作为当前分支的第一个commitgit rm -rf .# 查看一下状态 有可能上面一条命令,没有删除还没有提交的的文件git state 合并多个commit1234567891011121314151617# 这个命令,将最近4个commit合并为1个,HEAD代表当前版本。# 将进入VIM界面,你可以修改提交信息。git rebase -i HEAD~4 # 可以看到其中分为两个部分,上方未注释的部分是填写要执行的指令,# 而下方注释的部分则是指令的提示说明。指令部分中由前方的命令名称、commit hash 和 commit message 组成# 当前我们只要知道 pick 和 squash 这两个命令即可。# --> pick 的意思是要会执行这个 commit# --> squash 的意思是这个 commit 会被合并到前一个commit# 我们将 需要保留的 这个 commit 前方的命令改成 squash 或 s,然后输入:wq以保存并退出# 这是我们会看到 commit message 的编辑界面# 其中, 非注释部分就是两次的 commit message, 你要做的就是将这两个修改成新的 commit message。# # 输入wq保存并推出, 再次输入git log查看 commit 历史信息,你会发现这两个 commit 已经合并了。# 将修改强制推送到前端git push -f origin master 修改远程Commit记录123456789101112131415161718git commit --amend# amend只能修改没有提交到线上的,最后一次commit记录git rebase -i HEAD~3# 表示要修改当前版本的倒数第三次状态# 将要更改的记录行首单词 pick 改为 editpick 96dc3f9 doc: Update quick-start.mdpick f1cce8a test(Transition):Add transition test (#47)pick 6293516 feat(Divider): Add Divider component.# Rebase eeb03a4..6293516 onto eeb03a4 (3 commands)## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like "squash", but discard this commit's log message# x, exec = run command (the rest of the line) using shell# d, drop = remove commit 保存并退出,会弹出下面提示 12345678910111213141516# You can amend the commit now, with# # git commit --amend# # Once you are satisfied with your changes, run# # git rebase --continue# 通过这条命令进入编辑页面更改commit,保存退出git commit --amend# 保存退出确认修改,继续执行 rebase, git rebase --continue# 如果修改多条记录反复执行上面两条命令直到完成所有修改# 最后,确保别人没有提交进行push,最好不要加 -f 强制推送git push -f origin master 添加忽略文件1echo node_modules/ >> .gitignore 利用commit关闭一个issue这个功能在Github上可以玩儿,Gitlab上特别老的版本不能玩儿哦,那么如何跟随着commit关闭一个issue呢? 在confirm merge的时候可以使用一下命令来关闭相关issue: fixes #xxx、 fixed #xxx、 fix #xxx、 closes #xxx、 close #xxx、 closed #xxx、 同步fork的上游仓库Github教程同步fork教程,在Github上同步一个分支(fork) 设置添加多个远程仓库地址。 在同步之前,需要创建一个远程点指向上游仓库(repo).如果你已经派生了一个原始仓库,可以按照如下方法做。 123456789101112$ git remote -v# List the current remotes (列出当前远程仓库)# origin https://github.com/user/repo.git (fetch)# origin https://github.com/user/repo.git (push)$ git remote add upstream https://github.com/otheruser/repo.git# Set a new remote (设置一个新的远程仓库)$ git remote -v# Verify new remote (验证新的原唱仓库)# origin https://github.com/user/repo.git (fetch)# origin https://github.com/user/repo.git (push)# upstream https://github.com/otheruser/repo.git (fetch)# upstream https://github.com/otheruser/repo.git (push) 同步更新仓库内容 同步上游仓库到你的仓库需要执行两步:首先你需要从远程拉去,之后你需要合并你希望的分支到你的本地副本分支。从上游的存储库中提取分支以及各自的提交内容。 master 将被存储在本地分支机构 upstream/master 1234567git fetch upstream# remote: Counting objects: 75, done.# remote: Compressing objects: 100% (53/53), done.# remote: Total 62 (delta 27), reused 44 (delta 9)# Unpacking objects: 100% (62/62), done.# From https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY# * [new branch] master -> upstream/master 检查你的 fork’s 本地 master 分支 12git checkout master# Switched to branch 'master' 合并来自 upstream/master 的更改到本地 master 分支上。 这使你的前 fork’s master 分支与上游资源库同步,而不会丢失你本地修改。 12345678git merge upstream/master# Updating a422352..5fdff0f# Fast-forward# README | 9 -------# README.md | 7 ++++++# 2 files changed, 7 insertions(+), 9 deletions(-)# delete mode 100644 README# create mode 100644 README.md 批量修改历史commit中的名字和邮箱1.克隆仓库 注意参数,这个不是普通的clone,clone下来的仓库并不能参与开发 12git clone --bare https://github.com/user/repo.gitcd repo.git 2.命令行中运行代码 OLD_EMAIL原来的邮箱CORRECT_NAME更正的名字CORRECT_EMAIL更正的邮箱 将下面代码复制放到命令行中执行 123456789101112131415git filter-branch -f --env-filter 'OLD_EMAIL="wowohoo@qq.com"CORRECT_NAME="小弟调调"CORRECT_EMAIL="更正的邮箱@qq.com"if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]then export GIT_COMMITTER_NAME="$CORRECT_NAME" export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"fiif [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]then export GIT_AUTHOR_NAME="$CORRECT_NAME" export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"fi' --tag-name-filter cat -- --branches --tags 执行过程 123Rewrite 160d4df2689ff6df3820563bfd13b5f1fb9ba832 (479/508) (16 seconds passed, remaining 0 predicted)Ref 'refs/heads/dev' was rewrittenRef 'refs/heads/master' was rewritten 3.同步到远程仓库 同步到push远程git仓库 1git push --force --tags origin 'refs/heads/*' 我还遇到了如下面错误,lab默认给master分支加了保护,不允许强制覆盖。Project(项目)->Setting->Repository 菜单下面的Protected branches把master的保护去掉就可以了。修改完之后,建议把master的保护再加回来,毕竟强推不是件好事。 1remote: GitLab: You are not allowed to force push code to a protected branch on this project. 当上面的push 不上去的时候,先 git pull 确保最新代码 123git pull --allow-unrelated-histories# 或者指定分枝git pull origin master --allow-unrelated-histories 查看某个文件历史12345git log --pretty=oneline 文件名 # 列出文件的所有改动历史 git show c178bf49 # 某次的改动的修改记录 git log -p c178bf49 # 某次的改动的修改记录 git blame 文件名 # 显示文件的每一行是在那个版本最后修改。 git whatchanged 文件名 # 显示某个文件的每个版本提交信息:提交日期,提交人员,版本号,提交备注(没有修改细节) 查看git仓库中最近修改的分支1git for-each-ref --count=30 --sort=-committerdate refs/heads/ --format='%(refname:short)' 打造自己的git命令1234git config --global alias.st statusgit config --global alias.br branchgit config --global alias.co checkoutgit config --global alias.ci commit 配置好后再输入git命令的时候就不用再输入一大段了,例如我们要查看状态,只需: 1git st 中文乱码的解决方案1git config --global core.quotepath false 新建仓库initgit init #初始化 statusgit status #获取状态 addgit add file # .或*代表全部添加git rm --cached <added_file_to_undo> # 在commit之前撤销git add操作git reset head # 好像比上面git rm --cached更方便 commitgit commit -m "message" #此处注意乱码 remotegit remote add origin git@github.com:JSLite/test.git #添加源 push123git push -u origin master # push同事设置默认跟踪分支 git push origin master git push -f origin master # 强制推送文件,缩写 -f(全写--force) clonegit clone git://github.com/JSLite/JSLite.js.gitgit clone git://github.com/JSLite/JSLite.js.git mypro #克隆到自定义文件夹git clone [user@]example.com:path/to/repo.git/ #SSH协议还有另一种写法。 git clone支持多种协议,除了HTTP(s)以外,还支持SSH、Git、本地文件协议等,下面是一些例子。git clone <版本库的网址> <本地目录名> 1234567$ git clone http[s]://example.com/path/to/repo.git/$ git clone ssh://example.com/path/to/repo.git/$ git clone git://example.com/path/to/repo.git/$ git clone /opt/git/project.git $ git clone file:///opt/git/project.git$ git clone ftp[s]://example.com/path/to/repo.git/$ git clone rsync://example.com/path/to/repo.git/ 本地help1git help config # 获取帮助信息 add12git add * # 跟踪新文件 git add -u [path] # 添加[指定路径下]已跟踪文件 rm12345rm *&git rm * # 移除文件 git rm -f * # 移除文件 git rm --cached * # 取消跟踪 git mv file_from file_to # 重命名跟踪文件 git log # 查看提交记录 commit12345678git commit #提交更新 git commit -m 'message' #提交说明 git commit -a #跳过使用暂存区域,把所有已经跟踪过的文件暂存起来一并提交 git commit --amend #修改最后一次提交 git commit log #查看所有提交,包括没有push的commit git commit -m "#133" #关联issue 任意位置带上# 符号加上issue号码 git commit -m "fix #133" commit关闭issue git commit -m '概要描述'$'\n\n''1.详细描述'$'\n''2.详细描述' #提交简要描述和详细描述 reset12345git reset HEAD * # 取消已经暂存的文件 git reset --mixed HEAD * # 同上 git reset --soft HEAD * # 重置到指定状态,不会修改索引区和工作树 git reset --hard HEAD * # 重置到指定状态,会修改索引区和工作树 git reset -- files * # 重置index区文件 revert123git revert HEAD # 撤销前一次操作 git revert HEAD~ # 撤销前前一次操作 git revert commit # 撤销指定操作 checkout12345git checkout -- file # 取消对文件的修改(从暂存区——覆盖worktree file) git checkout branch|tag|commit -- file_name # 从仓库取出file覆盖当前分支 git checkout HEAD~1 [文件] # 将会更新 working directory 去匹配某次 commit git checkout -- . # 从暂存区取出文件覆盖工作区 git checkout -b gh-pages 0c304c9 # 这个表示 从当前分支 commit 哈希值为 0c304c9 的节点,分一个新的分支gh-pages出来,并切换到 gh-pages diff12345678910git diff file # 查看指定文件的差异 git diff --stat # 查看简单的diff结果 git diff # 比较Worktree和Index之间的差异 git diff --cached # 比较Index和HEAD之间的差异 git diff HEAD # 比较Worktree和HEAD之间的差异 git diff branch # 比较Worktree和branch之间的差异 git diff branch1 branch2 # 比较两次分支之间的差异 git diff commit commit # 比较两次提交之间的差异 git diff master..test # 上面这条命令只显示两个分支间的差异 git diff master...test # 你想找出‘master’,‘test’的共有 父分支和'test'分支之间的差异,你用3个‘.'来取代前面的两个'.' stash123456git stash # 将工作区现场(已跟踪文件)储藏起来,等以后恢复后继续工作。 git stash list # 查看保存的工作现场 git stash apply # 恢复工作现场 git stash drop # 删除stash内容 git stash pop # 恢复的同时直接删除stash内容 git stash apply stash@{0} # 恢复指定的工作现场,当你保存了不只一份工作现场时。 merge1git merge --squash test # 合并压缩,将test上的commit压缩为一条 cherry-pick12git cherry-pick commit # 拣选合并,将commit合并到当前分支 git cherry-pick -n commit # 拣选多个提交,合并完后可以继续拣选下一个提交 rebase123456git rebase master # 将master分之上超前的提交,变基到当前分支 git rebase --onto master 169a6 # 限制回滚范围,rebase当前分支从169a6以后的提交 git rebase --interactive # 交互模式,修改commit git rebase --continue # 处理完冲突继续合并 git rebase --skip # 跳过 git rebase --abort # 取消合并 分支branch删除123456git push origin :branchName # 删除远程分支 git push origin --delete new # 删除远程分支new git branch -d branchName # 删除本地分支,强制删除用-D git branch -d test # 删除本地test分支 git branch -D test # 强制删除本地test分支 git remote prune origin # 远程删除了,本地还能看到远程存在,这条命令删除远程不存在的分支 提交1git push -u origin branchName # 提交分支到远程origin主机中 拉取git fetch -p #拉取远程分支时,自动清理 远程分支已删除,本地还存在的对应同名分支。 分支合并1234git merge branchName # 合并分支 - 将分支branchName和当前所在分支合并 git merge origin/master # 在本地分支上合并远程分支。 git rebase origin/master # 在本地分支上合并远程分支。 git merge test # 将test分支合并到当前分支 重命名git branch -m old new #重命名分支 查看1234567git branch # 列出本地分支 git branch -r # 列出远端分支 git branch -a # 列出所有分支 git branch -v # 查看各个分支最后一个提交对象的信息 git branch --merge # 查看已经合并到当前分支的分支 git branch --no-merge # 查看为合并到当前分支的分支 git remote show origin # 可以查看remote地址,远程分支 新建1234git branch test # 新建test分支 git branch newBrach 3defc69 # 指定哈希3defc69,新建分支名字为newBrachgit checkout -b newBrach origin/master # 取回远程主机的更新以后,在它的基础上创建一个新的分支 git checkout -b newBrach 3defc69 # 以哈希值3defc69,新建 newBrach 分支,并切换到该分支 连接12git branch --set-upstream dev origin/dev # 将本地dev分支与远程dev分支之间建立链接 git branch --set-upstream master origin/next # 手动建立追踪关系 分支切换123git checkout test # 切换到test分支 git checkout -b test # 新建+切换到test分支 git checkout -b test dev # 基于dev新建test分支,并切换 远端12345678git fetch <远程主机名> <分支名> # fetch取回所有分支(branch)的更新 git fetch origin remotebranch[:localbranch] # 从远端拉去分支[到本地指定分支] git merge origin/branch # 合并远端上指定分支 git pull origin remotebranch:localbranch # 拉去远端分支到本地分支 git push origin branch # 将当前分支,推送到远端上指定分支 git push origin localbranch:remotebranch # 推送本地指定分支,到远端上指定分支 git push origin :remotebranch # 删除远端指定分支 git checkout -b [--track] test origin/dev # 基于远端dev分支,新建本地test分支[同时设置跟踪] submodule克隆项目同时克隆submodule 1git clone https://github.com/jaywcjlove/handbook.git --depth=1 --recurse-submodules 克隆项目,之后再手动克隆 submodule 子项目 123456git submodule add --force '仓库地址' '路径'# 其中,仓库地址是指子模块仓库地址,路径指将子模块放置在当前工程下的路径。# 注意:路径不能以 / 结尾(会造成修改不生效)、不能是现有工程已有的目录(不能順利 Clone)git submodule init # 初始化submodulegit submodule update # 更新submodule(必须在根目录执行命令)git submodule update --init --recursive # 下载的工程带有submodule 当使用git clone下来的工程中带有submodule时,初始的时候,submodule的内容并不会自动下载下来的,此时,只需执行如下命令: 12345git submodule foreach git pull # submodule 里有其他的 submodule 一次更新git submodule foreach git pull origin master # submodule更新git submodule foreach --recursive git submodule initgit submodule foreach --recursive git submodule update 删除文件1git rm -rf node_modules/ remotegit是一个分布式代码管理工具,所以可以支持多个仓库,在git里,服务器上的仓库在本地称之为remote。个人开发时,多源用的可能不多,但多源其实非常有用。 123456git remote add origin1 git@github.com:yanhaijing/data.js.git git remote # 显示全部源 git remote -v # 显示全部源+详细信息 git remote rename origin1 origin2 # 重命名 git remote rm origin # 删除 git remote show origin # 查看指定源的全部信息 标签tag当开发到一定阶段时,给程序打标签是非常棒的功能。 12345678910111213git tag -a v0.1 -m 'my version 1.4' # 新建带注释标签 git push origin --tags # 一次性推送所有分支 git push origin v1.5 # 推送单个tag到orgin源上 git tag -v v1.4.2.1 # 验证标签,验证已经签署的标签git show v1.5 # 看到对应的 GPG 签git tag # 列出现有标签 git tag v0gi.1 # 新建标签 git checkout tagname # 切换到标签 git tag -d v0.1 # 删除标签 git push origin :refs/tags/v0.1 # 删除远程标签 git pull --all # 获取远程所有内容包括tag git --git-dir='<绝对地址>/.git' describe --tags HEAD # 查看本地版本信息 日志log123456789101112131415161718git config format.pretty oneline #显示历史记录时,每个提交的信息只显示一行 git config color.ui true #彩色的 git 输出 git log #查看最近的提交日志 git log --pretty=oneline #单行显示提交日志 git log --graph --pretty=oneline --abbrev-commit git log -num #显示第几条log(倒数) git reflog #查看所有分支的所有操作记录 git log --since=1.day #一天内的提交;你可以给出各种时间格式# 比如说具体的某一天(“2008-01-15”),或者是多久以前(“2 years 1 day 3 minutes ago”) git log --pretty="%h - %s" --author=自己的名字 #查看自己的日志 git log -p -2 #展开两次更新显示每次提交的内容差异 git log --stat #要快速浏览其他协作者提交的更新都作了哪些改动 git log --pretty=format:"%h - %an, %ar : %s"#定制要显示的记录格式 git log --pretty=format:'%h : %s' --date-order --graph # 拓扑顺序展示 git log --pretty=format:'%h : %s - %ad' --date=short #日期YYYY-MM-DD显示 git log <last tag> HEAD --pretty=format:%s # 只显示commit git config --global format.pretty '%h : %s - %ad' --date=short #日期YYYY-MM-DD显示 写入全局配置 选项 说明 选项 说明 %H 提交对象(commit)的完整哈希字串 %ad 作者修订日期(可以用 -date= 选项定制格式) %h 提交对象的简短哈希字串 %ar 作者修订日期,按多久以前的方式显示 %T 树对象(tree)的完整哈希字串 %cn 提交者(committer)的名字 %t 树对象的简短哈希字串 %ce 提交者的电子邮件地址 %P 父对象(parent)的完整哈希字串 %cd 提交日期 %p 父对象的简短哈希字串 %cr 提交日期,按多久以前的方式显示 %an 作者(author)的名字 %s 提交说明 %ae 作者的电子邮件地址 - - Pretty Formats 重写历史1234git commit --amend # 改变最近一次提交 git rebase -i HEAD~3 # 修改最近三次的提交说明,或者其中任意一次 git commit --amend # 保存好了,这些指示很明确地告诉了你该干什么 git rebase --continue # 修改提交说明,退出编辑器。 123pick f7f3f6d changed my name a bitpick 310154e updated README formatting and added blamepick a5f4a0d added cat-file 改成 12pick 310154e updated README formatting and added blamepick f7f3f6d changed my name a bit 删除仓库12cd ..rm -rf repo.git Github官方教程 其它12git help * # 获取命令的帮助信息 git status # 获取当前的状态,非常有用,因为git会提示接下来的能做的操作 报错问题解决1. git fatal: protocol error: bad line length character: No s 解决办法:更换remote地址为 http/https 的 2. The requested URL returned error: 403 Forbidden while accessing 解决github push错误的办法: 12345678910#vim 编辑器打开 当前项目中的config文件vim .git/config#修改[remote "origin"] url = https://github.com/jaywcjlove/example.git #为下面代码[remote "origin"] url = https://jaywcjlove@github.com/jaywcjlove/example.git 3. git status 显示中文问题 在查看状态的时候 git status 如果是中文就显示下面的情况 1\344\272\247\345\223\201\351\234\200\346\261\202 解决这个问题方法是: 1git config --global core.quotepath false 参考资料 Git官网 Github 15分钟学习Git Git参考手册 Git简明手册 Git Magic Git Community Book 中文版 Pro Git 图解Git git-简明指南 learnGitBranching 在线学习工具 初级教程 廖雪峰的Git教程 蒋鑫老师将带你入github的大门 git详解 oschina教程 Git撤销一切,汇总各种回滚撤销的场景 Git 教程 | 菜鸟教程runoob.com Git 本地仓库和裸仓库 沉浸式学 Git Git进阶用法,主要是rebase高级用法]]></content>
<categories>
<category>实践教程</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JS字符串和Array方法]]></title>
<url>%2Fblog%2F2018%2F05%2F30%2FJS%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%92%8CArray%E6%96%B9%E6%B3%95%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[String对象属性length属性length属性是字符串中的一个基本属性了,可以获取字符串长度,要注意的是js的中文每个汉字也只代表一个字符。1234var str1 = 'abc';var str2 = '中国';console.log(str1.length); //3console.log(str2.length); //2 prototype属性prototype属性用来给对象添加属性或方法,并且添加的方法或属性在所有的实例上共享。可以用来扩展js内置对象,如给字符串添加了一个去除两边空格的方法:123String.prototype.trim = function(){ return this.replace(/^\s*|\s*$/g, '');} 1234567891011121314封装成函数 function trim(str,type) { // [type]类型 var type=type||"b"; if(type=="b"){ return str.replace(/^\s*|\s*$/g,""); // 去除两边的空白 }else if(type=="l"){ return str.replace(/^\s*/g,""); // 去除左边空白 }else if(type=="r"){ return str.replace(/\s*$/g,""); // 去除右边空白 }else if(type=="a"){ return str.replace(/\s*/g,""); // 去除所有空白 }} String对象方法1. 获取类方法(1) charAt()charAt()方法可用来获取指定位置的字符串,参数为字符串索引值,从0开始到string.length - 1,若不在这个范围将返回一个空字符串123var str = 'abcde';console.log(str.charAt(2)); //返回cconsole.log(str.charAt(8)); //返回空字符串 (2) charCodeAt()charCodeAt()方法可返回指定位置的字符的Unicode编码。charCodeAt()方法与charAt()方法类似,都需要传入一个索引值作为参数,区别是前者返回指定位置的字符的编码,而后者返回的是字符子串12var str = 'abcde';console.log(str.charCodeAt(0)); //返回97 2. 查找类方法(1) indexOf()(数组中也有该方法)stringObject.indexOf(searchvalue,fromindex) indexOf()用来检索指定的字符串值在字符串中首次出现的位置。它可以接收两个参数,searchvalue表示要查找的子字符串,fromindex表示查找的开始位置,省略的话则从开始位置进行检索。1234var str = 'abcdeabcde';console.log(str.indexOf('a')); // 返回0console.log(str.indexOf('a', 3)); // 返回5console.log(str.indexOf('bc')); // 返回1 (2) lastIndexOf()(数组中也有该方法)stringObject.lastIndexOf(searchvalue,fromindex) lastIndexOf返回的是一个指定的子字符串值最后出现的位置,其检索顺序是从后向前。与indexOf()类似,这两个方法在搜索到第一个匹配的子字符串后就停止运行,所以如果想找到字符串中所有的子字符串出现的位置,可以循环调用。1234var str = 'abcdeabcde';console.log(str.lastIndexOf('a')); // 返回5console.log(str.lastIndexOf('a', 3)); // 返回0 从第索引3的位置往前检索console.log(str.lastIndexOf('bc')); // 返回6 (3) search()stringObject.search(substr)stringObject.search(regexp) search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。它会返回第一个匹配的子字符串的起始位置,如果没有匹配的,则返回-1。1234var str = 'abcDEF';console.log(str.search('c')); //返回2console.log(str.search('d')); //返回-1console.log(str.search(/d/i)); //返回3 (4) match()stringObject.match(substr)stringObject.match(regexp) match()方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。 如果参数中传入的是子字符串或是没有进行全局匹配的正则表达式,那么match()方法会从开始位置执行一次匹配,如果没有匹配到结果,则返回null。否则则会返回一个数组,该数组的第0个元素存放的是匹配文本,除此之外,返回的数组还含有两个对象属性index和input,分别表示匹配文本的起始字符索引和stringObject 的引用(即原字符串)。1234var str = '1a2b3c4d5e';console.log(str.match('h')); //返回nullconsole.log(str.match('b')); //返回["b", index: 3, input: "1a2b3c4d5e"]console.log(str.match(/b/)); //返回["b", index: 3, input: "1a2b3c4d5e"] 如果参数传入的是具有全局匹配的正则表达式,那么match()从开始位置进行多次匹配,直到最后。如果没有匹配到结果,则返回null。否则则会返回一个数组,数组中存放所有符合要求的子字符串,并且没有index和input属性。在字符串上调用这个方法本质上与调用RegExp的exec()方法相同。123var str = '1a2b3c4d5e';console.log(str.match(/h/g)); //返回nullconsole.log(str.match(/\d/g)); //返回["1", "2", "3", "4", "5"] ES6新增includes()、startsWith()、endsWith()includes():返回布尔值,表示是否找到了参数字符串startsWith():返回布尔值,表示参数字符串是否在源字符串的头部endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部这三个方法的参数与indexOf(),lastIndexOf()一样1234var s = 'Hello world';s.startsWith('world',6); // trues.endsWith('Hello',5); // trues.includes('Hello',6); //false 注意:使用第2个参数n时,endsWith的行为与其他两个方法有所不同。它针对前面n个字符,而其他两个方法针对从第n个位置开始直到字符串结束的字符。 3. 截取类方法(1)substring()stringObject.substring(start,end) substring()是最常用到的字符串截取方法,它可以接收两个参数(参数不能为负值),分别是要截取的开始位置和结束位置,它将返回一个新的字符串,其内容是从start处到end-1处的所有字符。若结束参数(end)省略,则表示从start位置一直截取到最后。1234var str = 'abcdefg';console.log(str.substring(1, 4)); //返回bcdconsole.log(str.substring(1)); //返回bcdefgconsole.log(str.substring(-1)); //返回abcdefg,传入负值时会视为0 (2) slice()(数组也有)stringObject.slice(start,end) slice()方法与substring()方法非常类似,它传入的两个参数也分别对应着开始位置和结束位置。而区别在于,slice()中的参数可以为负值,如果参数是负数,则该参数规定的是从字符串的尾部开始算起的位置。也就是说,-1 指字符串的最后一个字符。12345var str = 'abcdefg';console.log(str.slice(1, 4)); //返回bcdconsole.log(str.slice(-3, -1)); //返回efconsole.log(str.slice(1, -1)); //返回bcdefconsole.log(str.slice(-1, -3)); //返回空字符串,若传入的参数有问题,则返回空 (3) substr()stringObject.substr(start,length) substr()方法可在字符串中抽取从start下标开始的指定数目的字符。其返回值为一个字符串,包含从 stringObject的start(包括start所指的字符)处开始的length个字符。如果没有指定 length,那么返回的字符串包含从start到stringObject的结尾的字符。另外如果start为负数,则表示从字符串尾部开始算起。1234var str = 'abcdefg';console.log(str.substr(1, 3)) //返回bcdconsole.log(str.substr(2)) //返回cdefgconsole.log(str.substr(-2, 4)) //返回fg,目标长度较大的话,以实际截取的长度为准 4. 其他方法(1) replace()方法 stringObject.replace(regexp/substr,replacement) replace()方法用来进行字符串替换操作,它可以接收两个参数,前者为被替换的子字符串(可以是正则),后者为用来替换的文本。 如果第一个参数传入的是子字符串或是没有进行全局匹配的正则表达式,那么replace()方法将只进行一次替换(即替换最前面的),返回经过一次替换后的结果字符串。123var str = 'abcdeabcde';console.log(str.replace('a', 'A'));console.log(str.replace(/a/, 'A')); 如果第一个参数传入的全局匹配的正则表达式,那么replace()将会对符合条件的子字符串进行多次替换,最后返回经过多次替换的结果字符串。123var str = 'abcdeabcdeABCDE';console.log(str.replace(/a/g, 'A')); //返回AbcdeAbcdeABCDEconsole.log(str.replace(/a/gi, '$')); //返回$bcde$bcde$BCDE (2) split()方法 stringObject.split(separator,howmany) split()方法用于把一个字符串分割成字符串数组。第一个参数separator表示分割位置(参考符),第二个参数howmany表示返回数组的允许最大长度(一般情况下不设置)。12345678var str = 'a|b|c|d|e';console.log(str.split('|')); //返回["a", "b", "c", "d", "e"]console.log(str.split('|', 3)); //返回["a", "b", "c"]console.log(str.split('')); //返回["a", "|", "b", "|", "c", "|", "d", "|", "e"]也可以用正则来进行分割var str = 'a1b2c3d4e';console.log(str.split(/\d/)); //返回["a", "b", "c", "d", "e"] (3) toLowerCase()和toUpperCase()toLowerCase()方法可以把字符串中的大写字母转换为小写,toUpperCase()方法可以把字符串中的小写字母转换为大写123var str = 'JavaScript';console.log(str.toLowerCase()); //返回javascriptconsole.log(str.toUpperCase()); //返回JAVASCRIPT Array常用方法(主要是遍历方法)forEachforEach() 方法指定数组的每项元素都执行一次传入的函数,返回值为undefined。语法:arr.forEach(fn, thisArg)fn 表示在数组每一项上执行的函数,接受三个参数: value 当前正在被处理的元素的值index 当前元素的数组索引array 数组本身 thisArg 可选,用来当做fn函数内的this对象。forEach 将为数组中每一项执行一次fn函数,那些已删除,新增或者从未赋值的项将被跳过(但不包括值为 undefined 的项)。遍历过程中,fn会被传入上述三个参数。 everyevery() 方法使用传入的函数测试所有元素,只要其中有一个函数返回值为 false,那么该方法的结果为 false;如果全部返回 true,那么该方法的结果才为 true。因此 every 方法存在如下规律: 若需检测数组中存在元素大于100 (即 one > 100),那么我们需要在传入的函数中构造 “false” 返回值 (即返回 item <= 100),同时整个方法结果为 false 才表示数组存在元素满足条件;(简单理解为:若是单项判断,可用 one false ===> false) 若需检测数组中是否所有元素都大于100 (即all > 100)那么我们需要在传入的函数中构造 “true” 返回值 (即返回 item > 100),同时整个方法结果为 true 才表示数组所有元素均满足条件。(简单理解为:若是全部判断,可用 all true ===> true) 以下是鸭式辨型的写法:12345var o = {0:10, 1:8, 2:25, length:3};var bool = Array.prototype.every.call(o,function(value, index, obj){ return value >= 8;},o);console.log(bool); somesome() 方法刚好同 every() 方法相反,some测试数组元素时,只要有一个函数返回值为 true,则该方法返回true,若全部返回 false,则该方法返回false。some方法存在如下规律: 若需检测数组中存在元素大于100 (即 one > 100),那么我们需要在传入的函数中构造 “true” 返回值 (即返回 item > 100),同时整个方法结果为 true 才表示数组存在元素满足条件;(简单理解为:若是单项判断,可用 one true ===> true) 若需检测数组中是否所有元素都大于100(即 all > 100),那么我们需要在传入的函数中构造 “false” 返回值 (即返回 item <= 100),同时整个方法结果为 false 才表示数组所有元素均满足条件。(简单理解为:若是全部判断,可用 all false ===> false) 你注意到没有,some方法与includes方法有着异曲同工之妙,他们都是探测数组中是否拥有满足条件的元素,一旦找到,便返回true。多观察和总结这种微妙的关联关系,能够帮助我们深入理解它们的原理。 filterfilter() 方法使用传入的函数测试所有元素,并返回所有通过测试的元素组成的新数组。它就好比一个过滤器,筛掉不符合条件的元素。123456语法:arr.filter(fn, thisArg)var array = [18, 9, 10, 35, 80];var array2 = array.filter(function(value, index, array){ return value > 20;});console.log(array2); // [35, 80] mapmap()方法遍历数组,使用传入函数处理数组每个元素,并返回函数的返回值组成的新数组。语法:arr.map(fn, thisArg) 参数:第一个参数:函数 函数接收三个参数 1、数组的项 2、该项在数组中的位置 3、数组对象本身第二个参数:第一个参数的执行环境(this指向) 返回值: forEach() 无返回值 every() 对数组运行给定函数,如果该函数对每一项都返回true,则返回true some() 对数组运行给定函数,如果该函数对任意一项返回true,则返回true filter() 对数组执行给定函数,返回该函数返回true的项组成的数组 map() 对数组执行给定函数,返回每次函数调用结果组成的数组 reducereduce() 方法接收一个方法作为累加器,数组中的每个值(从左至右) 开始合并,最终为一个值。语法:arr.reduce(fn, initialValue)fn 表示在数组每一项上执行的函数,接受四个参数: previousValue 上一次调用回调返回的值,或者是提供的初始值value 数组中当前被处理元素的值index 当前元素在数组中的索引array 数组自身 initialValue 指定第一次调用 fn 的第一个参数。当 fn 第一次执行时: 如果 initialValue 在调用 reduce 时被提供,那么第一个 previousValue 将等于 initialValue,此时 item 等于数组中的第一个值;如果 initialValue 未被提供,那么 previousVaule 等于数组中的第一个值,item 等于数组中的第二个值。此时如果数组为空,那么将抛出 TypeError。如果数组仅有一个元素,并且没有提供 initialValue,或提供了 initialValue 但数组为空,那么fn不会被执行,数组的唯一值将被返回。 【ES6】Array.of() 用于创建数组,用法和 new Array() 一样。弥补 Array() 构造函数的不足(即参数不同,行为不同),Array.of() 的行为始终一致,将传入的值作为数组的项,产生数组 参数:任意数量任意值返回值:创建的数组 【ES6】Array.from(obj, func, context) 用于将 类数组对象(拥有length属性的对象) 和 可遍历对象(部署iterable接口的对象,包括 Set/Map) 转为真正的数组 参数:{Object} obj 要转为数组的对象{Function} func 一个函数,功能类似于数组的map方法,对每一个对象属性执行该函数,并返回由该函数的返回值组成的数组{Object} context 第二个函数参数的执行环境(this指向)返回值:生成的数组 【ES6】entries(),keys()和values()描述:entries(),keys() 和 values() 都用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用 for…of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。 【ES7】includes()查找数组中是否包含给定值 参数:第一个参数:要查找的值第二个参数:查找的起始位置,默认是0,负数表示倒数,查出范围会重置为0返回值:true包含, false不包含includes 相比于indexOf的优势有两点:1、更加语义化,不需要判断返回值是否为 -1。2、由于 indexOf 底层在判断是否相等时使用的是全等操作符 ===,这会导致使用 indexOf 查找 NaN 时查不到,而 includes 则不存在这样的问题 总结pop,push,reverse,shift,sort,splice,unshift,fill(ES6),copyWithin(ES6) 会改变原数组join,concat,indexOf,lastIndexOf,slice,toString,includes(ES7) 不会改变原数组map,filter,some,every,reduce,forEach和ES6新增的方法entries、find、findIndex、keys、values这些迭代方法不会改变原数组 数组的这些方法之间存在很多共性,比如:所有插入元素的方法, 比如 push、unshift,一律返回数组新的长度;所有删除元素的方法,比如 pop、shift、splice 一律返回删除的元素,者返回删除的多个元素组成的数组;部分遍历方法,比如forEach、every、some、filter、map、find、findIndex,它们都包含function(value,index,array){} 和thisArg这样两个形参。 Array.prototype的所有方法均具有鸭式辨型这种神奇的特性。它们不止可以用来处理数组对象,还可以处理类数组对象。例如javascript中一个纯天然的类数组对象字符串(String),像join方法(不改变当前对象自身)就完全适用,可惜的是Array.prototype中很多方法均会去试图修改当前对象的length属性,比如说pop、push、shift, unshift方法,操作String对象时,由于String对象的长度本身不可更改,这将导致抛出TypeError错误,Array.prototype本身就是一个数组,并且它的长度为0。 几个注意点:shift,pop会返回那个被删除的元素splice 会返回被删除元素组成的数组,或者为空数组push 会返回新数组长度some 在有true的时候停止every 在有false的时候停止上述的迭代方法可以在最后追加一个参数thisArg,它是执行 callback 时的 this 值 参考链接:http://riny.net/2012/the-summary-of-javascript-string]]></content>
<categories>
<category>Javascript</category>
</categories>
</entry>
<entry>
<title><![CDATA[Javascript难点]]></title>
<url>%2Fblog%2F2018%2F05%2F15%2FJavaScript%E9%9A%BE%E7%82%B9%2F</url>
<content type="text"><![CDATA[JavaScript引擎是单线程运行的,浏览器无论在什么时候都有且只有一个线程在运行JavaScript程序,浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。浏览器内核的实现至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件,单线程的JavaScript引擎与另外那些线程进行互动通信,虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异。 原型与原型链说到原型,就得提一下构造函数,首先看下面一个简单的例子:1234567function Dog(name,age){ this.name = name; this.age = age;}let dog1 = new Dog("哈士奇",3);let dog2 = new Dog("泰迪",2); 首先创造空的对象,再让this指向这个对象,通过this.name进行赋值,最终返回this,这其实也是new 一个对象的过程。 其实: let obj = {} 是 let obj = new Object()的语法糖; let arr = [] 是 let arr = new Array()的语法糖; function Dog(){} 是 let Dog = new Fucntion()的语法糖。 在js中,所有对象都是Object的实例,并继承Object.prototype的属性和方法,但是有一些是隐性的。 看一下原型的规则: 1.所有的引用类型(包括数组,对象,函数)都具有对象特性;可自由扩展属性。123456var obj = {};obj.attribute = "原型";var arr = [];arr.attribute = "作用域";function fn1 () {}fn1.attribute = "闭包"; 2.所有的引用类型(包括数组,对象,函数)都有隐性原型属性(proto),值也是一个普通的对象。1console.log(obj.__proto__); 3.所有的函数,都有一个prototype属性,值也是一个普通的对象。1console.log(obj.prototype); 4.所有的引用类型的proto属性值都指向构造函数的prototype属性值。1console.log(obj.__proto__ === Object.prototype); // true 5.当试图获取对象属性时,如果对象本身没有这个属性,那就会去他的proto(prototype)中去寻找。 123456789101112function Dog(name){ this.name = name; } Dog.prototype.callName = function (){ console.log(this.name,"wang wang"); }let dog1 = new Dog("Three Mountain"); dog1.printName = function (){ console.log(this.name);}dog1.callName(); // Three Mountain wang wangdog1.printName(); // Three Mountain 原型链如下图:找一个属性,首先会在f.proto中去找,因为属性值为一个对象,那么就会去f.proto.proto去找,同理如果还没找到,就会一直向上去查找,直到结果为null为止,这个串起来的链即为原型链。 作用域及闭包说到作用域,肯定会想到是执行上下文。每个函数都有自己的excution context,和variable object。这些环境用于储存上下文中的变量,函数声明,参数等。只有函数才能制造作用域。PS:for if else 不能创造作用域1234567console.log(a) ; // undefinedvar a = 1;//可理解为var a;console.log(a); // undefineda = 1; 执行console.log时,a只是被声明出来,并没有赋值;所以结果当然是undefined。 this 本质上来说,在js里this是一个指向函数执行环境的指针,this永远指向最后调用它的对象,并且在执行时才能获取值,定义是无法确认它的值。箭头函数没有它自己的this值,箭头函数内的this值继承自外围作用域。1234567891011121314var a = { name : "A", fn : function (){ console.log (this.name) } } a.fn() // this === a a 调用了fn() 所以此时this为aa.fn.call ({name : "B"}) // this === {name : "B"} 使用call(),将this的值指定为{name:"B"}var fn1 = a.fn fn1() // this === window 虽然指定fn1 = a.fn,但是调用是有window调用,所以 this 为window this有多种使用场景,下面会主要介绍4个使用场景: 1.作为构造函数执行123456function Student(name,age) { this.name = name // this === s this.age = age // this === s //return this}var s = new Student("hpu",22) 首先new 字段会创建一个空的对象,然后调用apply()函数,将this指向这个空对象。这样的话,函数内部的this就会被空对象代替。 2.作为普通函数执行1234function fn () { console.log (this) // this === window}fn () 3.作为对象属性执行1234567var obj = { name : "A", printName : function () { console.log (this.name) // this === obj }}obj.printName () 4.call(),apply(),bind() 三个函数可以修改this的指向,具体往下看:12345678910var name = "小明" , age = "17"var obj = { name : "安妮", objAge : this.age, fun : function () { console.log ( this.name + "今年" + this.age ) } } console.log(obj.objAge) // 17 obj.fun() // 安妮今年undefined 12345678910111213var name = "小明" , age = "17" var obj = { name : "安妮", objAge :this.age, fun : function (like,dislike) { console.log (this.name + "今年" + this.age ,"喜欢吃" + like + "不喜欢吃" + dislike) } } var a = { name : "Jay", age : 23 } obj.fun.call(a,"苹果","香蕉") // Jay今年23 喜欢吃苹果不喜欢吃香蕉 obj.fun.apply(a,["苹果","香蕉"]) // Jay今年23 喜欢吃苹果不喜欢吃香蕉 obj.fun.bind(a,"苹果","香蕉")() // Jay今年23 喜欢吃苹果不喜欢吃香蕉 首先call,apply,bind第一个参数都是this指向的对象,call和apply如果第一个参数指向null或undefined时,那么this会指向windows对象。 call,apply,bind的执行方式如上例所示。call,apply都是改变上下文中的this,并且是立即执行的。bind方法可以让对应的函数想什么时候调用就什么时候调用。 闭包 闭包的概念很抽象,看下面的例子理解什么叫闭包12345678910function a(){ var n = 0; this.fun = function () { n++; console.log(n); };}var c = new a();c.fun(); //1c.fun(); //2 闭包就是能够读取其他函数内部变量的函数。在js中只有函数内部的子函数才能读取局部变量。所以可以简单的理解为:定义在内部函数的函数。 用途主要有两个: 1)前面提到的,读取函数内部的变量。 2)让变量值始终保持在内存中。 异步和单线程先感受下异步12345console.log("start");setTimeout(function () { console.log("medium");}, 1000);console.log("end"); 使用异步后,打印的顺序为 start-> end->medium,因为没有阻塞。 异步产生原因 首先因为js为单线程,也就是说CPU同一时间只能处理一个事务。得按顺序,一个一个处理。 如上例所示,第一步:执行第一行打印 “start”;第二步:执行setTimeout,将其中的函数分存起来,等待时间结束后执行;第三步:执行最后一行,打印“end”;第四步:处于空闲状态,查看暂存中,是否有可执行的函数;第五步:执行分存函数。 js引擎单线程 js的主要用途是与用户互动,以及操作DOM,这决定它只能是单线程。例:一个线程要添加DOM节点,一个线程要删减DOM节点,容易造成分歧。 为了更好使用多CPU,H5提供了web Worker 标准,允许js创建多线程,但是子线程受到主线程控制,而且不得操作DOM。 任务列队 单线程就意味着,所有的任务都要排队,前一个结束,才会执行后面的任务。如果列队是因为计算量大,CPU忙不过来,倒也算了。但是更多的时候,CPU是闲置的,因为IO设备处理得很慢,例如 ajax读取网络数据。js设计者便想到,主线程完全可以不管IO设备,将其挂起,然后执行后面的任务。等后面的任务结束掉,在反过头来处理挂起的任务。 梳理一下: 1)所有的同步任务都在主线程上执行,形成一个执行栈 2)除了主线程之外,还存在一个任务列队,只要一步任务有了运行结果,就在任务列队中植入一个时间 3)主线程完成所有任务,就会读取列队任务,并将其执行 4)重复上面三步 只要主线程空了,就会读取任务列队,这就是js的运行机制,也被称为 event loop(事件循环)。]]></content>
<categories>
<category>Javascript</category>
</categories>
<tags>
<tag>grammer</tag>
</tags>
</entry>
<entry>
<title><![CDATA[集群和负载均衡]]></title>
<url>%2Fblog%2F2018%2F05%2F06%2F%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%9B%86%E7%BE%A4%E5%92%8C%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%2F</url>
<content type="text"><![CDATA[一个庞大的高访问量的业务系统,需要高性能、可靠的服务器框架支撑。高性能要求服务器在巨大压力下仍然高速运行,读写返回正确的业务信息,前端用户体验良好。可靠性要求服务器出现宕机、罢工等情况,可以及时恢复服务器正常工作状态,支持业务系统24小时健康运行。使用缓存、读写分离技术提高服务器访问资源速度,解决大访问量资源拥堵问题;使用负载均衡与高可用技术提高服务器响应速度以及服务器稳定性,解决服务器处理大用户量请求问题以及服务器宕机的及时恢复能力。同时,需要部署运维监控平台,监控服务器上服务程序与资源使用情况。 这样一个业务系统基础架构可以划分下面几个模块:负载均衡与代理、Web主站服务、APP接口服务、图片服务器、数据库与缓存服务。这里负载均衡和代理主要是两个作用:实现多台机器按照算法轮流工作,分担服务压力,当一台机器宕机或者罢工,其他机器也可以继续运行;代理隐藏服务内部真实结构,多台对外提供统一地址,运行相同业务系统,后文会详细介绍负载均衡。 主站框架是一个Web服务器(apache、tomcat、nginx等)集群,集群中全部机器运行相同业务系统。通过负载均衡代理与客户端通讯,每一次通讯只有一台机器为当前客户端服务。需要解决session共享问题,否则将会丢失用户的登录状态,在用户体验方面有逻辑错误。常见的共享session方法有数据库共享、cookie共享、内存共享。使用最多的是memcache共享方式,memcache把多个服务器的共享内存拼接成一块大的内存使用,保存用户的session信息。Tomcat服务集群可以简单配置memcache共享内存,PHP中也可以直接配置设置memcache共享内存。 Nginx负载解决session的方式:ip_hash、sticky。ip_hash根据IP保存响应服务器,在一张存储表单中,IP对应上次访问的服务器,以后来自于该IP的访问都使用这个这台服务器,解决session问题,存在局限性影响负载均衡的功能。Sticky使用cookie的方式解决session共享问题,其实是避开session共享。Sticky把cookie与服务器绑定,存储于客户端缓存当中,客户端再次访问时直接进入到cookie绑定的服务器,关闭客户端session也随之消失。 缓存服务可以提高服务的响应速度,处理及时性要求高的数据时,数据首先进入缓存,然后通过消息队列写入到数据库。从数据库查询出来的实时数据也可以保存在缓存中,在缓存中直接提供用户访问,执行用户操作数据请求,再把数据返回数据库。 Redis是一款出色的缓存服务器,内存级别的键值对数据库,支持丰富数据结构,数据库操作命令也是很齐全。最重要是Redis操作速度非常快,满足缓存服务器需求。Redis提供单机的分片集群,单机硬件性能要求比较高。Redis也可以进行分布式部署,搭建分布式缓存服务。 安全配置:隐藏常见系统服务信息、配置用户权限、开启防火墙、关闭无用系统服务、定期更新系统风险评估:进行渗透测试、漏洞扫描安全防御:配置IDS\IPS、进行源代码审计、DDOS防御、恶意代码检测 配置运维监控平台,实时监控服务器的健康状况。CPU、内存、磁盘、输入输出、网络性能等参数,配置报警规则,触发报警是立即调用API接口或者第三方回调,发送报警信息到邮箱、微信等。同时,自定监控数据项,检测Web服务、数据库服务、后台程序等运行状态,连续出现拒绝服务行为立刻报警,通知管理员。 负载均衡介绍负载均衡(Load Balance)是建立在现有网络结构之上,提供一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。就是分摊任务到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。 负载均衡,核心就是网络流量分发,服务器负载均衡根据LB设备处理到的报文层次,分为四层服务器负载均衡(TCP,UDP)和七层负载均衡(HTTP,HTTPS等),四层处理到IP包的IP头,不解析报文四层以上载荷(L4 SLB),基本就是根据连接信息(TCP)或者本身的特征(源IP,目标IP)等做;七层处理到报文载荷部分,比如HTTP,RTSP,SIP报文头,有时也包括报文内容部分,可以用域名(HTTP头里的Host),URL,Cookie,Header这些信息来做。四层LB可以说是作为路由进行流量转发,七层LB常称作代理。 负载均衡根据所采用的设备对象(软硬件负载均衡),应用的OSI网络层次(网络层次上的负载均衡),及应用的地理结构(本地/全局负载均衡)等来分类。负载均衡现在比较新的做法是用dpdk这种内核bypass方案做的负载均衡,绕过了linux内核比较复杂的网络协议栈,因此性能会有明显的提升(轻松跑满万兆网卡)。 根据负载均衡所作用在 OSI 模型的位置不同,负载均衡可以大概分为以下几类: 二层负载均衡(mac) 根据OSI模型分的二层负载,一般是用虚拟mac地址方式,外部对虚拟MAC地址请求,负载均衡接收后分配后端实际的MAC地址响应。 三层负载均衡(ip) 一般采用虚拟IP地址方式,外部对虚拟的ip地址请求,负载均衡接收后分配后端实际的IP地址响应。 四层负载均衡(tcp) 在三层负载均衡的基础上,用ip+port接收请求,再转发到对应的机器。 七层负载均衡(http) 根据虚拟的url或IP,主机名接收请求,再转向相应的处理服务器。反向代理,就是通过代理来做(Nginx)。由于流量都会过LB,因此可以做到比较精细的流量分发(比如各种权重,七层的各种转发规则)。坏处就是代理本身可能成为瓶颈,以及过了一层代理造成网络延时的增加,而代理本身也会有一定成本,因此实现成本较高。 在实际应用中,比较常见的就是四层负载及七层负载。这里也重点说下这两种负载。 一、四层负载均衡(基于IP+端口的负载均衡)所谓四层负载均衡,也就是主要通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 在三层负载均衡的基础上,通过发布三层的IP地址(VIP),然后加四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,转发至后台服务器,并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理。 以常见的TCP为例,负载均衡设备在接收到第一个来自客户端的SYN 请求时,即通过上述方式选择一个最佳的服务器,并对报文中目标IP地址进行修改(改为后端服务器IP),直接转发给该服务器。TCP的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下,为保证服务器回包可以正确返回给负载均衡设备,在转发报文的同时可能还会对报文原来的源地址进行修改。 对应的负载均衡器称为四层交换机(L4 switch),主要分析IP层及TCP/UDP层,实现四层负载均衡。此种负载均衡器不理解应用协议(如HTTP/FTP/MySQL等等)要处理的流量进行NAT处理,转发至后台服务器,并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理。 实现四层负载均衡的软件有:F5:硬件负载均衡器,功能很好,但是成本很高。lvs:重量级的四层负载软件nginx:轻量级的四层负载软件,带缓存功能,正则表达式较灵活haproxy:模拟四层转发,较灵活 二、七层负载均衡(基于虚拟的URL或主机IP的负载均衡)所谓七层负载均衡,也称为“内容交换”,也就是主要通过报文中的真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 在四层负载均衡的基础上(没有四层是绝对不可能有七层的),再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。举个例子,如果你的Web服务器分成两组,一组是中文语言的,一组是英文语言的,那么七层负载均衡就可以当用户来访问你的域名时,自动辨别用户语言,然后选择对应的语言服务器组进行负载均衡处理。 以常见的TCP为例,负载均衡设备如果要根据真正的应用层内容再选择服务器,只能先代理最终的服务器和客户端建立连接(三次握手)后,才可能接受到客户端发送的真正应用层内容的报文,然后再根据该报文中的特定字段,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。负载均衡设备在这种情况下,更类似于一个代理服务器。负载均衡和前端的客户端以及后端的服务器会分别建立TCP连接。所以从这个技术原理上来看,七层负载均衡明显的对负载均衡设备的要求更高,处理七层的能力也必然会低于四层模式的部署方式。 对应的负载均衡器称为七层交换机(L7 switch),除了支持四层负载均衡以外,还有分析应用层的信息,如HTTP协议URI或Cookie信息,实现七层负载均衡。此种负载均衡器能理解应用协议。 实现七层负载均衡的软件有:haproxy:天生负载均衡技能,全面支持七层代理,会话保持,标记,路径转移;nginx:只在http协议和mail协议上功能比较好,性能与haproxy差不多;apache:功能较差Mysql proxy:功能尚可 四层和七层负载均衡的区别,举个例子形象的说明:四层负载均衡就像银行的自助排号机,每一个达到银行的客户根据排号机的顺序,选择对应的窗口接受服务;而七层负载均衡像银行大堂经理,先确认客户需要办理的业务,再安排排号。这样办理理财、存取款等业务的客户,会根据银行内部资源得到统一协调处理,加快客户业务办理流程。 特征 四层负载均衡 七层负载均衡 基于 基于IP+Port的 基于虚拟的URL或主机IP等 类似 路由器 代理服务器 握手次数 1次 2次 复杂度 低 高 性能 高,无需解析内容 中,需要算法识别 URL,Cookie 和 HTTP head 等信息 安全性 低,无法识别 DDoS等攻击 高, 可以防御SYN cookie以SYN flood等 额外 无 会话保持,图片压缩,防盗链等 总结:从上面的对比看来四层负载与七层负载最大的区别就是效率与功能的区别。四层负载架构设计比较简单,无需解析具体的消息内容,在网络吞吐量及处理能力上会相对比较高,而七层负载均衡的优势则体现在功能多,控制灵活强大。在具体业务架构设计时,使用七层负载或者四层负载还得根据具体的情况综合考虑。 以下介绍下四层七层负载均衡实现方式 一、http重定向当http代理(比如浏览器)向web服务器请求某个URL后,web服务器可以通过http响应头信息中的Location标记来返回一个新的URL。这意味着HTTP代理需要继续请求这个新的URL,完成自动跳转。 性能缺陷:1、吞吐率限制主站点服务器的吞吐率平均分配到了被转移的服务器。现假设使用RR(Round Robin)调度策略,子服务器的最大吞吐率为1000reqs/s,那么主服务器的吞吐率要达到3000reqs/s才能完全发挥三台子服务器的作用,那么如果有100台子服务器,那么主服务器的吞吐率可想而知得有大?相反,如果主服务的最大吞吐率为6000reqs/s,那么平均分配到子服务器的吞吐率为2000reqs/s,而现子服务器的最大吞吐率为1000reqs/s,因此就得增加子服务器的数量,增加到6个才能满足。 2、重定向访问深度不同有的重定向一个静态页面,有的重定向相比复杂的动态页面,那么实际服务器的负载差异是不可预料的,而主站服务器却一无所知。因此整站使用重定向方法做负载均衡不太好。 我们需要权衡转移请求的开销和处理实际请求的开销,前者相对于后者越小,那么重定向的意义就越大,例如下载。你可以去很多镜像下载网站试下,会发现基本下载都使用了Location做了重定向。 二、DNS负载均衡DNS负责提供域名解析服务,当访问某个站点时,实际上首先需要通过该站点域名的DNS服务器来获取域名指向的IP地址,在这一过程中,DNS服务器完成了域名到IP地址的映射,同样,这样映射也可以是一对多的,这时候,DNS服务器便充当了负载均衡调度器,它就像http重定向转换策略一样,将用户的请求分散到多台服务器上,但是它的实现机制完全不同。 相比http重定向,基于DNS的负载均衡完全节省了所谓的主站点,或者说DNS服务器已经充当了主站点的职能。但不同的是,作为调度器,DNS服务器本身的性能几乎不用担心。因为DNS记录可以被用户浏览器或者互联网接入服务商的各级DNS服务器缓存,只有当缓存过期后才会重新向域名的DNS服务器请求解析。也说是DNS不存在http的吞吐率限制,理论上可以无限增加实际服务器的数量。 特性:1、可以根据用户IP来进行智能解析。DNS服务器可以在所有可用的A记录中寻找离用记最近的一台服务器。 2、动态DNS:在每次IP地址变更时,及时更新DNS服务器。当然,因为缓存,一定的延迟不可避免。 不足:1、没有用户能直接看到DNS解析到了哪一台实际服务器,加服务器运维人员的调试带来了不便。 2、策略的局限性。例如你无法将HTTP请求的上下文引入到调度策略中,而在前面介绍的基于HTTP重定向的负载均衡系统中,调度器工作在HTTP层面,它可以充分理解HTTP请求后根据站点的应用逻辑来设计调度策略,比如根据请求不同的URL来进行合理的过滤和转移。 3、如果要根据实际服务器的实时负载差异来调整调度策略,这需要DNS服务器在每次解析操作时分析各服务器的健康状态,对于DNS服务器来说,这种自定义开发存在较高的门槛,更何况大多数站点只是使用第三方DNS服务。 4、DNS记录缓存,各级节点的DNS服务器不同程序的缓存会让你晕头转向。 5、基于以上几点,DNS服务器并不能很好地完成工作量均衡分配,最后,是否选择基于DNS的负载均衡方式完全取决于你的需要。 三、反向代理负载均衡这个肯定大家都有所接触,因为几乎所有主流的Web服务器都热衷于支持基于反向代理的负载均衡。它的核心工作就是转发HTTP请求。 相比前面的HTTP重定向和DNS解析,反向代理的调度器扮演的是用户和实际服务器中间人的角色: 1、任何对于实际服务器的HTTP请求都必须经过调度器 2、调度器必须等待实际服务器的HTTP响应,并将它反馈给用户(前两种方式不需要经过调度反馈,是实际服务器直接发送给用户) 特性:1、调度策略丰富。例如可以为不同的实际服务器设置不同的权重,以达到能者多劳的效果。 2、对反向代理服务器的并发处理能力要求高,因为它工作在HTTP层面。 3、反向代理服务器进行转发操作本身是需要一定开销的,比如创建线程、与后端服务器建立TCP连接、接收后端服务器返回的处理结果、分析HTTP头部信息、用户空间和内核空间的频繁切换等,虽然这部分时间并不长,但是当后端服务器处理请求的时间非常短时,转发的开销就显得尤为突出。例如请求静态文件,更适合使用前面介绍的基于DNS的负载均衡方式。 4、反向代理服务器可以监控后端服务器,比如系统负载、响应时间、是否可用、TCP连接数、流量等,从而根据这些数据调整负载均衡的策略。 5、反射代理服务器可以让用户在一次会话周期内的所有请求始终转发到一台特定的后端服务器(粘滞会话),这样的好处一是保持session的本地访问,二是防止后端服务器的动态内存缓存的资源浪费。 四、IP负载均衡(LVS-NAT)NAT服务器:它工作在传输层,它可以修改发送来的IP数据包,将数据包的目标地址修改为实际服务器地址。 数据的流向: 客户端 –> Load Balancer –> RS –> Load Balancer –> 客户端 从Linux2.4内核开始,其内置的Neftilter模块在内核中维护着一些数据包过滤表,这些表包含了用于控制数据包过滤的规则。可喜的是,Linux提供了iptables来对过滤表进行插入、修改和删除等操作。更加令人振奋的是,Linux2.6.x内核中内置了IPVS模块,它的工作性质类型于Netfilter模块,不过它更专注于实现IP负载均衡。 IPVS的管理工具是ipvsadm,它为提供了基于命令行的配置界面,可以通过它快速实现负载均衡系统。这就是大名鼎鼎的LVS(Linux Virtual Server,Linux虚拟服务器)。 1、打开调度器的数据包转发选项 echo 1 > /proc/sys/net/ipv4/ip_forward 2、检查实际服务器是否已经将NAT服务器作为自己的默认网关,如果不是,如添加 route add default gw xx.xx.xx.xx 3、使用ipvsadm配置 ipvsadm -A -t 111.11.11.11:80 -s rr 添加一台虚拟服务器,-t 后面是服务器的外网ip和端口,-s rr是指采用简单轮询的RR调度策略(这属于静态调度策略,除此之外,LVS还提供了系列的动态调度策略,比如最小连接(LC)、带权重的最小连接(WLC),最短期望时间延迟(SED)等) ipvsadm -a -t 111.11.11.11:80 -r 10.10.120.210:8000 -m ipvsadm -a -t 111.11.11.11:80 -r 10.10.120.211:8000 -m 添加两台实际服务器(不需要有外网ip),-r后面是实际服务器的内网ip和端口,-m表示采用NAT方式来转发数据包 运行ipvsadm -L -n可以查看实际服务器的状态。这样就大功告成了。 实验证明使用基于NAT的负载均衡系统。作为调度器的NAT服务器可以将吞吐率提升到一个新的高度,几乎是反向代理服务器的两倍以上,这大多归功于在内核中进行请求转发的较低开销。但是一旦请求的内容过大时,不论是基于反向代理还是NAT,负载均衡的整体吞吐量都差距不大,这说明对于一开销较大的内容,使用简单的反向代理来搭建负载均衡系统是值考虑的。 这么强大的系统还是有它的瓶颈,那就是NAT服务器的网络带宽,包括内部网络和外部网络。当然可以配备千兆交换机或万兆交换机,甚至负载均衡硬件设备,除了这另一个简单有效的办法就是将基于NAT的集群和前面的DNS混合使用,比如5个100Mbps出口宽带的集群,然后通过DNS来将用户请求均衡地指向这些集群,同时,你还可以利用DNS智能解析实现地域就近访问。这样的配置对于大多数业务是足够了,但是对于提供下载或视频等服务的大规模站点,NAT服务器还是不够出色。 五、直接路由(LVS-DR)NAT是工作在网络分层模型的传输层(第四层),而直接路由是工作在数据链路层(第二层),貌似更屌些。它通过修改数据包的目标MAC地址(没有修改目标IP),将数据包转发到实际服务器上,不同的是,实际服务器的响应数据包将直接发送给客户羰,而不经过调度器。 数据的流向: 客户端 –> Load Balancer –> RS –> 客户端 1、网络设置 这里假设一台负载均衡调度器,两台实际服务器,购买三个外网ip,一台机一个,三台机的默认网关需要相同,最后再设置同样的ip别名,这里假设别名为10.10.120.193。这样一来,将通过10.10.120.193这个IP别名来访问调度器,你可以将站点的域名指向这个IP别名。 2、将ip别名添加到回环接口lo上 这是为了让实际服务器不要去寻找其他拥有这个IP别名的服务器,在实际服务器中运行: 另外还要防止实际服务器响应来自网络中针对IP别名的ARP广播,为此还要执行: echo “1” > /proc/sys/net/ipv4/conf/lo/arp_ignore echo “2” > /proc/sys/net/ipv4/conf/lo/arp_announce echo “1” > /proc/sys/net/ipv4/conf/all/arp_ignore echo “1” > /proc/sys/net/ipv4/conf/all/arp_announce 配置完了就可以使用ipvsadm配置LVS-DR集群了 ipvsadm -A -t 10.10.120.193:80 -s rr ipvsadm -a -t 10.10.120.193:80 -r 10.10.120.210:8000 -g ipvsadm -a -t 10.10.120.193:80 -r 10.10.120.211:8000 -g -g 就意味着使用直接路由的方式转发数据包 LVS-DR 相较于LVS-NAT的最大优势在于LVS-DR不受调度器宽带的限制,例如假设三台服务器在WAN交换机出口宽带都限制为10Mbps,只要对于连接调度器和两台实际服务器的LAN交换机没有限速,那么,使用LVS-DR理论上可以达到20Mbps的最大出口宽带,因为它的实际服务器的响应数据包可以不经过调度器而直接发往用户端啊,所以它与调度器的出口宽带没有关系,只能自身的有关系。而如果使用LVS-NAT,集群只能最大使用10Mbps的宽带。所以,越是响应数据包远远超过请求数据包的服务,就越应该降低调度器转移请求的开销,也就越能提高整体的扩展能力,最终也就越依赖于WAN出口宽带。 总的来说,LVS-DR适合搭建可扩展的负载均衡系统,不论是Web服务器还是文件服务器,以及视频服务器,它都拥有出色的性能。前提是你必须为实际器购买一系列的合法IP地址。 六、IP隧道(LVS-TUN)基于IP隧道的请求转发机制:将调度器收到的IP数据包封装在一个新的IP数据包中,转交给实际服务器,然后实际服务器的响应数据包可以直接到达用户端。目前Linux大多支持,可以用LVS来实现,称为LVS-TUN,与LVS-DR不同的是,实际服务器可以和调度器不在同一个WANt网段,调度器通过IP隧道技术来转发请求到实际服务器,所以实际服务器也必须拥有合法的IP地址。 总体来说,LVS-DR和LVS-TUN都适合响应和请求不对称的Web服务器,如何从它们中做出选择,取决于你的网络部署需要,因为LVS-TUN可以将实际服务器根据需要部署在不同的地域,并且根据就近访问的原则来转移请求,所以有类似这种需求的,就应该选择LVS-TUN。]]></content>
<categories>
<category>性能优化</category>
</categories>
</entry>
<entry>
<title><![CDATA[git简要教程]]></title>
<url>%2Fblog%2F2018%2F04%2F26%2Fgit%E7%AE%80%E8%A6%81%E6%95%99%E7%A8%8B%2F</url>
<content type="text"><![CDATA[配置 GitGit的设置文件为 .gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。123456789# 显示当前的Git配置$ git config --list# 编辑Git配置文件$ git config -e [--global]# 设置提交代码时的用户信息$ git config [--global] user.name "[name]"$ git config [--global] user.email "[email address]" 一、 创建与合并分支1、 从master分支创建dev分支并切换到dev分支123git checkout mastergit checkout -b dev 其中,git checkout -b dev 等价于:123git branch devgit checkout dev 查看本地当前的分支,分支前面带“*”表示当前分支,剩下的分支表示本地有的分支。1git branch 查看远程全部的分支,白色的表示本地有的,红色的表示本地没有,仅在远程存在。1git branch -a 2、修改代码、提交代码(当前的操作是在dev分支上进行)123git add a.htmlgit commit -m "提交文件a.html" 3、分支合并(将dev合并到master)123git checkout master git merge dev 4、合并完成后,删除dev分支.(删除dev分支时,注意我们当前所在的分支不能是dev分支)1git branch -d dev 5、删除后,查看分支(此时看不到dev分支了)1git branch 6、总结 :工作中经常从master创建新的分支,具体操作如下12345678910111213master创建新分支:git checkout mastergit checkout -b issues1234git push origin issues1234git add .git commit -m "***"git push origin issues1234 注意:将本地分支branch1推到远端的branch2操作步骤:git push origin branch1:branch2 7、删除分支:123git branch -D issues1234 //本地强制删除分支issues1234git push origin :issues1234 //推到远程 二、 解决冲突1、发生冲突的文件12345<<<<<<< HEADCreating a new branch is quick & simple.=======Creating a new branch is quick AND simple.>>>>>>> feature1 其中,git使用<<<<<<<,=======,>>>>>>>标记文件中自己和别人产生冲突的部分。 在<<<<<<<,=======之间为自己的代码;=======,>>>>>>>之间为别人的代码。 如果保留自己的代码,将别人的代码删掉即可。 2、冲突解决后提交1234567git statusgit add ***git commit -m "fix conflict"git push origin 分支名 三、Bug分支1、储藏更改:将当前更改的代码储藏起来,等以后恢复使用1git stash 2、恢复储藏的代码123456789git stash pop //恢复的同时把stash内容删掉或者git stash apply //恢复stash,但是stash内容并不删除git stash drop //在上面操作的基础上,以此来删除stash注: git stash list //查看全部的stash列表 3、将stash空间清空1git stash clear 4、git stash pop 和 git stash apply 区别123原来git stash pop stash@{id}命令会在执行后将对应的stash id 从stash list里删除而 git stash apply stash@{id} 命令则会继续保存stash id。 四、版本回退1、回退至上一个版本1git reset --hard HEAD 2、回退至指定版本1git reset --hard 版本号 3、查看以往版本号(本地的commit)1git reflog 4、查看各版本号及信息(所有的commit:本地commit + 其他同事的commit)1git log 五、撤销修改1、撤销修改1git checkout -- a.html 分两种情况分析:①: 还没有执行 git add 操作,执行上面的操作后,会恢复到和版本库中一模一样的版本状态。 ②: 执行了git add ,还没执行 git commit ,再执行上面的操作后,会恢复到git add 结束后的状态 注:一旦执行了git commit -m “*”,就不能再使用上面的命令回退。 2、撤销新建文件 比如新建一个aa.html页面,并未执行git add ,即没有被git追踪,此时如果你想撤销新建动作,可执行:1git clean -f ../aa.html 3、撤销新建文件夹 比如新建一个文件夹”demo”,并未执行git add ,即没有被git追踪,此时如果你想撤销新建动作,可执行:1git clean -df ./demo 六、对于已经push的版本,进行回退1、第一步:1git reset --hard 版本号 //本地回退到指定的版本 2、第二步:1git push -f origin dev //将远程的也回退到指定版本 七、本地同步远程删除的分支123git fetch origin -p //用来清除已经没有远程信息的分支这样git branch -a 就不会拉取远程已经删除的分支了 八、删除掉没有与远程分支对应的本地分支从gitlab上看不到的分支在本地可以通过git branch -a 查到,删掉没有与远程分支对应的本地分支:1git fetch -p 九、查看远程库的一些信息,及与本地分支的信息1git remote show origin Git命令清单增加删除文件123456789101112131415161718192021# 添加指定文件到暂存区$ git add [file1] [file2] ...# 添加指定目录到暂存区,包括子目录$ git add [dir]# 添加当前目录的所有文件到暂存区$ git add .# 添加每个变化前,都会要求确认# 对于同一个文件的多处变化,可以实现分次提交$ git add -p# 删除工作区文件,并且将这次删除放入暂存区$ git rm [file1] [file2] ...# 停止追踪指定文件,但该文件会保留在工作区$ git rm --cached [file]# 改名文件,并且将这个改名放入暂存区$ git mv [file-original] [file-renamed] 代码提交123456789101112131415161718# 提交暂存区到仓库区$ git commit -m [message]# 提交暂存区的指定文件到仓库区$ git commit [file1] [file2] ... -m [message]# 提交工作区自上次commit之后的变化,直接到仓库区$ git commit -a# 提交时显示所有diff信息$ git commit -v# 使用一次新的commit,替代上一次提交# 如果代码没有任何新变化,则用来改写上一次commit的提交信息$ git commit --amend -m [message]# 重做上一次commit,并包括指定文件的新变化$ git commit --amend [file1] [file2] ... 分支命令123456789101112131415161718192021222324252627282930313233343536373839404142# 列出所有本地分支$ git branch# 列出所有远程分支$ git branch -r# 列出所有本地分支和远程分支$ git branch -a# 新建一个分支,但依然停留在当前分支$ git branch [branch-name]# 新建一个分支,并切换到该分支$ git checkout -b [branch]# 新建一个分支,指向指定commit$ git branch [branch] [commit]# 新建一个分支,与指定的远程分支建立追踪关系$ git branch --track [branch] [remote-branch]# 切换到指定分支,并更新工作区$ git checkout [branch-name]# 切换到上一个分支$ git checkout -# 建立追踪关系,在现有分支与指定的远程分支之间$ git branch --set-upstream [branch] [remote-branch]# 合并指定分支到当前分支$ git merge [branch]# 选择一个commit,合并进当前分支$ git cherry-pick [commit]# 删除分支$ git branch -d [branch-name]# 删除远程分支$ git push origin --delete [branch-name]$ git branch -dr [remote/branch] 标签命令1234567891011121314151617181920212223242526# 列出所有tag$ git tag# 新建一个tag在当前commit$ git tag [tag]# 新建一个tag在指定commit$ git tag [tag] [commit]# 删除本地tag$ git tag -d [tag]# 删除远程tag$ git push origin :refs/tags/[tagName]# 查看tag信息$ git show [tag]# 提交指定tag$ git push [remote] [tag]# 提交所有tag$ git push [remote] --tags# 新建一个分支,指向某个tag$ git checkout -b [branch] [tag] 查看信息123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960# 显示有变更的文件$ git status# 显示当前分支的版本历史$ git log# 显示commit历史,以及每次commit发生变更的文件$ git log --stat# 搜索提交历史,根据关键词$ git log -S [keyword]# 显示某个commit之后的所有变动,每个commit占据一行$ git log [tag] HEAD --pretty=format:%s# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件$ git log [tag] HEAD --grep feature# 显示某个文件的版本历史,包括文件改名$ git log --follow [file]$ git whatchanged [file]# 显示指定文件相关的每一次diff$ git log -p [file]# 显示过去5次提交$ git log -5 --pretty --oneline# 显示所有提交过的用户,按提交次数排序$ git shortlog -sn# 显示指定文件是什么人在什么时间修改过$ git blame [file]# 显示暂存区和工作区的差异$ git diff# 显示暂存区和上一个commit的差异$ git diff --cached [file]# 显示工作区与当前分支最新commit之间的差异$ git diff HEAD# 显示两次提交之间的差异$ git diff [first-branch]...[second-branch]# 显示今天你写了多少行代码$ git diff --shortstat "@{0 day ago}"# 显示某次提交的元数据和内容变化$ git show [commit]# 显示某次提交发生变化的文件$ git show --name-only [commit]# 显示某次提交时,某个文件的内容$ git show [commit]:[filename]# 显示当前分支的最近几次提交$ git reflog 远程同步1234567891011121314151617181920212223# 下载远程仓库的所有变动$ git fetch [remote]# 显示所有远程仓库$ git remote -v# 显示某个远程仓库的信息$ git remote show [remote]# 增加一个新的远程仓库,并命名$ git remote add [shortname] [url]# 取回远程仓库的变化,并与本地分支合并$ git pull [remote] [branch]# 上传本地指定分支到远程仓库$ git push [remote] [branch]# 强行推送当前分支到远程仓库,即使有冲突$ git push [remote] --force# 推送所有分支到远程仓库$ git push [remote] --all 撤销12345678910111213141516171819202122232425262728293031# 恢复暂存区的指定文件到工作区$ git checkout [file]# 恢复某个commit的指定文件到暂存区和工作区$ git checkout [commit] [file]# 恢复暂存区的所有文件到工作区$ git checkout .# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变$ git reset [file]# 重置暂存区与工作区,与上一次commit保持一致$ git reset --hard# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变$ git reset [commit]# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致$ git reset --hard [commit]# 重置当前HEAD为指定commit,但保持暂存区和工作区不变$ git reset --keep [commit]# 新建一个commit,用来撤销指定commit# 后者的所有变化都将被前者抵消,并且应用到当前分支$ git revert [commit]# 暂时将未提交的变化移除,稍后再移入$ git stash$ git stash pop 参考链接:https://segmentfault.com/a/1190000014461898 参考链接:http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html]]></content>
<categories>
<category>实践教程</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[移动端H5缓存]]></title>
<url>%2Fblog%2F2018%2F04%2F20%2F%E7%A7%BB%E5%8A%A8%E7%AB%AFH5%E7%BC%93%E5%AD%98%2F</url>
<content type="text"><![CDATA[HTML5是新一代的HTML标准,加入很多新的特性。离线存储(亦称为缓存机制)是其中一个非常重要的特性。HTML5引入了离线存储,这意味着移动Web应用可进行缓存,并可在没有Internet连接时离线进行访问。HTML5应用程序缓存为应用带来三个优势:123离线浏览:用户可在应用离线时使用它们;速度:已缓存资源加载得更快;减少服务器负载:浏览器将只从服务器下载更新过或更改过的资源。 根据标准,到目前为止,H5共有6种缓存机制,有些是之前已有,有些是H5才新加入的。123456浏览器缓存机制Dom Storgage(Web Storage)存储机制Web SQL Database存储机制(不推荐)Application Cache(AppCache)机制Indexed Database (IndexedDB)File System API 下面分析各种缓存机制的原理、用法及特点;然后针对Android移动端Web性能加载优化的需求,看如何适当利用缓存机制来提高Web的加载性能。 1. 浏览器缓存机制浏览器缓存机制是指通过HTTP协议头里的Cache-Control(或Expires)和Last-Modified(或Etag)等字段来控制文件缓存的机制。这应该是Web中最早的缓存机制了,是在HTTP协议中实现的,有点不同于Dom Storage、AppCache等缓存机制,但本质上是一样的,可以理解为一个是协议层实现的,一个是应用层实现的。 Cache-Control和Last-Modified一般用在静态资源文件上,如JS、CSS和一些图像文件。通过设置资源文件缓存属性,对提高资源文件加载速度,节省流量很有意义,特别是移动网络环境。但问题是:缓存有效时长应该如何设置,如果太短,就起不到缓存的使用,设置的太长,在服务端资源文件有更新时,浏览器有缓存,则不能及时取到最新的文件。 对于移动端的缓存,任何一个网络请求的增加,加载消耗时间都是比较大的(尤其弱网环境下)。对于强缓存只要缓存不到期,是不会向服务器发送请求,但是如果是协商缓存的情况下,304的问题就比较大,它会造成无用的服务器请求,导致网络的延时。Last-Modified需要向服务器发起查询请求,才能知道资源文件有没有更新,虽然服务器可能返回304告诉没有更新,但也还有一个请求的过程。对于移动网络,这个请求可能是比较耗时的,有一种说法叫“消灭304”,指的就是优化掉304的请求。 通过抓包可以发现,带if-Modified-Since字段的请求,如果服务器回包304,回包会带有Cache-Control:max-age或Expires字段,文件的强缓存有效时间会更新,就是文件强缓存会重新有效。304回包后如果再请求,则又可以直接使用本地缓存文件了,不用再向服务器发送请求查询文件是否更新了,除非新的强缓存资源文件时间再次过期。 另外,Cache-Control与Last-Modified是浏览器内核的机制,一般都是标准的实现,不能更改或设置。以QQ浏览器的X5为例,Cache-Control与Last-Modified缓存不能禁用,缓存容量是12MB,不分Host,过期的缓存会最先被清除。如果都没过期,应该优先清最早的缓存或最快到期的或文件大小最大的,过期缓存也有可能还是有效的,清除缓存会导致资源文件的重新拉取。还有,浏览器,如X5,在使用缓存文件时,是没有对缓存文件内容进行校验的,这样缓存文件内容被修改的可能。 分析发现,浏览器的缓存机制还不是非常完美的缓存机制。完美的缓存机制应该是这样的: 缓存文件没更新,尽可能使用缓存,不用和服务器交互;缓存文件有更新时,第一时间能使用到新的文件;缓存的文件要保持完整性,不使用被修改过的缓存文件;缓存的容量大小要能设置或控制,缓存文件不能因为存储空间限制或过期被清除。 以X5为例,第1、2条不能同时满足,第3、4条都不能满足。 在实际应用中,为了解决Cache-Control缓存时长不好设置的问题,以及为了“消灭304”,采用的方式是: 1. 在要缓存的资源文件名中加上版本号或文件MD5值字串,如common.d5d02a02.js、common.v1.js,同时设置Cache-Control:max-age=31536000,也就是一年。在一年时间内,资源文件如果本地有缓存,就会使用缓存,这样就可以避免协商缓存的304的回包现象。2. 如果资源文件有修改,则更新文件内容,同时修改资源文件名,如common.v2.js,html页面也会引用新的资源文件名,实现静态资源非覆盖式更新。通过这种方式,实现了:缓存文件没有更新,则使用缓存;缓存文件有更新,则第一时间使用最新文件的目的。即上面说的第1、2条。第3、4条由于浏览器内部机制,目前还无法满足。 2. Dom Storage(Web Storage)存储机制DOM存储是一套在Web Applications 1.0规范中首次引入的与存储相关的特性的总称,现在已经分离出来,单独发展成为独立的W3C Web存储规范。DOM存储被设计为用来提供一个更大存储量、更安全、更便捷的存储方法,从而可以代替掉将一些不需要让服务器知道的信息存储到Cookies里的这种传统方法。这是对Dom Storage存储机制的官方表述。 Dom Storage是通过存储字符串的Key/Value对来提供的,并提供5MB(不同浏览器可能不同,分Host)的存储空间(Cookies才4KB)。另外Dom Storage存储的数据在本地,不像 Cookies,每次请求一次页面,Cookies 都会发送给服务器。 DOM Storage分为sessionStorage和localStorage。localStorage对象和sessionStorage对象使用方法基本相同,它们的区别在于作用的范围不同。sessionStorage用来存储与页面相关的数据,它在页面关闭后无法使用。而localStorage则持久存在,在页面关闭后也可以使用。 sessionStorage是个全局对象,它维护着在页面会话(page session)期间有效的存储空间。只要浏览器开着,页面会话周期就会一直持续。当页面重新载入(reload)或者被恢复(restores)时,页面会话也是一直存在的。每在新标签或者新窗口中打开一个新页面,都会初始化一个新的会话。 当浏览器被意外刷新的时候,一些临时数据应当被保存和恢复。sessionStorage对象在处理这种情况的时候是最有用的,比如恢复我们在表单中已经填写的数据。1234567891011121314151617181920212223<script type="text/javascript"> // 当页面刷新时,从sessionStorage恢复之前输入的内容 window.onload = function(){ if (window.sessionStorage) { var name = window.sessionStorage.getItem("name"); if (name != "" || name != null){ document.getElementById("name").value = name; } } }; // 将数据保存到sessionStorage对象中 function saveToStorage() { if (window.sessionStorage) { var name = document.getElementById("name").value; window.sessionStorage.setItem("name", name); window.location.href="session_storage.html"; } } </script><form action="./session_storage.html"> <input type="text" name="name" id="name"/> <input type="button" value="Save" onclick="saveToStorage()"/></form><br> 把上面的代码复制到session_storage.html(也可以从附件中直接下载)页面中,用Google Chrome浏览器的不同Page或Window打开,在输入框中分别输入不同的文字,再点击“Save”,然后分别刷新。每个Page或Window显示都是当前Page输入的内容,互不影响。关闭Page,再重新打开,上一次输入保存的内容已经没有了。 Local Storage的接口、用法与Session Storage一样,唯一不同的是:Local Storage保存的数据是持久性的。当前Page关闭(Page Session结束后),保存的数据依然存在。重新打开Page,上次保存的数据可以获取到。另外,Local Storage是全局性的,同时打开两个Page会共享一份存数据,在一个Page中修改数据,另一个Page中是可以感知到的。123456789101112131415<script> // 通过localStorage直接引用key, 另一种写法,等价于: // localStorage.getItem("pageLoadCount"); // localStorage.setItem("pageLoadCount", value); if (!localStorage.pageLoadCount) { localStorage.pageLoadCount = 0; localStorage.pageLoadCount = parseInt(localStorage.pageLoadCount) + 1; document.getElementById('count').textContent = localStorage.pageLoadCount; }</script><p> You have viewed this page <span id="count">an untold number of</span> time(s)</p><br> 将上面代码复制到local_storage.html的页面中,用浏览器打开,pageLoadCount的值是1;关闭Page重新打开,pageLoadCount的值是2。这是因为第一次的值已经保存了。用两个Page同时打开local_storage.html,并分别交替刷新,发现两个Page是共享一个pageLoadCount的。 分析:Dom Storage给Web提供了一种更录活的数据存储方式,存储空间更大(相对Cookies),用法也比较简单,方便存储服务器或本地的一些临时数据。 从Dom Storage提供的接口来看,Dom Storage适合存储比较简单的数据,如果要存储结构化的数据,可能要借助JSON了,将要存储的对象转为JSON字串。不太适合存储比较复杂或存储空间要求比较大的数据,也不适合存储静态的文件等。 在Android内嵌Webview中,需要通过Webview设置接口启用Dom Storage。123WebView myWebView = (WebView) findViewById(R.id.webview);WebSettings webSettings = myWebView.getSettings();webSettings.setDomStorageEnabled(true);<br> 拿Android类比的话,Web的Dom Storage机制类似于Android的SharedPreference机制。 3. Web SQL Database存储机制HTML5也提供基于SQL的数据库存储机制,用于存储适合数据库的结构化数据。但根据官方的标准文档,这种存储机制不再推荐使用,将来也不再维护,而是推荐使用AppCache和IndexedDB,所以这里就不多做阐述了。 4. Application Cache机制Application Cache(简称AppCache)似乎是为支持Web App离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control和Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但AppCache是对浏览器缓存机制的补充,不是替代。先拿W3C官方的一个例子,说下AppCache机制的用法与功能:123456789101112131415<!DOCTYPE html><html manifest="demo_html.appcache"><body><script src="demo_time.js"></script> <p id="timePara"><button onclick="getDateTime()">Get Date and Time</button></p> <p><img src="img_logo.gif" width="336" height="69"></p> <p> Try opening <a href="tryhtml5_html_manifest.htm" target="_blank"> this page</a> , then go offline, and reload the page. The script and the image should still work. </p></body></html> 上面HTML文档,引用外部一个JS文件和一个GIF图片文件,在其HTML头中通过manifest属性引用了一个appcache结尾的文件。 我们在Google Chrome浏览器中打开这个HTML链接,JS功能正常,图片也显示正常。禁用网络,关闭浏览器重新打开这个链接,发现JS工作正常,图片也显示正常。当然也有可能是浏览缓存起的作用,我们可以在文件的浏览器缓存过期后,禁用网络再试,发现HTML页面也是正常的。 通过Google Chrome浏览器自带的工具,我们可以查看已经缓存的AppCache(分Host)。 上面截图中的缓存,就是我们刚才打开HTML的页面AppCache。从截图中看,HTML页面及HTML引用的JS、GIF图像文件都被缓存了;另外HTML头中manifest属性引用的appcache文件也缓存了。 AppCache的原理有两个关键点:manifest属性和manifest文件。 HTML在头中通过manifest属性引用manifest文件。manifest文件,就是上面以appcache结尾的文件,是一个普通文件文件,列出了需要缓存的文件。123CACHE MANIFESTdemo.jsimg.gif 上面截图中的manifest文件,就HTML代码引用的manifest文件。文件比较简单,第一行是关键字,第二、三行就是要缓存的文件路径(相对路径)。这只是最简单的manifest文件,完整的还包括其他关键字与内容。引用manifest文件的HTML和manifest文件中列出的要缓存的文件最终都会被浏览器缓存。 完整的manifest文件,包括三个Section,如下:123456789CACHE MANIFEST# 2012-02-21 v1.0.0/theme.css/logo.gif/main.jsNETWORK:login.aspFALLBACK:/html/ /offline.html 总的来说,浏览器在首次加载HTML文件时,会解析manifest属性,并读取manifest文件,获取CACHE MANIFEST下要缓存的文件列表,再对文件缓存。 AppCache的缓存文件,与浏览器的缓存文件分开存储的,还是一份?应该是分开的。因为AppCache在本地也有5MB(分Host)的空间限制。 AppCache在首次加载生成后,也有更新机制。被缓存的文件如果要更新,需要更新manifest文件。因为浏览器在下次加载时,除了会默认使用缓存外,还会在后台检查manifest文件有没有修改(byte by byte),发现有修改,就会重新获取manifest文件,对CACHE MANIFEST下文件列表检查更新。manifest文件与缓存文件的检查更新也遵守浏览器缓存机制。 如用用户手动清了AppCache缓存,下次加载时,浏览器会重新生成缓存,也可算是一种缓存的更新。另外,Web App也可用代码实现缓存更新。 分析:AppCache看起来是一种比较好的缓存方法,除了缓存静态资源文件外,也适合构建Web离线 App。在实际使用中有些需要注意的地方,有一些可以说是”坑“。 1. 要更新缓存的文件,需要更新包含它的manifest文件,那怕只加一个空格。常用的方法,是修改manifest文件注释中的版本号。如:# 2012-02-21 v1.0.0。 2.被缓存的文件,浏览器是先使用,再通过检查manifest文件是否有更新来更新缓存文件。这样缓存文件可能用的不是最新的版本。 3. 在更新缓存过程中,如果有一个文件更新失败,则整个更新会失败。 4. manifest和引用它的HTML要在相同Host。 5. manifest文件中的文件列表,如果是相对路径,则是相对manifest文件的相对路径。 6. manifest也有可能更新出错,导致缓存文件更新失败。 7. 没有缓存的资源在已经缓存的HTML中不能加载,即使有网络。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/。 8. manifest文件本身不能被缓存,且manifest文件的更新使用的是浏览器缓存机制。所以manifest文件的Cache-Control缓存时间不能设置太长。另外,根据官方文档,AppCache已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持AppCache的,以后就不太确定了。 在Android内嵌Webview中,需要通过Webview设置接口启用AppCache,同时还要设置缓存文件的存储路径,另外还可以设置缓存的空间大小。 5. Indexed DatabaseIndexedDB也是一种数据库的存储机制,但不同于已经不再支持的Web SQL Database。IndexedDB不是传统的关系数据库,可归为NoSQL数据库。IndexedDB又类似于Dom Storage的key-value的存储方式,但功能更强大,且存储空间更大。 IndexedDB存储数据是key-value的形式。Key是必需,且要唯一;Key可以自己定义,也可由系统自动生成。Value也是必需的,但Value非常灵活,可以是任何类型的对象。一般Value都是通过Key来存取的。 IndexedDB提供了一组API,可以进行数据存、取以及遍历。这些API都是异步的,操作的结果都是在回调中返回。 IndexedDB有个非常强大的功能,就是index(索引)。它可对Value对象中任何属性生成索引,然后可以基于索引进行Value对象的快速查询。 要生成索引或支持索引查询数据,需求在首次生成存储对象时,调用接口生成属性的索引。可以同时对对象的多个不同属性创建索引。如下面代码就对name和email两个属性都生成了索引。1234var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true })//first arg is name of index, second is the path (col)objectStore.createIndex("name","name", {unique:false})objectStore.createIndex("email","email", {unique:true}) 生成索引后,就可以基于索引进行数据的查询。Android在4.4开始加入对IndexedDB的支持,只需打开允许JS执行的开关就好了。 分析:IndexedDB是一种灵活且功能强大的数据存储机制,它集合了Dom Storage和Web SQL Database的优点,用于存储大块或复杂结构的数据,提供更大的存储空间,使用起来也比较简单。可以作为Web SQL Database的替代。不太适合静态文件的缓存。以key-value 的方式存取对象,可以是任何类型值或对象,包括二进制。可以对对象任何属性生成索引,方便查询。较大的存储空间,默认推荐250MB(分Host),比Dom Storage的5MB要大得多。通过数据库的事务(tranction)机制进行数据操作,保证数据一致性。异步的 API 调用,避免造成等待而影响体验。 6. File System APIFile System API是HTML5新加入的存储机制。它为Web App提供了一个虚拟的文件系统,就像Native App访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。 File System API也是一种可选的缓存机制,和前面的SQL Database、IndexedDB 和App Cache等一样。File System API有自己的一些特定的优势:123可以满足大块的二进制数据(large binary blobs)存储需求。可以通过预加载资源文件来提高性能。可以直接编辑文件。 浏览器给虚拟文件系统提供了两种类型的存储空间:临时的和持久性的。临时的存储空间是由浏览器自动分配的,但可能被浏览器回收;持久性的存储空间需要显示的申请,申请时浏览器会给用户一提示,需要用户进行确认。持久性的存储空间是Web App自己管理,浏览器不会回收,也不会清除内容。持久性的存储空间大小是通过配额来管理的,首次申请时会一个初始的配额,配额用完需要再次申请。 虚拟的文件系统是运行在沙盒中,不同Web App的虚拟文件系统是互相隔离的,虚拟文件系统与本地文件系统也是隔离的。 移动端Web加载性能(缓存)优化分析完HTML5提供的各种缓存机制,回到移动端(针对Android,可能也适用于iOS)的场景。现在Android App(包括手Q和WX)大多嵌入了Webview的组件(系统Webview或QQ浏览器的X5组件),通过内嵌Webview来加载一些HTML5的运营活动页面或资讯页。这样可充分发挥Web前端的优势:快速开发、发布,灵活上下线。但Webview也有一些不可忽视的问题,比较突出的就是加载相对较慢,会相对消耗较多流量。 通过对一些HTML5页面进行调试及抓包发现,每次加载一个HTML5页面,都会有较多的请求。除了HTML主URL自身的请求外,HTML外部引用的JS、CSS、字体文件、图片都是一个独立的HTTP请求,每一个请求都串行的(可能有连接复用)。这么多请求串起来,再加上浏览器解析、渲染的时间,Web整体的加载时间变得较长;请求文件越多,消耗的流量也会越多。我们可综合使用上面说到几种缓存机制,来帮助我们优化 Web的加载性能。 缓存机制 优势 适用场景 浏览器 HTTP协议层支持 静态文件缓存 Dom Storage 较大存储空间,简单 临时简单数据存储,Cookie扩展 Web SQL Database 存储复杂数据结构 不推荐,IndexDB替代 AppCache 构建离线App 不推荐,离线App,静态文件缓存 IndexDB 存储任何类型数据,索引 结构,关系复杂的数据结构 File System API 支持文件系统操作 数据适合以文件进行管理场景 结论:综合各种缓存机制比较,对于静态文件,如JS、CSS、字体、图片等,适合通过浏览器缓存机制来进行缓存,通过缓存文件可大幅提升Web的加载速度,且节省流量。但也有一些不足:缓存文件需要首次加载后才会产生;浏览器缓存的存储空间有限,缓存有被清除的可能;缓存的文件没有校验。要解决这些不足,可以参考手Q的离线包,它有效的解决了这些不足。 对于Web在本地或服务器获取的数据,可以通过Dom Storage和IndexedDB进行缓存。也在一定程度上减少和Server的交互,提高加载速度,同时节省流量。 @转载请注明出处原文链接:https://www.csdn.net/article/2015-12-16/2826489/1]]></content>
<categories>
<category>性能优化</category>
</categories>
<tags>
<tag>cache</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Web缓存机制]]></title>
<url>%2Fblog%2F2018%2F04%2F03%2FWeb%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[Web缓存是指一个Web资源(如html页面,图片,js,数据等)存在于Web服务器和客户端(浏览器)之间的副本。缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。 web缓存的作用减少网络带宽消耗(当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本)降低服务器压力(给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,减小服务器压力)减少网络延迟,加开页面打开速度 缓存机制是web开发的重要知识点,也是系统优化的重要方向。在前端开发中,缓存有利于加快网页的加载速度,同时缓存能够被反复利用,所以可以减少流量和带宽的开销。这里将系统的介绍在Web开发中的缓存方式,还会涉及到部分操作系统缓存知识。本文会重点介绍浏览器端的缓存机制也就是HTTP缓存,其机制是根据HTTP报文的缓存标识进行的。在分析缓存机制之前,先介绍下浏览器的HTTP报文。HTTP报文分为两种: HTTP请求(Request)报文,报文格式为:请求行 – HTTP头(通用信息头,请求头,实体头) – 请求报文主体(只有POST才有报文主体),如下图 HTTP响应(Response)报文,报文格式为:状态行 – HTTP头(通用信息头,响应头,实体头) – 响应报文主体,如下图 注:通用信息头指的是请求和响应报文都支持的头域,分别为Cache-Control、Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via;实体头则是实体信息的实体头域,分别为Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。这里只是为了方便理解,将通用信息头,响应头/请求头,实体头都归为了HTTP头。 Web开发中的不同缓存1. 数据库缓存 我们可能听说过memcached,它就是一种数据库层面的缓存方案。类似的还有Redis缓存,也是一种基于内存的数据库,它的功能更加强大,并且支持备份和数据持久化。数据库缓存是指,当web应用的关系比较复杂,数据库中的表很多的时候,如果频繁进行 数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。 2. CDN缓存(服务端缓存) CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能,可以归属为全局负载均衡或者说是四层负载均衡。通常情况下,浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,浏览器和服务器之间的缓存机制,在这种架构下同样适用。一般对于网站上的静态资源文件可以采用CDN分发,可以加快网站访问速度。 3. 代理服务器缓存(服务端缓存) 代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大,可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。也可以将其归为七层负载均衡,但需要两次TCP握手连接,然功能更多,如会话保持,图片压缩,防盗链等。 4. 浏览器缓存 每个浏览器都实现了 HTTP 缓存,我们通过浏览器使用HTTP协议与服务器交互的时候,浏览器就会根据一套与服务器约定的规则进行缓存工作。最新的HTML5协议新增了离线缓存属性,对缓存机制进一步的优化,可以达到,实现图片存在客户端,跨域共享客户端缓存,做到真正的离线访问WEB应用,实现客户端的数据库。 5. 应用层缓存 应用层缓存是指我们在代码层面上做的缓存。通过代码逻辑,把曾经请求过的数据或资源等,缓存起来,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率,再次需要数据时通过逻辑上的处理选择可用的缓存的数据。h5新增的storage就可以归属到应用层缓存。 操作系统缓存首先关于存储的概念有以下几种方式:cache:缓存为了让从DB/磁盘拿出来的东西放到缓存(放于内存);磁盘文件:本地存储的视频,图片,计算机里面的文件;数据库:系统项目中的数据存储;内存:计算机中所有的程序运行都是在内存中,所以内存对计算机的性能影响很大。在操作系统缓存机制中,有buffer和cache两种方式,它们都是占用内存(buffer记录元数据,权限属性等,cache缓存文件)。I/O过程本身的延迟,以及高速设备与低速设备交互时的等待延迟,Buffer和Cache就是从这两个方向上产生的优化提高系统性能的方式。 buffer缓存是块设备的读写缓冲区,buffer是I/O缓存,用于内存和硬盘(或其他 I/O设备)之间的数据交换的速度而设计的。通常在写一个非常大的文件,文件会被分成一个个的小block块,往内存上写,然后再写入磁盘,这样的效率会很慢。这种情况下,内存就会攒足一次大的block块再写入磁盘,这样就不会有第一种情况里的延迟,这就是buffer。buffer的主要目的是进行流量整形,把突发的大数量较小规模读写整理成平稳的较大规模的I/O,以减少响应次数(比如从网上下载视频,不能下一点点数据就写入硬盘,而是达到一定量的数据一整块写,不然硬盘负荷太大)。 Cache缓存是高速缓存,用于cpu与内存之间的缓冲,是系统两端处理速度不匹配时的一种折衷策略。主要原因是cpu与memory,由于cpu快,memory跟不上,且有些值使用次数多,所以放入cache中,主要目的是使用内存来缓存可能被再次访问的数据,可以保持冗余的、被重复计算的、计算后的数据(buffer不行)。Cache是经常被使用在I/O请求上,来提高系统性能。如果cache的值很大,说明cache住的文件数很多。如果频繁访问到的文件都能被cache住,那么磁盘的读IO必会非常小。 Web中浏览器缓存浏览器端的缓存分为强缓存(Expires和Cache-Control—优先级高,直接使用本地缓存资源)和协商缓存(Last-Modified / If-Modified-Since和 Etag / If-None-Match—优先级高,先发送请求到服务器端确认资源更改来确认是否使用缓存)。 1. 强缓存在HTTP/1.0使用的是Expires字段,该字段表示缓存到期时间,即有效时间+当时服务器的时间,然后将这个时间设置在header中返回给服务器。如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源。该时间是一个绝对时间,由于服务端和浏览器端时间不一致,或者用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,因此更推荐另一种强缓存方式。Expires字段如下:1Expires: Thu, 15 Mar 2018 09:09:09 GMT 在HTTP/1.1中,增加了一个字段Cache-Control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求,主要是利用该字段的max-age值来进行判断,它是一个相对值,资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则资源失效。1Cache-Control: max-age=9590000 Cache-Control除了max-age字段外,还有几个比较常用的字段设置值: no-cache:不用本地缓存,使用协商缓存,先发送请求到服务器根据响应确认资源是否被更改,如果之前的响应中存在ETag或者Last-Modified,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。 注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires 2. 协商缓存协商缓存(Last-Modified或者Etag)的过程是先从缓存中获取对应的数据标识,然后向服务器发送请求,确认数据是否更新,如果更新,则返回新数据和新缓存。反之,则返回304状态码,告知客户端缓存未更新,可继续使用,这正好弥补了一些强缓存的缺陷,协商缓存主要应用于一些时常需要动态更新的资源文件。协商缓存在协议里的字段是Last-Modified或者Etag,这两个字段都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。 Last-Modified/If-Modified-Since具体过程: 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header浏览器收到304的响应后,就会从缓存中加载资源如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值 Etag/If-None-Match Etag字段值是由服务器生成的每个资源的唯一标识字符串(一般都是hash生成的),只要资源内容有变化这个值就会改变,其判断过程与Last-Modified类似,与它不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,响应头还会把这个ETag返回,即使这个ETag跟之前的没有变化。 Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,之所以在HTTP1.1中新增Etag主要为了解决几个Last-Modified的问题: 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);某些服务器不能精确的得到文件的最后修改时间。这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。 注意:Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。 用户浏览器行为对缓存的影响刷新网页 => 如果缓存没有失效,浏览器会直接使用缓存;反之,则向服务器请求数据手动刷新(F5) => 浏览器会认为缓存失效,在请求服务器时加上Cache-Control: max-age=0字段,然后询问服务器数据是否更新。强制刷新(Ctrl + F5) => 浏览器会直接忽略缓存,在请求服务器时加上Cache-Control: no-cache字段,然后重新向服务器拉取文件。更多用户行为可以用如下表格进行表示: 用户行为 Expires / Cache-Control Last-Modified / Etag 地址栏回车 有效 有效 页面链接跳转 有效 有效 新窗口 有效 有效 前进后退 有效 有效 F5刷新 无效 有效 Ctrl+F5刷新 无效 无效 移动端的缓存处理Cache-Control和Last-Modified一般用在静态资源文件上,如JS、CSS和一些图像文件。通过设置资源文件缓存属性,对提高资源文件加载速度,节省流量很有意义,特别是移动网络环境。但问题是:缓存有效时长应该如何设置,如果太短,就起不到缓存的使用,设置的太长,在服务端资源文件有更新时,浏览器有缓存,则不能及时取到最新的文件。 对于移动端的缓存,任何一个网络请求的增加,加载消耗时间都是比较大的(尤其弱网环境下)。对于强缓存只要缓存不到期,是不会向服务器发送请求,但是如果是协商缓存的情况下,304的问题就比较大,它会造成无用的服务器请求,导致网络的延时。Last-Modified需要向服务器发起查询请求,才能知道资源文件有没有更新,虽然服务器可能返回304告诉没有更新,但也还有一个请求的过程。对于移动网络,这个请求可能是比较耗时的,有一种说法叫“消灭304”,指的就是优化掉304的请求。 通过抓包可以发现,带if-Modified-Since字段的请求,如果服务器回包304,回包会带有Cache-Control:max-age或Expires字段,文件的强缓存有效时间会更新,就是文件强缓存会重新有效。304回包后如果再请求,则又可以直接使用本地缓存文件了,不用再向服务器发送请求查询文件是否更新了,除非新的强缓存资源文件时间再次过期。 另外,Cache-Control与Last-Modified是浏览器内核的机制,一般都是标准的实现,不能更改或设置。以QQ浏览器的X5为例,Cache-Control与Last-Modified缓存不能禁用,缓存容量是12MB,不分Host,过期的缓存会最先被清除。如果都没过期,应该优先清最早的缓存或最快到期的或文件大小最大的,过期缓存也有可能还是有效的,清除缓存会导致资源文件的重新拉取。还有,浏览器,如X5,在使用缓存文件时,是没有对缓存文件内容进行校验的,这样缓存文件内容被修改的可能。 分析发现,浏览器的缓存机制还不是非常完美的缓存机制。完美的缓存机制应该是这样的: 缓存文件没更新,尽可能使用缓存,不用和服务器交互;缓存文件有更新时,第一时间能使用到新的文件;缓存的文件要保持完整性,不使用被修改过的缓存文件;缓存的容量大小要能设置或控制,缓存文件不能因为存储空间限制或过期被清除。 以X5为例,第1、2条不能同时满足,第3、4条都不能满足。 在实际应用中,为了解决Cache-Control缓存时长不好设置的问题,以及为了“消灭304”,采用的方式是: 1. 在要缓存的资源文件名中加上版本号或文件MD5值字串,如common.d5d02a02.js、common.v1.js,同时设置Cache-Control:max-age=31536000,也就是一年。在一年时间内,资源文件如果本地有缓存,就会使用缓存,这样就可以避免协商缓存的304的回包现象。2. 如果资源文件有修改,则更新文件内容,同时修改资源文件名,如common.v2.js,html页面也会引用新的资源文件名,实现静态资源非覆盖式更新。通过这种方式,实现了:缓存文件没有更新,则使用缓存;缓存文件有更新,则第一时间使用最新文件的目的。即上面说的第1、2条。第3、4条由于浏览器内部机制,目前还无法满足。 浏览器缓存回忆浏览器端强缓存优于协商缓存进行,先判断强缓存资源是否过期,若强缓存(Expires和Cache-Control)生效则直接使用本地浏览器缓存,若失效则进行协商缓存(Last-Modified / If-Modified-Since和 Etag / If-None-Match)发送请求到服务端,协商缓存由服务器响应和本地进行对比验证决定资源是否有效,若协商缓存失效,那么代表该请求的缓存失效,择重新发送请求获取响应结果下载资源,再存入浏览器缓存中,生效则返回304状态码,继续使用缓存,整个浏览器缓存主要过程如下图: @转载请注明出处]]></content>
<categories>
<category>性能优化</category>
</categories>
<tags>
<tag>cache</tag>
</tags>
</entry>
<entry>
<title><![CDATA[hexo迁移]]></title>
<url>%2Fblog%2F2018%2F03%2F21%2Fhexo%E8%BF%81%E7%A7%BB%2F</url>
<content type="text"><![CDATA[操作步骤一、安装必要软件12安装 Git 客户端安装 node JS 二、在 github 官网添加新电脑产生的密钥三、源文件拷贝将原来电脑上个人博客目录下必要文件拷贝到新电脑上(比如D:/Blog目录下),注意无需拷贝全部,只拷如下几个文件:12345_config.ymlpackage.jsonscaffolds/source/themes/ 四、安装 hexo1在 cmd 下输入指令安装 hexo:npm install hexo-cli -g 五、进入 D:/Blog 目录(拷贝到新电脑的目录),安装相关模块1234npm installnpm install hexo-deployer-git --save // 文章部署到 git 的模块npm install hexo-generator-feed --save // 建立 RSS 订阅(选择安装)npm install hexo-generator-sitemap --save // 建立站点地图(选择安装) 六、部署发布文章123hexo clean // 清除缓存 网页正常情况下可以忽略此条命令hexo g // 生成静态网页hexo d // 开始部署 第二种方法具体的思路是:在生成的已经推到github上的hexo静态代码出建立一个分支,利用这个分支来管理自己hexo的源文件。如果能在刚刚配置hexo的时候就想好以后的迁移的问题就太好了,可以省掉很多麻烦。 具体的操作:克隆gitHub上面生成的静态文件到本地1git clone https://github.com/yourname/hexo-test.github.io.git 把克隆到本地的文件除了git的文件都删掉,找不到git的文件的话就到删了吧。不要用hexo init初始化。 将之前使用hexo写博客时候的整个目录(所有文件)搬过来。把该忽略的文件忽略了1touch .gitignore 创建一个叫hexo的分支1git checkout -b hexo 提交复制过来的文件到暂存区1git add --all 提交1git commit -m "新建分支源文件" 推送分支到github1git push --set-upstream origin hexo 到这里基本上就搞定了,以后再推就可以直接git push了,hexo的操作跟以前一样。 今后无论什么时候想要在其他电脑上面用hexo写博客,就直接把创建的分支克隆下来,npm install安装依赖之后就可以用了。 克隆分支的操作1git clone -b hexo https://github.com/yourname/hexo-test.github.io.git 因为上面创建的是一个名字叫hexo的分支,所以这里-b后面的是hexo,再把后面的gitHub的地址换成你自己的hexo博客的地址就可以了。 这样完了以后,每次写完博客发布之后不要忘了还要git push把源文件推到分支上。 原文链接:https://blog.csdn.net/lvonve/article/details/79587321原文链接:https://www.jianshu.com/p/beb8d611340a]]></content>
<categories>
<category>实践教程</category>
</categories>
</entry>
<entry>
<title><![CDATA[Hexo]]></title>
<url>%2Fblog%2F2018%2F01%2F13%2Fhexo%2F</url>
<content type="text"><![CDATA[Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new "My New Post" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment]]></content>
</entry>
</search>