Yourz Notes


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

前端渲染引擎doT.js解析

发表于 2015-09-28 | 分类于 架构相关

背景

前端渲染有很多框架,而且形式和内容在不断发生变化。这些演变的背后是设计模式的变化,而归根到底是功能划分逻辑的演变:MVC—>MVP—>MVVM(忽略最早混在一起的写法,那不称为模式)。近几年兴起的React、Vue、Angular等框架都属于MVVM模式,能帮我们实现界面渲染、事件绑定、路由分发等复杂功能。但在一些只需完成数据和模板简单渲染的场合,它们就显得笨重而且学习成本较高了。

例如,在美团外卖的开发实践中,前端经常从后端接口取得长串的数据,这些数据拥有相同的样式模板,前端需要将这些数据在同一个样式模板上做重复渲染操作。

解决这个问题的模板引擎有很多,doT.js(出自女程序员Laura Doktorova之手)是其中非常优秀的一个。下表将doT.js与其他同类引擎做了对比:

框架 大小 压缩版本大小 迭代 条件表达式 自定义语法
doT.js 6KB 4KB √ √ √
mustache 18.9 KB 9.3 KB √ × √
Handlebars 512KB 62.3KB √ √ √
artTemplate(腾讯) - 5.2KB √ √ √
BaiduTemplate(百度) 9.45KB 6KB √ √ √
jQuery-tmpl 18.6KB 5.98KB √ √ √

可以看出,doT.js表现突出。而且,它的性能也很优秀,本人在Mac Pro上的用Chrome浏览器(版本为:56.0.2924.87)上做100条数据10000次渲染性能测试,结果如下:

性能测试

从上可以看出doT.js更值得推荐,它的主要优势在于:

  1. 小巧精简,源代码不超过两百行,6KB的大小,压缩版只有4KB;
  2. 支持表达式丰富,涵盖几乎所有应用场景的表达式语句;
  3. 性能优秀;
  4. 不依赖第三方库。

本文主要对doT.js的源码进行分析,探究一下这类模板引擎的实现原理。

如何使用

如果之前用过doT.js,可以跳过此小节,doT.js使用示例如下:

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
<script type="text/html" id="tpl">
<div>
<a>name:{{= it.name}}</a>
<p>age:{{= it.age}}</p>
<p>hello:{{= it.sayHello() }}</p>
<select>
{{~ it.arr:item}}
<option {{?item.id == it.stringParams2}}selected{{?}} value="{{=item.id}}">
{{=item.text}}
</option>
{{~}}
</select>
</div>
</script>
<script>
$("#app").html(doT.template($("#tpl").html())({
name:'stringParams1',
stringParams1:'stringParams1_value',
stringParams2:1,
arr:[{id:0,text:'val1'},{id:1,text:'val2'}],
sayHello:function () {
return this[this.name]
}
}));
</script>

可以看出doT.js的设计思路:将数据注入到预置的视图模板中渲染,返回HTML代码段,从而得到最终视图。

下面是一些常用语法表达式对照表:

1
2
3
4
5
6
7
项目     | JavaScript语法 | 对应语法             | 案例
------- | ------------ | ---------------------| ------------
输出变量 | = | {{= 变量名}} | {{=it.name }}
条件判断 | if | {{? 条件表达式}} | {{? i > 3}}
条件转折 | else/else if | {{??}}/{{?? 表达式}} | {{?? i ==2}}
循环遍历 | for | {{~ 循环变量}} | {{~ it.arr:item}}...{{~}}
执行方法 | funcName() | {{= funcName() }} | {{= it.sayHello() }}

源码分析及实现原理

和后端渲染不同,doT.js的渲染完全交由前端来进行,这样做主要有以下好处:

  1. 脱离后端渲染语言,不需要依赖后端项目的启动,从而降低了开发耦合度、提升开发效率;
  2. View层渲染逻辑全在JavaScript层实现,容易维护和修改;
  3. 数据通过接口得到,无需考虑后端数据模型变化,只需关心数据格式。

doT.js源码核心:

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
...
// 去掉所有制表符、空格、换行
str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ")
.replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str)
.replace(/'|\\/g, "\\$&")
.replace(c.interpolate || skip, function(m, code) {
return cse.start + unescape(code,c.canReturnNull) + cse.end;
})
.replace(c.encode || skip, function(m, code) {
needhtmlencode = true;
return cse.startencode + unescape(code,c.canReturnNull) + cse.end;
})
// 条件判断正则匹配,包括if和else判断
.replace(c.conditional || skip, function(m, elsecase, code) {
return elsecase ?
(code ? "';}else if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}else{out+='") :
(code ? "';if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}out+='");
})
// 循环遍历正则匹配
.replace(c.iterate || skip, function(m, iterate, vname, iname) {
if (!iterate) return "';} } out+='";
sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
+vname+"=arr"+sid+"["+indv+"+=1];out+='";
})
// 可执行代码匹配
.replace(c.evaluate || skip, function(m, code) {
return "';" + unescape(code,c.canReturnNull) + "out+='";
})
+ "';return out;")
...

try {
return new Function(c.varname, str);//c.varname 定义的是new Function()返回的函数的参数名
} catch (e) {
/* istanbul ignore else */
if (typeof console !== "undefined") console.log("Could not create a template function: " + str);
throw e;
}
...

这段代码总结起来就是一句话:用正则表达式匹配预置模板中的语法规则,将其转换、拼接为可执行HTML代码,作为可执行语句,通过new Function()创建的新方法返回。

代码解析重点1:正则替换

正则替换是doT.js的核心设计思路,本文不对正则表达式做扩充讲解,仅分析doT.js的设计思路。先来看一下doT.js中用到的正则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
templateSettings: {
evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g, //表达式
interpolate: /\{\{=([\s\S]+?)\}\}/g, // 插入的变量
encode: /\{\{!([\s\S]+?)\}\}/g, // 在这里{{!不是用来做判断,而是对里面的代码做编码
use: /\{\{#([\s\S]+?)\}\}/g,
useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,// 自定义模式
defineParams:/^\s*([\w$]+):([\s\S]+)/, // 自定义参数
conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g, // 条件判断
iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g, // 遍历
varname: "it", // 默认变量名
strip: true,
append: true,
selfcontained: false,
doNotSkipEncoded: false // 是否跳过一些特殊字符
}

源码中将正则定义写到一起,这样方便了维护和管理。在早期版本的doT.js中,处理条件表达式的方式和tmpl一样,采用直接替换成可执行语句的形式,在最新版本的doT.js中,修改成仅一条正则就可以实现替换,变得更加简洁。

doT.js源码中对模板中语法正则替换的流程如下:

渲染流程

代码解析重点2:new Function()运用

函数定义时,一般通过Function关键字,并指定一个函数名,用以调用。在JavaScript中,函数也是对象,可以通过函数对象(Function Object)来创建。正如数组对象对应的类型是Array,日期对象对应的类型是Date一样,如下所示:

1
var funcName = new Function(p1,p2,...,pn,body);

参数的数据类型都是字符串,p1到pn表示所创建函数的参数名称列表,body表示所创建函数的函数体语句,funcName就是所创建函数的名称(可以不指定任何参数创建一个匿名函数)。

下面的定义是等价的。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 一般函数定义方式
function func1(a,b){
return a+b;
}
// 参数是一个字符串通过逗号分隔
var func2 = new Function('a,b','return a+b');
// 参数是多个字符串
var func3 = new Function('a','b','return a+b');
// 一样的调用方式
console.log(func1(1,2));
console.log(func2(2,3));
console.log(func3(1,3));
// 输出
3 // func1
5 // func2
4 // func3

从上面的代码中可以看出,Function的最后一个参数,被转换为可执行代码,类似eval的功能。eval执行时存在浏览器性能下降、调试困难以及可能引发XSS(跨站)攻击等问题,因此不推荐使用eval执行字符串代码,new Function()恰好解决了这个问题。回过头来看doT代码中的”new Function(c.varname, str)”,就不难理解varname是传入可执行字符串str的变量。

具体关于new Fcuntion的定义和用法,详细请阅读Function详细介绍。

性能之因

读到这里可能会产生一个疑问:doT.js的性能为什么在众多引擎如此突出?通过阅读其他引擎源代码,发现了它们核心代码段中都存在这样那样的问题。

jQuery-tmpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function buildTmplFn( markup ) {
return new Function("jQuery","$item",
// Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
"var $=jQuery,call,__=[],$data=$item.data;" +

// Introduce the data as local variables using with(){}
"with($data){__.push('" +

// Convert the template into pure JavaScript
jQuery.trim(markup)
.replace( /([\\'])/g, "\\$1" )
.replace( /[\r\t\n]/g, " " )
.replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
.replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
function( all, slash, type, fnargs, target, parens, args ) {
//省略部分模板替换语句,若要阅读全部代码请访问:https://github.com/BorisMoore/jquery-tmpl
}) +
"');}return __;"
);
}

在上面的代码中看到,jQuery-teml同样使用了new Function()的方式编译模板,但是在性能对比中jQuery-teml性能相比doT.js相差甚远,出现性能瓶颈的关键在于with语句的使用。

with语句为什么对性能有这么大的影响?我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
var datas = {persons:['李明','小红','赵四','王五','张三','孙行者','马婆子'],gifts:['平民','巫师','狼','猎人','先知']};
function go(){
with(datas){
var personIndex = 0,giftIndex = 0,i=100000;
while(i){
personIndex = Math.floor(Math.random()*persons.length);
giftIndex = Math.floor(Math.random()*gifts.length)
console.log(persons[personIndex] +'得到了新的身份:'+ gifts[giftIndex]);
i--;
}
}
}

上面代码中使用了一个with表达式,为了避免多次从datas中取变量而使用了with语句。这看起来似乎提升了效率,但却产生了一个性能问题:在JavaScript中执行方法时会产生一个执行上下文,这个执行上下文持有该方法作用域链,主要用于标识符解析。当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了,一个新的可变对象将被创建,它包含指定对象的所有属性。此对象被插入到作用域链的最前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,这样访问datas的属性非常快,但是访问局部变量的速度却变慢了,所以访问代价更高了,如下图所示。

with

这个插件在GitHub上面介绍时,作者Boris Moore着重强调两点设计思路:

  1. 模板缓存,在模板重复使用时,直接使用内存中缓存的模板。在本文作者看来,这是一个鸡肋的功能,在实际使用中,无论是直接写在String中的模板还是从Dom获取的模板都会以变量的形式存放在内存中,变量使用得当,在页面整个生命周期内都能取到这个模板。通过源码分析之后发现jQuery-tmpl的模板缓存并不是对模板编译结果进行缓存,并且会造成多次执行渲染时产生多次编译,再加上代码with性能消耗,严重拖慢整个渲染过程。
  2. 模板标记,可以从缓存模板中取出对应子节点。这是一个不错的设计思路,可以实现数据改变只重新渲染局部界面的功能。但是我觉得:模板将渲染结果交给开发者,并渲染到界面指定位置之后,模板引擎的工作就应该结束了,剩下的对节点操作应该灵活的掌握在开发者手上。

不改变原来设计思路基础之上,尝试对源代码进行性能提升。

先保留提升前性能作为对比:

性能提升

首先来我们做第一次性能提升,移除源码中with语句。

第一次提升后:
性能提升2

接下来第二部提升,落实Boris Moore设计理念中的模板缓存:

性能提升3

优化后的这一部分代码段被我们修改成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function buildTmplFn( markup ) {

if(!compledStr){
// Convert the template into pure JavaScript
compledStr = jQuery.trim(markup)
.replace( /([\\'])/g, "\\$1" )
.replace( /[\r\t\n]/g, " " )
.replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
.replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
//省略部分模板替换语句
}

return new Function("jQuery","$item",
// Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
"var $=jQuery,call,__=[],$data=$item.data;" +

// Introduce the data as local variables using with(){}
"__.push('" + compledStr +
"');return __;"
)
}

在doT.js源码中没有用到with这类消耗性能的语句,与此同时doT.js选择先将模板编译结果返回给开发者,这样如要重复多次使用同一模板进行渲染便不会反复编译。

仅25行的模板:tmpl

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
(function(){
var cache = {};

this.tmpl = function (str, data){
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :

new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
"with(obj){p.push('" +

str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');");

return data ? fn( data ) : fn;
};
})();

阅读这段代码会惊奇的发现,它更像是baiduTemplate精简版。相比baiduTemplate而言,它移除了baiduTemplate的自定义语法标签的功能,使得代码更加精简,也避开了替换用户语法标签而带来的性能消耗。对于doT.js来说,性能问题的关键是with语句。

综合上述我对tmpl的源码进行移除with语句改造:

改造之前性能:

tmpl性能提升

改造之后性能:

tmpl性能提升2

如果读者对性能对比源码比较感兴趣可以访问 https://github.com/chen2009277025/TemplateTest 。

总结

通过对doT.js源码的解读,我们发现:

  1. doT.js的条件判断语法标签不直观。当开发者在使用过程中条件判断嵌套过多时,很难找到对应的结束语法符号,开发者需要自己严格规范代码书写,否则会给开发和维护带来困难。
  2. doT.js限制开发者自定义语法标签,相比较之下baiduTemplate提供可自定义标签的功能,而baiduTemplate的性能瓶颈恰好是提供自定义语法标签的功能。

很多解决我们问题的插件的代码往往简单明了,那些庞大的插件反而存在负面影响或无用功能。技术领域有一个软件设计范式:“约定大于配置”,旨在减少软件开发人员需要做决定的数量,做到简单而又不失灵活。在插件编写过程中开发者应多注意使用场景和性能的有机结合,使用恰当的语法,尽可能减少开发者的配置,不求迎合各个场景。

JS继承浅析

发表于 2015-08-11 | 分类于 基础总结

简介

  • JS作为脚本语言,主要是在浏览器中执行阶段性的特定任务,所以有其本身的特点:
    • 一切都是对象,函数(function)也不例外
    • 每个对象都有内置变量constructor和__proto__,constructor指向构造函数,__proto__指向构造函数的原型
  • 后续的不断发展,在很多方面得到了延伸:
    • 模块化(AMD/CMD/CommonJs)
    • MVVM框架(React/Vue/Angular)
    • 前后端适配(Node/Browser)
  • 在推出新的规范ES6的同时,引入了类及继承。本文简要介绍继承,并讨论其在JS中的实现。
    • 继承是面向对象四大特征(抽象、封装、继承、多态)之一,主要为了复用已有功能。使子类可直接拥有或访问,父类允许的成员变量和方法
    • JS的继承方式比较奇特,不像传统的面向对象语言(C++、Java等),而是通过其原型链
    • 一个对象在访问变量或者函数时,首先在类内部查找,其次是父类内部,最后是其.__proto__

实现继承

创建基类

  • 创建一个基类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var Widget = function(){
    // 内部成员变量
    this.messages = [];
    };

    // 方法
    Widget.prototype.push = function(element) {
    this.messages.push(element);
    }
  • 子类SubWidget继承后,可访问父类的变量和方法:

    1
    2
    3
    4
    5
    6
    7
    8
    var sub1 = new SubWidget( 'foo' );
    var sub2 = new SubWidget( 'bar' );

    sub1.messages.push( 'foo' );
    sub2.messages.push( 'bar' );

    console.log(sub1.messages); // ['foo']
    console.log(sub2.messages); // ['bar']

实例继承

1
var SubWidget = new Widget();
  • 优点:SubWidget是一个实例(非函数),可以访问父类及原型的所有变量和方法
  • 缺点:没有prototype属性,并且共享所有元素

原型继承

继承可以分成两部分:父类成员(变量或方法)和prototype成员

1 继承父类成员

1
2
3
4
5
6
var SubWidget = function( name ){
// 继承父类的成员
Widget.call( this, arguments);
// 初始化自有成员
this.name = name;
};

后初始化自有成员变量或方法,可覆盖父类的同名变量或方法。此时,SubWidget.prototype指向空对象

2 继承父类prototype

只要能显示 访问父类prototype中的变量和方法 即可

2.1 原型为父类的实例

1
SubWidget.prototype = new Widget();

子类prototype中有父类的内部变量,且constructor指向父类(而不是子类本身)。

2.2 原型为父类原型

1
SubWidget.prototype = Widget.prototype;

改变子类prototype的同时,会改变父类prototype

2.3 混合

1
2
3
4
5
6
7
function Super(parent) {
var F = function(){};
F.prototype = parent.prototype;
this.prototype = new F();
this.prototype.constructor = this;
}
super.call(SubWidget, Widget);

需要增加一个内部函数

完整实现

结合1和2.3的完整实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function superProtoType(parent) {
var F = function(){};
F.prototype = parent.prototype;
this.prototype = new F();
this.prototype.constructor = this;
}

var SubWidget = (function(){
var constructFunc = function(name) {
Widget.call(this, this.arguments);
this.name = name;
};
superProtoType.call(constructFunc, Widget);

return constructFunc;
})();

是不是类似于ES6实现的继承方式^_^

图1

图1:数字为优先级

继承与实例

  • 函数.prototype.constructor需要指向自身(Code1:函数默认创建时,即指向自身),否则会改变实例的constructor指向

    1
    2
    3
    4
    5
    6
    7
    var testCls = function() {
    this.array = [];
    }
    var inst = new testCls();
    console.log(inst.constructor); // testCls
    console.log(testCls.prototype.constructor); // testCls
    console.log(testCls.prototype); // Object{constructor: function}

    Code1:log中可以看出,.constructor都指向testCls

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var testCls = function() {
    this.array = [];
    }
    testCls.prototype = {
    push: function() {

    }
    };
    var inst = new testCls();
    console.log(inst.constructor); // Object function
    console.log(testCls.prototype.constructor); // Object function
    console.log(testCls.prototype); // Object {push: function}

    Code2:改变.prototype指向后,.constructor都指向改变后的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var testCls = function() {
    this.array = [];
    }
    testCls.prototype = {
    push: function() {

    },
    constructor: testCls
    };
    var inst = new testCls();
    console.log(inst.constructor); // testCls
    console.log(testCls.prototype.constructor); // testCls
    console.log(testCls.prototype); // Object {push: function, constructor: function}

    Code3:与Code1默认行为一致

  • A instanceof B 的判断逻辑为:A.__proto__…__proto__ === B.prototype。所以在图1中,sub1 instanceof SubWidget和Widget均返回true。

  • Function、Object的构造函数为Function,所以Function(Object).__proto__ === Function.prototype

图2红线所示,Function,Object为Function的实例

  • Function.prototype是一个函数,构造函数是Function。但Function.prototype.__proto__ === Object.prototype,由3可替换为Function(Object).__proto__.__proto__ === Object.prototype

图2红线和绿线所示,Function,Object为Object的实例

图2

图2

HTTP/2

发表于 2015-07-22 | 分类于 基础总结

#前言
在开始长篇大论之前,让我们先看一个网络购物的例子。
有一天,客户端下了一个单子,服务端收到订单之后,给客户端发货,这个交易的过程,就是基于连接发生的请求与响应。

test01

这个客户端,是个剁手党,一天要下好几个单子,可是在HTTP/1.0时代,连接无法复用,每次下完单,都被强制登出/关机,下一次下单,就得重新登录。

剁手党何其能忍?
其中一名剁手党看不下去了,作为一个程序猿,他决定改变这种现状,不然以后还怎么愉快地买键盘买主机买刀砍产品经理呢?
这个伟大的程序猿想到了一种优化方式:设置Connection:Keep-Alive,保持连接在一段时间内不断开。

HTTP/1.1默认开启Keep-Alive,但是,在keep-alive背景下,必须要等待订单1完成后,再继续处理订单2、3……这样的方式显然浪费时间,于是,万能的程序猿又想到了一个方法:HTTP pipelining。不等订单1结束,客户端就连续下了订单2、3、4……
(果然是剁手党之王 ……)

test01

只可惜,仓库是按顺序发货的,若订单1的商品暂时没货,需要调货,订单2、3的商品要等订单1的商品发出之后才能发出。
为了解决这种情况,买家增加了好几个购物渠道(建立多个连接),某东、某会、某品……这样假如其中一个购物渠道阻塞了,其他渠道的订单可以不受影响……

但是,这还是不能完全解决问题:

购物渠道有限,最多只有6个;

  • 每换一个购物渠道都得与客服沟通三次(TCP三次握手),既浪费时间,也会对卖家的服务端造成压力,同时容易受到环境影响而中途断开,需要再次重建;
  • 随着订单的增多,多的订单还是只能按照先进先出(FIFO)的顺序进行排队,阻塞依旧很严重。
  • 然而这一切都被机智的程序猿看在眼里,他们创造了一种SPDY协议,后续在此基础上,又起草了HTTP/2协议。相比于HTTP1.X,HTTP/2解决了许多问题:

多路复用

多路复用,即单个连接上同时进行多个业务单元数据的传输。
有了多路复用之后,在同一个交易渠道上,能够同时完成客户所有订单货物的采购和交付,客户端只要在每个订单上备注好ID,货物拆分发货,乱序到达之后按照ID重新组装即可,不会因为某个包裹的延误导致整体配送进度的推迟。

test01

请求优先级

假如订单2的商品特别重要,就在订单2上留一段备注,服务端收到订单之后,会优先发出订单2的包裹。
同时,服务端评估订单5是短保产品,需要尽快到货,也会将订单5优先发货。

test01

如此,一些比较重要的内容(如网页框架等)即可优先展示。

头部压缩

HTTP1.X的头部越来越膨胀,很多都是重复且多余的,HTTP/2可以压缩头部的大小,并且避免了重复的传输,可以大大降低延迟。
就好比货物越轻,运送速度则越快,HTTP/2协议下,卖家发货时将多余包装扔掉,这样买家就能更快地收到货啦!

test01

服务端推送

服务端推送是HTTP/2的一大亮点。
在客户端下了订单1之后,服务端预先判断客户端可能会需要下订单2、3、4……于是主动发货。这种主动推送的机制,可以节省接下来的几个请求耗时,提升访问速度。

test01

科普完毕的分割线

有了HTTP/2之后,卖家(网站)能够更快地将内容呈现给买家(用户)。

test01

#1 HTTP/2是什么

##1.1 混沌之初
HTTP全称是超文本传输协议(HyperText Transfer Protocol) 。伴随着计算机网络和浏览器的诞生,HTTP/1.0也随之而来,处于计算机网络中的应用层,HTTP是建立在TCP协议之上,所以HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性,例如TCP建立连接的3次握手和每次建立连接带来的RTT延迟时间。

##1.2 矛盾凸显

###1.2.1 互联网的快速发展

  1. 古老的协议
    HTTP建立之初,主要就是为了将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。也是说对于前端来说,我们所写的HTML页面将要放在我们的web服务器上,用户端通过浏览器访问url地址来获取网页的显示内容。目前,互联网广泛使用的超文本传输协议(HTTP)是一个非常成功的协议,然而,HTTP 1.1更新时间是1999年(非常久远了),互联网发展日新月异,它的很多问题也渐渐暴露出来。
  2. WEB2.0的到来
    但随着互联网的发展和web2.0的诞生,更多的内容开始被展示(更多的图片文件),排版变得更精美(更多的css),更复杂的交互也被引入(更多的js)。用户打开一个网站首页所加载的数据总量和请求的个数也在不断增加。今天绝大部分的门户网站首页大小都会超过2M,请求数量可以多达100个。
  3. 移动互联网时代的到来
    当ajax的出现,我们又多了一种向服务器端获取数据的方法,这些其实都是基于HTTP协议的。同样到了移动互联网时代,我们页面可以跑在手机端浏览器里面,但是和PC相比,手机端的网络情况更加复杂,这使得我们开始了不得不对HTTP进行深入理解并不断优化过程中。
    ###1.2.2 矛盾成因
    影响一个HTTP网络请求的因素主要有两个:带宽和延迟。
  • 带宽:如果说我们还停留在拨号上网的阶段,带宽可能会成为一个比较严重影响请求的问题,但是现在网络基础建设已经使得带宽得到极大的提升,我们不再会担心由带宽而影响网速,那么就只剩下延迟了。
  • 延迟成因:

    • 浏览器阻塞(HOL blocking):浏览器会因为一些原因阻塞请求。浏览器对于同一个域名,同时只能有 4~10 个连接(这个根据浏览器内核不同可能会有所差异),超过浏览器最大连接数限制,后续请求就会被阻塞。
    • DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。这个通常可以利用DNS缓存结果来达到减少这个时间的目的。
    • 建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。
      ###1.2.3 矛盾现状
      在HTTP/1.1发布之前,HTTP/1.0被抱怨最多的就是连接无法复用,和head of line blocking这两个问题。理解这两个问题有一个十分重要的前提:客户端是依据域名来向服务器建立连接,一般PC端浏览器会针对单个域名的server同时建立4~10个连接,手机端的连接数则一般控制在4~6个。显然连接数并不是越多越好,资源开销和整体延迟都会随之增大。

    连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。

    head of line blocking会导致带宽无法被充分利用,以及后续健康请求被阻塞。假设有5个请求同时发出,如下图:

    test01

    对于HTTP/1.0的实现,在第一个请求没有收到回复之前,后续从应用层发出的请求只能排队,请求2,3,4,5只能等请求1的response回来之后才能逐个发出。网络通畅的时候性能影响不大,一旦请求1的request因为什么原因没有抵达服务器,或者response因为网络阻塞没有及时返回,影响的就是所有后续请求,问题就变得比较严重了。

    解决连接无法复用

    HTTP/1.0协议头里可以设置Connection:Keep-Alive。在header里设置Keep-Alive可以在一定时间内复用连接,具体复用时间的长短可以由服务器控制,一般在15s左右。到HTTP/1.1之后Connection的默认值就是Keep-Alive,如果要关闭连接复用需要显式的设置Connection:Close。

    解决head of line blocking

    Head of line blocking(以下简称为holb)是HTTP/2之前网络体验的最大祸源。正如上图所示,健康的请求会被不健康的请求影响,而且这种体验的损耗受网络环境影响,出现随机且难以监控。为了解决holb带来的延迟,协议设计者设计了一种新的pipelining机制。

    test01

    即使以上两个问题有了解决方案,HTTP/1.1还是存在诸多问题:

    1. 只有幂等的请求(GET,HEAD)能使用pipelining,非幂等请求比如POST不能使用,因为请求之间可能会存在先后依赖关系。
    2. head of line blocking并没有完全得到解决,server的response还是要求依次返回,遵循FIFO(first in first out)原则。也就是说如果请求1的response没有回来,2,3,4,5的response也不会被送回来。
    3. HTTP/1.1在使用时,header里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求header基本不怎么变化。
    4. 虽然HTTP/1.1支持了keep-alive,来弥补多次创建连接产生的延迟,但是keep-alive使用多了同样会给服务端带来大量的性能压力,并且对于单个文件被不断请求的服务(例如图片存放网站),keep-alive可能会极大的影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。

    ##1.3 应运而生
    ###1.3.1 SPDY 大放异彩
    2012年google如一声惊雷提出了SPDY的方案,大家才开始从正面看待和解决老版本HTTP协议本身的问题。

    SPDY可以说是综合了HTTPS和HTTP两者有点于一体的传输协议,主要解决:

    • 降低延迟,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
    • 请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
    • header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
    • 基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
    • 服务端推送(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。

SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将HTTP1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。

test01

###1.3.2 HTTP/2主角登场
HTTP/1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP/1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP/1.1也是当前使用最为广泛的HTTP协议。HTTP/2即超文本传输协议 2.0,是下一代HTTP协议。是由互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis (HTTPbis)工作小组进行开发。是自1999年HTTP/1.1发布后的首个更新。

test01

HTTP/2可以说是SPDY的升级版(其实原本也是基于SPDY设计的),但是,HTTP/2 跟 SPDY 仍有不同的地方,主要是以下两点:

  • HTTP/2 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS
  • HTTP/2 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE

#2 HTTP/2的新特性

##2.1 二进制分帧层
HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,例如:如空白字符、大小写、行尾、空行等的处理,二进制则不同,只认0和1的组合。HTTP/2将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。

然后,HTTP/2 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。

test01

###2.1.1 帧通用格式
帧头为固定的9个字节((24+8+8+1+31)/8=9)呈现,变化的为帧的负载(payload),负载内容是由帧类型(Type)定义。

  • 帧长度Length:无符号的自然数,24个比特表示,仅表示帧负载所占用字节数,不包括帧头所占用的9个字节。默认大小区间为为0~16,384(2^14),一旦超过默认最大值2^14(16384),发送方将不再允许发送,除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE(一般此值区间为2^14 ~ 2^24)值的通知。
  • 帧类型Type:8个比特表示,定义了帧负载的具体格式和帧的语义,HTTP/2规范定义了10个帧类型,这里不包括实验类型帧和扩展类型帧
  • 帧的标志位Flags:8个比特表示,服务于具体帧类型,默认值为0x0。
  • 帧保留比特位:1个比特表示,在HTTP/2语境下为保留的比特位。
  • 流标识符:无符号的31比特表示无符号自然数。0x0值表示为帧仅作用于连接,不隶属于单独的流。
    ###2.1.2 帧类型
    规范定义了10个正式使用到帧类型,扩展实验类型的ALTSVC、BLOCKED等不在介绍之列。
  • SETTINGS 设置帧,接收者向发送者通告己方设定,服务器端在连接成功后必须第一个发送的帧。
  • HEADER 报头主要载体,请求头或响应头,同时也用于打开一个流,在流处于打开”open”或者远程半关闭”half closed (remote)”状态都可以发送。
  • CONTINUATION 用于协助HEADERS/PUSH_PROMISE等单帧无法包含完整的报头剩余部分数据。
  • DATA 一个或多个DATA帧作为请求、响应内容载体。
  • PUSH_PROMISE 服务器端通知对端初始化一个新的推送流准备稍后推送数据。
  • PING 优先级帧,类型值为0x6,8个字节表示。发送者测量最小往返时间,心跳机制用于检测空闲连接是否有效。
  • PRIORITY 优先级帧,类型值为0x2,5个字节表示。表达了发送方对流优先级权重的建议值,在流的任何状态下都可以发送,包括空闲或关闭的流。
  • WINDOW_UPDATE 流量控制帧,作用于单个流以及整个连接,但只能影响两个端点之间传输的DATA数据帧。但需注意,中介不转发此帧。
  • RST_STREAM 优先级帧,类型值为0x3,4个字节表示。表达了发送方对流优先级权重的建议值,任何时间任何流都可以发送,包括空闲或关闭的流。
  • GOAWAY 一端通知对端较为优雅的方式停止创建流,同时还要完成之前已建立流的任务。
    ##2.2 多路复用
    多路复用,即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。

多路复用带来的好处:

  1. 可以减少服务链接压力,内存占用少了,连接吞吐量大了
  2. 由于 TCP 连接减少而使网络拥塞状况得以改观;
  3. 慢启动时间减少,拥塞和丢包恢复速度更快。

多路复用原理图:

test01

##2.3 首部压缩
首部压缩,HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP/2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。

##2.4 并行双向字节流
并行双向字节流的请求和响应,在HTTP/2上,客户端和服务器可以把HTTP 消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。注意,同一链接上有多个不同方向的数据流在传输。客户端可以一边乱序发送stream,也可以一边接收者服务器的响应,而服务器那端同理。
事实上,这个机制会在整个 Web 技术栈中引发一系列连锁反应, 从而带来巨大的性能提升,因为:

  • 可以并行交错地发送请求,请求之间互不影响;
  • 可以并行交错地发送响应,响应之间互不干扰;
  • 只使用一个连接即可并行发送多个请求和响应;
  • 消除不必要的延迟,从而减少页面加载的时间;
    ##2.5 取消数据流
    数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。
    ##2.6 服务端推送
    服务端推送(server push),就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

当服务端需要主动推送某个资源时,便会发送一个 Frame Type 为 PUSH_PROMISE 的 Frame,里面带了 PUSH 需要新建的 Stream ID。意思是告诉客户端:接下来我要用这个 ID 向你发送东西,客户端准备好接着。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE 类型,便会准备接收服务端要推送的流。

##2.7 请求优先级
请求优先级,HTTP/2允许浏览器指定资源的优先级。每个HTTP/2流里面有个优先值,这个优先值确定着客户端和服务器处理不同的流采取不同的优先级策略,高优先级的流都应该优先发送,但又不会绝对的。绝对地遵守,可能又会引入首队阻塞的问题:高优先级的请求慢导致阻塞其他资源交付。分配处理资源和客户端与服务器间的带宽,不同优先级的混合也是必须的。

#3 注意事项

##3.1 升级
HTTP/2 协议本身并没有要求它必须基于 HTTPS(TLS)部署,但是出于以下三个原因,实际使用中,HTTP/2 和 HTTPS 几乎都是捆绑在一起:

  • HTTP 数据明文传输,数据很容易被中间节点窥视或篡改,HTTPS 可以保证数据传输的保密性、完整性和不被冒充;
  • 正因为 HTTPS 传输的数据对中间节点保密,所以它具有更好的连通性。基于 HTTPS 部署的新协议具有更高的连接成功率;
  • 当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2;

对比HTTPS的升级改造,HTTP/2获取会稍微简单一些,你可能关注以下问题:

  • 前文说了HTTP/2其实可以支持非HTTPS的,但是现在主流的浏览器像chrome,firefox表示还是只支持基于 TLS 部署的HTTP/2协议,所以要想升级成HTTP/2还是需要先升级为HTTPS。
  • 当你的网站已经升级HTTPS之后,那么升级HTTP/2就简单很多,如果你使用NGINX,只要在配置文件中启动相应的协议就可以了,可以参考NGINX白皮书,NGINX配置HTTP/2官方指南。
  • 使用了HTTP/2,原本的HTTP1.x怎么办,这个问题其实不用担心,HTTP/2完全兼容HTTP1.x的语义,对于不支持HTTP/2的浏览器,NGINX等WEB服务器会自动向下兼容的。

##3.2 抛弃针对 HTTP/1.x 的优化

  1. 资源合并和内容内嵌
    资源合并(如:Spriting)和内容内嵌(如:图片Base64)其本身的目的是通过减少请求数量,达到优化的目的。而HTTP/2的多路复用,让通过减少请求数量的优化方式不在需要。
  2. 域名分片
    域名分片本身解决的问题是浏览器对于单个域名的server同时建立6~8个连接,手机端的连接数则一般控制在4~6个。域名分片本身的目的是通过多域名的方式突破这个限制,达到优化的目的。而在HTTP/2上,把 HTTP 消息分解为独立的帧,交错发送。客户端可以一边乱序发送stream,也可以一边接收者服务器的响应,而服务器那端同理。也就是说“域名分区”这种优化手段对于HTTP/2是无用的,因为资源都是并行交错发送,且没有限制,不需要额外的多域名并行下载。
    ##3.3 常见问题
    为什么修订 HTTP ?

HTTP/1.1 已经很好地服务 Web 超过 15 个年头,但它的缺点开始显现出来。
载入一个 Web 页面相比之前会占用更多的资源(详情可见HTTP压缩页大小统计),高效的载入这些资源很难,因为 HTTP 实际上对每个 TCP 连接,只允许一个优先的 HTTP 请求。
在过去,对于并发请求,浏览器使用多个 TCP 连接。然而这也是有局限的;如果使用了过多的连接,则会有相反的效果(TCP 的流控机制导致发送窗口乘性递减,从而产生的拥塞事件影响了性能和网络的表现),同时从根本上来讲这也是不公平的(因为浏览器承载的资源需求大于它们享有的网络资源)。
此外,大量的请求意味着“线上”有很多重复的数据。
HTTP/1.1 在这两个问题上消耗了大量的资源。如果发起过多的请求,则会影响性能。
这些问题导致了像雪碧图、数据内联、域共享和文件合并有了最佳的实践场合。这些技巧正是底层协议问题的表现,并且在使用中也导致了它们自身的一系列问题。

谁创造了 HTTP/2 ?

HTTP/2 由 IETF 的 HTTP 工作组开发,他们也在维护着 HTTP 协议。他们由一群 HTTP 实现者、用户、网络运营商和 HTTP 专家组成。
注意虽然我们的邮件列表托管在 W3C 的站点上,并工作并不是他们所承担。然而,Tim Berners-Lee 和 W3C TAG 与 WG 的进度保持了一致。
很多人对相关工作作出了贡献,不过大部分活跃的参与者都来自于像 Firefox、Chrome、Twitter、Microsoft 的 HTTP stack、Curl 和 Akamai 这样“大”项目的工程师,以及若干 Python、Ruby 和 NodeJS 的 HTTP 实现者。
要了解更多 IETF 的参与者,请看 IETF 之道。你也可以在 Github 的贡献者图表中了解到哪些人正在对规范做着贡献,以及在我们的实现列表中了解哪些人正在参与实现。

与 SPDY 有什么关联?

HTTP/2 第一次出现并被讨论的时候,SPDY 正得到厂商 (像 Mozilla 和 nginx)的青睐,并被看成是基于 HTTP/1.x 的重大改进。
经过提议和投票流程之后,SPDY/2 被选为 HTTP/2 的基础。直从那时起,根据工作组的讨论和厂商的反馈,它已经有了很多变化。
在整个过程中,SPDY 的核心开发成员都参与了 HTTP/2 的发展,其中包括了 Mike Belshe 和 Roberto Peon。
在 2015 年 2 月份,Google 发表了放弃 SPDY 转而支持 HTTP/2 的声明。

是 HTTP/2.0 还是 HTTP/2 ?

工作组决定去掉小版本 (“.0”) ,因为它在 HTTP/1.x 中造成了很多困惑。
也就是说, HTTP 的版本仅仅代表它的线上兼容性,而不表示它的特性集合或者“市场吸引力”。

与 HTTP/1.x 有什么关键区别?

宏观上来讲,HTTP/2:

  • 基于二进制而不是文本的
  • 完全多路复用,代替原来的排序和阻塞机制
  • 在一条连接中并行处理多个请求
  • 压缩头部减少开销
  • 允许服务器主动推送响应到客户端的缓存中
    为什么 HTTP/2 是二进制的?

比起像 HTTP/1.x 这样的文本协议,二进制协议解析起来更高效、“线上”更紧凑,更重要的是错误更少,因为它对如空白字符、大小写、行尾、空行等的处理都更有效。
例如,HTTP/1.1 定义了四个不同的方法来解析一条消息;在HTTP/2中,仅需一个代码路径即可。
HTTP/2 在 telnet 中将不可用,但是我们有一些工具提供支持,比如 Wireshark 插件。

为什么 HTTP/2 是多路复用的?

HTTP/1.x 有个问题叫队头阻塞,即一个连接同时只能有效地承载一个请求。

HTTP/1.1 试过用流水线来解决这个问题,但是效果并不理想(数据量较大或者速度较慢的响应,仍然会阻碍排在后面的响应)。此外,由于网络中介和服务器都不能很好的支持流水线技术,导致部署起来困难重重。
客户端被迫使用一些启发式的算法(基本靠猜)来决定哪些连接来承载哪些请求;由于通常一个页面加载资源的连接需求,往往超过了可用连接资源的 10 倍,这对性能产生极大的负面影响,后果经常是引起了风暴式的阻塞。
而多路复用则能很好的解决这些问题,因为它能同时处理多个消息的请求和响应;甚至可以在传输过程中将一个消息跟另外一个糅合在一起。
所以客户端只需要一个连接就能加载一个完整的页面。

为什么只用一个 TCP 连接?

在 HTTP/1 下,浏览器为每个域分配了 4 到 8 个连接。因为许多站点使用多个域,因此一般一个页面会加载 30 多个连接。
一个应用同时打开这么多连接,已经远远超出了当初设计 TCP 时的预期;由于每一个连接都会在响应中开启一个数据洪流,中介网络的缓存存在溢出的风险,结果导致网络堵塞和数据重传。
此外,使用这么多连接还会强占许多网络资源。这些资源都是从脾气好的其它应用那“偷”来的 ( VoIP 就是个例子)。

服务器推送的收益是什么?

当浏览器请求一个网页时,服务器将会发回 HTML,在服务器开始发送 JavaScript、图片和 CSS 前,服务器需要等待浏览器解析 HTML 并发起所有内嵌资源的请求。
服务器推送服务通过“推送”那些它认为客户端将会需要的内容到客户端的缓存中,以此来避免往返的延迟。

为什么需要压缩头部?

来自 Mozilla 的 Patrick McManus 通过计算头部对页面负载的平均影响度,清晰地说明了这个问题。
假设一个页面中有大约 80 个资源(对于今天的 Web 来说这个数字是比较保守的),并且每个头部有 1400 比特(同样并不罕见,多谢 Cookies、Referer 等),这就需要至少七八个往返传递头部。这还没有计算响应时间——那只是客户端获取它们所花的时间而已。
这是由于 TCP 的慢启动机制造成的,在新的连接上发送数据包的速度取决于有多少个数据包已经被确认——在最初的几轮中这有效限制了可以发送的数据包的数量。
作为对比,即使是轻微的头部压缩也可以是让那些请求只需一个来回就能搞定——有时候甚至一个数据包就可以了。
这种开销是可以被节省下来的,特别是当你考虑移动端应用的时候,即使是条件良好的情况下,一般也会看到几百毫秒的往返延迟。

为什么使用 HPACK ?

SPDY/2 曾提出在每个方向上都使用一个单独的 GZIP 上下文用于消息头的压缩,这实现起来很容易,也很高效。

从那时候开始,一种针对流压缩(如 GZIP)中加密算法的重要攻击方式被发现:CRIME。
通过 CRIME,攻击者有能力在加密流中注入数据以“探测”文本内容并恢复它。由于是在 Web 环境中,JavaScript 使之成为了可能,并且,已经有在受 TLS 保护的 HTTP 资源中利用 CRIME 恢复 cookies 和鉴权令牌的先例。
因此,我们不能使用 GZIP 压缩。也没有找到其它安全和合适的压缩算法,我们就创造了一种新的、针对 HTTP 消息头的粗略压缩方案;由于 HTTP 的消息头在消息之间并不经常变化,因此我们可以得到合理的压缩效率,并且更加地安全。

HTTP/2 可以让 cookies(或其它头部)更好吗?

HTTP/2 是对现有运行的协议的修订,包括如何升级新的 HTTP 头部、方法,而不会影响 HTTP 的语义。

因为 HTTP 被使用得如此广泛,如果我们在这个版本中引入了一个新的状态机制(例如之前讨论过的例子)或者改变了核心方法(幸好没有人提起这件事),那么就意味着新的协议和现在的协议发生了不兼容。
特别要强调,我们需要的是无缝地从 HTTP/1 过渡到 HTTP/2。如果我们现在开始“清算”头部(大多数人会认同,HTTP消息头现在简直是一团糟),那么我们也就不得不面对现代 Web 中的互操作性问题。
那样做只会在采用新协议的过程中制造麻烦。

总而言之,工作组会对所有的 HTTP 负责,而不仅仅只是 HTTP/2。 因此,只要同现有的网络兼容,我们才可以独立于版本地运行新的机制。

非浏览器形式的 HTTP 用户该怎么办?

非浏览器用户如果已经在使用 HTTP,那么也可以使用 HTTP/2。
之前收到过 HTTP/2 针对 HTTP “API” 有更好的性能特点的报告,因为 API 在设计时不需要考虑像请求开销这样的问题。
曾提到过,我们认为 HTTP/2 改进的重点是典型的浏览器环境,因为这是该协议的主要应用场景。
我们的章程里面是这样说的:
最终的标准期望能针对已经部署的普通 HTTP 达到这些目标;
特别地,
包括 Web 浏览器(桌面和移动端)、非浏览器(“HTTP API”),
网络服务(不同尺度的)、中介(代理、企业防火墙,反向代理以及内容分发网络)。

同样,当前和未来针对 HTTP/1.x (头部、方法、状态码、缓存指令)的语义化扩展
也应该在新的协议中被支持。注意这并不涵盖 HTTP 的非标准用法(比如连接状态的
超时、客户端关联以及拦截代理);它们都不会被最终启用。

HTTP/2 需要加密吗?

不需要。经过广泛的讨论,工作组对于新的协议在加密(如 TLS)的使用上没有达成共识。
但是,在实现上厂商都已表明他们只会在加密连接下才会支持 HTTP/2,目前,没有浏览器支持不加密的 HTTP/2。

HTTP/2 该如何提升安全性?

HTTP/2 定义了一个所需 TLS 的基本描述信息;包括版本、密码套件和用到的扩展。
细节参见相关规范。
此外也有额外的相关讨论,比如对 HTTP:// URL(所谓的“机会主义加密”)使用TLS;参见issue #315。

现在可以使用 HTTP/2 了吗?

在浏览器上,Edge、Safari、Firefox 和 Chrome 的近期版本都支持 HTTP/2。其它使用 Blink 内核的浏览器(Opera 和 Yandex)也都支持 HTTP/2。
此外还有几个可用的服务器(包括 Akamai 的 beta 版本,Google 和 Twitter 的主站),以及一些你可以部署和测试的开源实现。
详情请看实现列表。

HTTP/2 会替换 HTTP/1.x 吗?

工作组的目标是 HTTP/1.x 的典型应用能够使用 HTTP/2 并且看到收益。之前提到,我们不能强制大家迁移,因为人们部署代理和服务器的方式让 HTTP/1.x 在未来还会使用一段时间。

未来会有 HTTP/3 吗?

如果 HTTP/2 的协商机制工作良好,未来会更容易升级到新的 HTTP 版本。

##3.4 实现问题
为什么规则会围绕头部帧的数据接续?

数据接续的存在是由于一个值(如 Set-Cookie)可以超过 16kb - 1,这意味着它不可能全部装进一个帧里面。所以就决定以最不容易出错的方式让所有的消息头数据以一个接一个帧的方式传递,这样就使得对消息头的解码和缓冲区的管理更加的容易。

HPACK状态的最小和最大尺寸是多少?

接收一方总是会控制 HPACK 中内存的使用量,并且最小能设置到 0,最大则要看 SETTING 帧中能表示的最大整型数是多少,目前是 2^32 - 1。

我怎样才能避免保持 HPACK 状态?

发送一个 SETTINGS 帧将状态尺寸 (SETTINGS_HEADER_TABLE_SIZE) 设置到 0,然后 RST 所有的流,直到一个带有 ACT 设置位的 SETTINGS 帧发送了过来。

为什么会有一个单独的压缩/流控制上下文?

原来的提案中有流分组的概念,它会共享上下文和流控等。那样有利于代理 (也有利于使用它们的用户的体验),而这样做相应也会增加一点复杂度。所以我们就决定先以一个简单的东西开头,看看它会带来多糟糕的问题,并且在未来的协议版本中解决这些问题(如果有的话)。

在 HPACK 中为什么会有一个 EOS 符号?

考虑到 CPU 的效率和安全,HPACK 的哈夫曼编码填充了哈夫曼编码字符串到下一个字节边界。因此对于任何特定的字符串可能需要 0-7 个比特的填充。

如果单独考虑哈夫曼解码,任何比所需要的填充长的符号都可以正常工作。但是,HPACK 的设计允许按字节对比哈夫曼编码的字符串。通过填充 EOS 符号需要的比特,我们确保用户在做哈夫曼编码字符串字节级比较时是相等的。反过来许多头部可以在不需要哈夫曼解码的情况下被解析。

我可以实现 HTTP/2 而不实现 HTTP/1.1 吗?

基本上是可以的。

对于运行在 TLS (h2) 之上的 HTTP/2 而言,如果你没有实现 http1.1 的 ALPN 标识,那你就不需要支持任何 HTTP/1.1 特性。

对于运行在 TCP (h2c) 之上的 HTTP/2 而言,你需要实现最初的升级(Upgrade)请求。

只支持 h2c 的客户端将需要生成一个请求 “*” 的 OPTIONS 请求或者请求 “/” 的 HEAD 请求,它们绝对安全,并且也很容易构建。要实现 HTTP/2 的客户端将只需要把没有带上 101 状态码的 HTTP/1.1 响应看做是一个错误就行了。

只支持 h2c 的服务器可以使用一个固定的 101 响应来接收一个包含升级(Upgrade)消息头字段的请求。没有 h2c 的 Upgrade 令牌的请求可以使用一个包含了 Upgrade 消息头字段的 505(HTTP版本不支持)状态码来拒绝。那些不希望处理 HTTP/1.1 响应的服务器,应该在发送了带有鼓励用户升级到 HTTP/2 以重试的连接引导之后,立即用带有 REFUSED_STREAM 的错误码拒绝该请求的第一份数据流。

我的 HTTP/2 的连接还需要 TCP_NODELAY 吗?
是的,可能需要。即使对于那种只需要使用单个连接下载大量数据的客户端实现,一些数据包仍然需要反向发回以达到最大的传送速度。没有 TCP_NODELAY(但仍然允许 Nagle 算法),要发出去的数据包可能会被阻塞一会儿,以达到与后续数据包合并的目的。
如果有这样一个数据包,它的目的是告诉对方仍有增加发送窗口的空间,延迟它的发送几百毫秒(或更多)将会对高速连接产生负面的影响。

##3.5 部署问题
我如何调试加密的 HTTP/2 ?

用许多方式可以访问应用的数据,最简单的方法是使用 NSS keylogging 并配合 Wireshark 的插件(最近的基本开发版本)。这对 Firefox 和 Chrome 都有效。

我如何使用 HTTP/2 的服务器推送?

HTTP/2 的服务器推送允许在不等待客户端请求的情况下向客户端提供内容。这可以节省请求的时间开销,特别是对于大型高延迟带宽的产品,它的网络交互时间主要消耗在资源上。
基于请求的内容推送不同的资源可能是不合适的。当前,浏览器只有在发起一个匹配请求(参见 RFC 7234 第四部分)时才会使用推送的请求。
一些缓存并不对所有请求头都响应变化。即使被列在了 Vary 头中。为了最大限度地提高推送资源的利用率,最好避免内容协商。基于 accept-encoding 的内容协商广泛被缓存认可,但是其它头部则不太会被支持。

#4 大势所趋
浏览器的支持情况:目前已经在很多Web浏览器和服务器中得到实现。大约有三分之二的浏览器已经支持HTTP/2,而且这个比例每月都在增加。

test01

截至2016年12月,前1000万个网站中,有10.8%支持HTTP/2,其中自然少不了Google、Twitter等行业先驱。

国内网站中,百度、豆瓣、知乎、QQ邮箱、携程、搜狐、蘑菇街及部分直播平台等已经开始用HTTP/2。

#延伸

参考资料:

https://ye11ow.gitbooks.io/http2-explained/content/part1.html

https://www.kancloud.cn/digest/web-performance-http2/74816

谈谈HTTP/2的协议协商机制

HTTP/2帧类型详解

不基于https实现的http2服务端、客户端(h2c)

HTTP/1.1和HTTP/2性能对比测试

HTTP/2最新支持情况

EventLoop&webWorke

发表于 2015-06-18 | 分类于 基础总结

。。。

dom元素继承

发表于 2015-05-01 | 分类于 基础总结

目录

  • 继承关系
  • 基础类型
    • EventTarget
    • Node
    • Element
    • HTMLElement

继承关系

EventTarget => Node => Element => HTMLElement => HTMLAnchorElement => 实例化HTMLAnchorElement

基础类型

EventTarget

方法 说明 备注 兼容性
EventTarget.addEventListener(type, listener[, useCapture]) 注册事件监听 参数
type
事件类型
listener
监听函数
useCapture
布尔值,指定事件是否在捕获或冒泡阶段执行。
true - 事件句柄在捕获阶段执行
false- 默认。事件句柄在冒泡阶段执行。
ie9-11
EventTarget.attachEvent(eventNameWithOn, callback) 注册事件监听 参数
eventNameWithOn
事件类型名称带on
callback
监听函数
非标准,ie6-8
EventTarget.removeEventListener(type, listener[, useCapture]) 移除事件监听 参数
type
事件类型
listener
监听函数
useCapture
布尔值,指定事件是否在捕获或冒泡阶段执行。
true - 事件句柄在捕获阶段执行
false- 默认。事件句柄在冒泡阶段执行。
ie9-11
EventTarget.detachEvent(eventNameWithOn, callback) 移除事件监听 参数
eventNameWithOn
事件类型名称带on
callback
监听函数
非标准,ie6-8
EventTarget.dispatchEvent(event) 触发事件 参数
event
事件对象
ie9-11
EventTarget.fireEvent(eventNameWithOn[, event]) 触发事件 参数
eventNameWithOn
事件类型名称带on
event
事件对象
非标准,ie6-8

Node

属性 说明 兼容性
Node.childNodes 返回节点的子节点的数组。
Node.firstChild 返回节点的第一个子节点。
Node.lastChild 返回节点的最后一个子节点。
Node.previousSibling 返回位于相同节点树层级的前一个节点。
Node.nextSibling 返回位于相同节点树层级的下一个节点。
Node.parentNode 返回节点的父节点。
Node.parentElement 返回节点的父元素节点,如果该元素没有父节点,或者父节点不是一个元素节点.则 返回null。
Node.nodeName 返回节点的名称。大写
Node.nodeType 返回节点的类型。
Node.nodeValue 设置或返回节点的值。
Node.textContent 设置或返回节点及其后代的文本内容。返回其他文本一样返回行内的样式和脚本代码。 ie9-11
Node.ownerDocument 返回节点的根节点。 节点
Node.baseURI 返回节点的绝对基准 URI。页面的路径 ie无
Node.localName 返回不带命名空间前缀的节点名称。 ie9-11
Node.namespaceURI 返回命名空间的 URI。 ie9-11
Node.prefix 返回选定节点的命名空间前缀。xml有 ie9-11
方法 说明 备注 兼容性
Node.appendChild(node) 向元素添加新的子节点,作为最后一个子节点。 参数
node
你要添加的节点对象。
Node.insertBefore(newnode,existingnode) 在指定的已有的子节点之前插入新节点。 参数
newnode
你想要插入的节点
existingnode
要添加新的节点前的子节点。
Node.removeChild(node) 从元素中移除子节点。 参数
node
想要删除的子节点。
Node.replaceChild(newnode,oldnode) 替换元素中的子节点。 参数
newnode
你要插入的节点对象。
oldnode
你要移除的节点对象。
Node.cloneNode(deep) 克隆某个元素。 参数
deep
如果传递给它的参数是 true,它还将递归复制当前节点的所有子孙节点。否则,它只复制当前节点。
Node.hasChildNodes() 如果元素拥有子节点,则返回 true,否则 false。
Node.normalize() 合并元素中相邻的文本节点,并移除空的文本节点。
Node.contains(node) 返回一个布尔值来表示是否传入的节点是,该节点的子节点。 参数
node
想要查询的子节点。
ie9-11
Node.compareDocumentPosition(node) 比较两个元素的文档位置。 参数
node
你要比较的节点对象。
返回值
1:没有关系,这两个节点不属于同一个文档。
2:element 位于 node 后。
4:element 位于 node 前。
8:element 位于 node 内。
16:node 位于 element 内。
32: 没有关系的,或是两个节点在同一元素的两个属性。
ie9-11
Node.isEqualNode(node) 检查两个元素是否相等。 参数
node
比较的另一个节点。
ie9-11
Node.isDefaultNamespace(namespaceURI) 如果指定的 namespaceURI 是当前节点默认的,则返回 true,否则返回 false。 参数
namespaceURI
你想要检查的命名空间URI。
ie9-11
Node.lookupPrefix(namespaceURI) 返回在节点上匹配指定的命名空间的前缀。 参数
namespaceURI
节点的命名空间 URI。
ie9-11
Node.lookupNamespaceURI(prefix) 返回匹配某个节点上所指定的前缀的命名空间 URI。 参数
prefix
节点的命名空间前缀。
ie9-11

Element

属性 说明 兼容性
Element.id 设置或返回元素的 id。
Element.tagName 返回元素的标签名。大写
Element.className 设置或返回元素的 class 属性。
Element.classList 设置或返回元素的 class 集合。 ie10-11
Element.attributes 返回的属性数组。
Element.innerHTML 设置或返回元素内的标签内容。
Element.outerHTML 设置或返回元素的标签内容。包括元素本身
Element.children 返回子元素集合。
Element.clientHeight 返回元素的可见高度。元素内容及其内边距所占据的空间大小。 body 时,与 scrollHeight 相同
Element.clientWidth 返回元素的可见宽度。元素内容及其内边距所占据的空间大小。 body 时,与 scrollWidth 相同
Element.clientLeft 返回元素的左边框厚度。
Element.clientTop 返回元素的上边框厚度。
Element.scrollHeight 返回内元素的实际高度。元素内容及其内边距所占据的空间大小,包括滚动条隐藏的。
Element.scrollWidth 返回内元素的实际宽度。元素内容及其内边距所占据的空间大小,包括滚动条隐藏的。
Element.scrollLeft 返回内部上部分已隐藏的高度。 documentElement–firefox,ie7-11
body–chrome
ie兼容模式都支持
Element.scrollTop 返回内部左部分已隐藏的宽度。 documentElement–firefox,ie7-11
body–chrome
ie兼容模式都支持
Element.firstElementChild 返回第一个子元素。 ie9-11
Element.lastElementChild 返回最后一个子元素。 ie9-11
Element.nextElementSibling 返回下一个兄弟子元素。 ie9-11
Element.previousElementSibling 返回上一个兄弟子元素。 ie9-11
Element.childElementCount 返回子元素的个数。 ie9-11
方法 说明 备                                  注 兼容性
Element.getBoundingClientRect() 返回元素的大小及其相对于浏览器的位置。包括边框和内边距 返回相对于浏览器的位置
width和height包括padding和border
Element.getClientRects() 返回元素的大小及其相对于浏览器的位置的集合。 返回相对于浏览器的位置
width和height包括padding和border
Element.scrollIntoView(alignToTop) 让当前的元素滚动到浏览器窗口的可视区域内。 参数
alignToTop
如果为true,元素的顶端将和其所在滚动区的可视区域的顶端对齐。
如果为false,元素的底端将和其所在滚动区的可视区域的底端对齐。
Element.insertAdjacentHTML(position, text) 将指定的文本解析为 HTML 或 XML,然后将结果节点插入到 DOM 树中的指定位置处。 参数
position
位置
beforebegin在 element 元素的前面。
afterbegin在 element 元素的第一个子节点前面。
beforeend在 element 元素的最后一个子节点后面。
afterend在 element 元素的后面。
text
字符串,会被解析成 HTML 或 XML。
Element.getElementsByTagName(tagname) 返回拥有指定标签名的所有子元素的集合。 参数
tagname
你想获取自元素的标签名。
Element.getElementsByClassName(classname) 返回文档中所有指定类名的元素集合,作为 NodeList 对象。 参数
classname
你需要获取的元素类名。
多个类名使用空格分隔,如 test demo。
ie9-11
Element.querySelector(selectors) 通过css条件选择第一个符合条件的dom节点 参数
selectors
指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。
对于多个选择器,使用逗号隔开,返回一个匹配的元素。
ie9-11
Element.querySelectorAll(selectors) 通过css条件选择所有符合条件的dom节点 参数
selectors
指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。
对于多个选择器,使用逗号隔开,返回一个匹配的元素。
ie9-11
Element.matches(selectors) 如果当前元素能被指定的css选择器查找到,则返回true,否则返回false。 参数
selectors
指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。
对于多个选择器,使用逗号隔开,返回一个匹配的元素。
ie无
Element.getAttribute(attributename) 返回元素的指定属性值。 参数
attributename
你想获取的属性值的属性名。
Element.setAttribute(attributename,attributevalue) 把指定属性设置或更改为指定值。 参数
attributename
你要添加的属性名称。
attributevalue
你要添加的属性值。
Element.removeAttribute(attributename) 从元素中移除指定属性。 参数
attributename
你想删除的属性值的属性名。
Element.hasAttribute(attributename) 如果元素拥有指定属性,则返回true,否则返回 false。 参数
attributename
你要检查的属性名。
ie8-11
Element.getAttributeNode(attributename) 返回元素指定的属性节点。 参数
attributenode
你想获取的属性节点的属性名。
Element.setAttributeNode(attributenode) 设置或更改指定属性节点。 参数
attributenode
你要添加的属性节点。
Element.removeAttributeNode(attributenode) 移除指定的属性节点,并返回被移除的节点。 参数
attributenode
你想移除的属性节点。
Element.getElementsByTagNameNS(ns,tagname) 返回拥有指定标签名的所有子元素的集合,通过命名空间 URI。 参数
ns
规定从中获取属性值的命名空间 URI。
tagname
你想获取自元素的标签名。
ie9-11
Element.getAttributeNS(ns,name) 返回元素的指定属性值,通过命名空间 URI。 参数
ns
规定从中获取属性值的命名空间 URI。
name
规定从中获取属性值的属性。
ie9-11
Element.setAttributeNS(ns,attributename,attributevalue) 把指定属性设置或更改为指定值,通过命名空间 URI。 参数
ns
规定从中获取属性值的命名空间 URI。
attributename
你要添加的属性名称。
attributevalue
你要添加的属性值。
ie9-11
Element.hasAttributeNS(ns,attributename) 如果元素拥有指定属性,则返回true,否则返回 false,通过命名空间 URI。 参数
ns
规定从中获取属性值的命名空间 URI。
attributename
你要检查的属性名。
ie9-11
Element.getAttributeNodeNS(ns,name) 返回元素指定的属性节点,通过命名空间 URI。 参数
ns
规定从中获取属性值的命名空间 URI。
name
规定从中获取属性值的属性。
ie9-11
Element.setAttributeNodeNS(ns,attributenode) 设置或更改指定属性节点,通过命名空间 URI。 参数
ns
规定从中获取属性值的命名空间 URI。
attributenode
你要添加的属性节点。
ie9-11
Element.removeAttributeNodeNS(ns,attributenode) 移除指定的属性节点,并返回被移除的节点,通过命名空间 URI。 参数
attributenode
你想移除的属性节点。
Element.closest(selectors) 获取匹配特定选择器且离当前元素最近的祖先元素(也可以是当前元素本身)。如果匹配不到,则返回 null。 参数
selectors
选择器
ie无
Element.remove() 将自己从所在的 DOM 树中删除。 ie无
Element.setCapture(retargetToElement) 在处理一个 mousedown 事件过程中调用这个方法来把全部的鼠标事件重新定向到这个元素,直到鼠标按钮被释放或者 document.releaseCapture() 被调用。 参数
retargetToElement
如果被设置为 true, 所有事件被直接定向到这个元素; 如果是 false, 事件也可以在这个元素的子元素上触发。
chrome无
Element.scrollIntoViewIfNeeded(alignCenter) 只在当前元素在视口中不可见的情况下,才滚
动浏览器窗口或容器元素,最终让它可见。
参数
alignCenter
对其位置
true
则表示尽量将元素显示在视口中部(垂直方向)。
false
移动最小的距离对其底部或顶部
只chrome

HTMLElement

属性 说明 兼容性
HTMLElement.accessKey 设置或返回元素的快捷键。
HTMLElement.style 设置或返回元素的 CSSStyleDeclaration 对象。
HTMLElement.title 设置或返回元素的 title 属性。
HTMLElement.tabIndex 设置或返回元素的 tab 键控制次序。
HTMLElement.dir 设置或返回元素的文本方向。
HTMLElement.lang 设置或返回元素的语言代码。
HTMLElement.spellcheck 设置或返回是否对元素内容进行拼写检查。
HTMLElement.contentEditable 设置或返回元素的可编辑状态。
HTMLElement.isContentEditable 返回元素的内容是否可被编辑。
HTMLElement.offsetParent 返回元素的偏移容器。(父集有CSS定位属性的元素或body元素)
HTMLElement.offsetHeight 返回元素的高度。包括border
HTMLElement.offsetWidth 返回元素的宽度。包括border
HTMLElement.offsetLeft 返回元素的水平偏移位置。元素的左外边框至父级元素的左内边框之间的像素距离。
HTMLElement.offsetTop 返回元素的垂直偏移位置。元素的上外边框至父级元素的上内边框之间的像素距离。
HTMLElement.innerText 设置或返回元素的纯文本内容。不包括标签,会忽略行内的样式和脚本
HTMLElement.outerText 设置或返回元素的纯文本内容。包括标签,设置的时候标签会删除,返回时同innerText。
HTMLElement.dataset 返回元素的自定义data特性(data-*)。 ie11
HTMLElement.hidden 设置或返回元素是否隐藏。 ie11
HTMLElement.draggable 设置或返回元素的可拖动状态。 ie10-11
方法 说明 备注 兼容性
HTMLElement.focus() 获取焦点。
HTMLElement.blur() 失去焦点。
HTMLElement.click() 点击。

闭包

发表于 2015-04-10 | 分类于 基础总结

一、执行上下文

先看一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function test(o){
console.log(i); //undefined
var i = 0;
if (typeof o == 'object') {
var j = 0;
for (var k = 0; k < 10; k++){
console.log(k); //0~9
}
console.log(k); //10
}
function log(){ console.log(j); }
console.log(j); //0 | undefined
}

我们知道,JavaScript的作用域只有全局作用域和函数作用域,而且JavaScript的函数作用域是在函数内声明的所有变量在函数体内始终可见。并且其中的变量在函数内var 声明之前已经可用,即存在声明提前的现象。

上述这个函数在具体执行的时候,用到了变量i, j, k, log以及o,并且从函数开始一直到函数执行结束的不同阶段,各个变量的值也不同。我们可以将执行上下文将理解为某段代码执行时可用的各种变量和函数的具体赋值情况。还有一个隐藏的变量this也需要注意。(如果是函数内,还有一个arguments变量也可使用) 结合起来即

1.变量(包括函数内定义的变量,形参变量,函数定义式变量)
2.this值
3.函数声明

这三类变量或函数的具体的值的情况决定了某段代码的执行上下文。比如上述函数放在全局环境下执行,test({name: 'steven'}) 函数初始时

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
<table>
<tr>
<th rowspan="4">变量</th>
<td>o</td>
<td>{name:steven}</td>
</tr>
<tr>
<td>i</td>
<td>undefined</td>
</tr>
<tr>
<td>j</td>
<td>undefined</td>
</tr>
<tr>
<td>k</td>
<td>undefined</td>
</tr>
<tr>
<th>this</th>
<td>window</td>
<td>window</td>
</tr>
<tr>
<th>函数声明</th>
<td>log</td>
<td>function</td>
</tr>
</table>

执行到最后一句console.log(j); 时为

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
<table>
<tr>
<th rowspan="4">变量</th>
<td>o</td>
<td>{name:steven}</td>
</tr>
<tr>
<td>i</td>
<td>0</td>
</tr>
<tr>
<td>j</td>
<td>0</td>
</tr>
<tr>
<td>k</td>
<td>10</td>
</tr>
<tr>
<th>this</th>
<td>window</td>
<td>window</td>
</tr>
<tr>
<th>函数声明</th>
<td>log</td>
<td>function</td>
</tr>
</table>

二、作用域链

JavaScript是基于词法作用域的语言,通过阅读包含变量定义在内的数行源码就能知道变量的作用域。
全局变量在程序中始终都有定义。局部变量在声明它的函数体以及其所嵌套的函数内始终是有定义的。

每一段JavaScript代码都有一个与之关联的作用域链(scope chain),这个作用域链是一个对象列表或者链表,这组对象定义了这段代码”作用域中”的变量。假如有如下函数

1
2
3
4
5
6
7
8
9
10
var t0 = 0;
function outer(t1){
var t2 = 2;
function inner(){
console.log(t1); //1
console.log(t2); //2
}
inner();
}
outer(1);

在函数inner中的代码,其可访问的作用域,一个是自身,再一个即是外层的outer函数的作用域,因此可以访问到t1, t2变量。作用域链可以看做这一个一个作用域从内到外的链表或者列表,内层代码需找某个变量(解析变量)时即从最内层作用域找起,一直找到最底层全局作用域。

执行上下文中的变量或函数可以认为都包含了整个作用域链(一直到全局环境)中的变量和函数,可以简单认为,作用域链是静态的范围,执行上下文是代码具体执行时的带有具体值的变量和具体定义的函数。

三、自由变量

在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。如下代码

1
2
3
4
5
var a = 10;
function fn(){
var b = 20;
console.log(a+b);
}

fn函数中使用的变量a,并不是来≠自于函数fn作用域内部定义,而是来自外部(可能是全局作用域,也可能是另一个函数作用域),则a就是一个自由变量。但是定义是否一定来自于父作用域呢,也未必,其实父作用域这个函数带有一定的迷惑性,不建议使用,看下述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var z = 10;var i = 11;

function foo(i) {
console.log(i);
console.log(z);
}
function test(){
var z = 20;
var i = 10;
(function () {
foo(i);
})();
}
test();

foo函数中的z打印时应该为什么值呢?

##四、闭包
Javascript权威指南中提到一段:

JavaScript采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为闭包。
(这个术语非常古老,是指函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量包裹了起来)

第二条是有关闭包的定义,简单来讲,函数会将其定义处上下的变量都带上。来看两个例子

1
2
3
4
5
6
7
8
// 代码1
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() { return scope; }
return f();
}
checkscope();
1
2
3
4
5
6
7
8
// 代码2
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() { return scope; }
return f;
}
checkscope()();

代码1中,判断return的scope比较容易,是局部变量 scope,代码2中则带有一点迷惑性。回想上述理论中函数执行依赖变量作用域,这个作用域在定义时决定,就明白答案了。(local scope)

结论1:函数执行用到的作用域取决于函数定义的位置。
1
2
3
4
5
6
7
8
// 代码3
function checkNum() {
var num = 1;
function f() { return console.log(num); }
num++;
return f;
}
checkNum()();

代码3 与代码2相比,差别在于 函数f定义的位置 和返回的位置中 插入了一段有关局部变量的操作。执行结果为多少呢?(2)

结论2:(内部)函数的执行上下文取决于函数执行或者return的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码4
function checkNum() {
var num = 1;
function f1() { return console.log(num++); }
function f2() { return console.log(num+=2);}
return {f1: f1, f2: f2}
}
var tmp1 = checkNum();
tmp1.f1();
tmp1.f2();
var tmp2 = checkNum();
tmp2.f1();
tmp2.f2();

代码4中,返回的对象包含了两个函数,均带有同一个内部变量num,在checkNum外调用时。tmp1.f1() 和 tmp2.f1()返回的值相同 均为1,而 tmp1.f2() 和 tmp2.f2() 均为4

结论3:同一个上下文中创建的闭包是共用一个[[Scope]]属性的。 也就是说,某个闭包对其中的变量做修改会影响到其他闭包对其变量的读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 代码5
function log(i){
return function inner() { console.log(i);}
}
var dataList = [];
for(var i = 0;i < 3;i++){
dataList[i] = log(i);
}
dataList[0]();
dataList[1]();
dataList[2]();

//代码6
var dataList = [];
for(var i = 0;i < 3;i++){
dataList[i] = (function(){
return function inner(){
console.log(i);
}
})();
}
dataList[0]();
dataList[1]();
dataList[2]();

代码5和代码6的差别不仅仅在于把log函数由外部提到了内部,而是inner函数的上下文([[scope]])有所变化。代码5中inner函数的i取自外层i,传递参数不同,则i不同,而代码6中的i直接引用了外层的i,当外层的i变化时,势必引起inner中的i变化。

再来看ECMAScript中的定义,ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  • 从实践角度:以下函数才算是闭包:

    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

理论上它将闭包看做是某一类函数,实践中将某类特性看成闭包,虽然有些差别,但是这种现象以及实际使用中产生的效果都是相同的。

##四、特例this

在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。

上述提到函数的变量作用域和定义有关,但是在函数作用域内有一个特殊情况,即this,虽然它可以和变量一样使用,但是this的指向,却是在函数执行时决定的。看下述例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 代码7
var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
   return function(){
      return this.name;
   };
  }
};
alert(object.getNameFunc()());

//代码8
var name = "The Window";
var object = {
 name : "My Object",
 getNameFunc : function(){
   var that = this;
   return function(){
      return that.name;
   };
 }
};
alert(object.getNameFunc()());

代码8中虽然返回了函数 that.name,但是其上下文也一并返回,这时候设定this,可以拿到正确的值。

##延伸

参考资料:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/#introduction
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
http://www.cnblogs.com/skylor/p/4721816.html
http://www.cnblogs.com/wangfupeng1988/p/3977924.html

BFC和IFC整理

发表于 2015-03-28 | 分类于 基础总结

前言

在日常开发过程中,经常遇见margin折叠、文本遮挡等的问题,让人很头疼,今天我们就来尝试分析出问题的产生原因。
通过阅读这篇wiki你将了解到:

BFC是什么(解决什么问题)?

什么样的两个元素才是外边距相邻?

什么是视觉格式化模型?

触发BFC的方法?

一、概念

BFC是block formatting context,块级格式化上下文,用于布局块级盒子的一块渲染区域。Formatting context 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

在IFC中,盒子水平放置,一个接着一个,从包含块的顶部开始。水平margins,borders,和padding在这些盒子中被平分。这些盒子也许通过不同的方式进行对齐:他们的底部和顶部也许被对齐,或者通过文字的基线进行对齐。矩形区域包含着来自一行的盒子叫做line box。line box的宽度由浮动情况和它的包含块决定。line box的高度由line-height的计算结果决定。一个line box总是足够高对于包含在它内的所有盒子

二、深入探讨

为了让大家更好的理解BFC,先看两个例子:

例子1:

test01

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
<style>
.left {
background: red;
width: 200px;
}

.left img {
width: 100%;
}

.right {
background: yellow;
height: 200px;
}
</style>
<div class="left">
<img src="../20150706180346_3miik.jpeg">
</div>
<div class="right">
加载中... 免费发送短信下载移动客户端 发送短信 扫描二维码下载 使用手机上的二维码扫描软件扫描,直接下载百度网盘。©2017 Baidu 移动开放平台 | 服务协议
加载中... 免费发送短信下载移动客户端 发送短信 扫描二维码下载 使用手机上的二维码扫描软件扫描,直接下载百度网盘。©2017 Baidu 移动开放平台 | 服务协议
加载中... 免费发送短信下载移动客户端 发送短信 扫描二维码下载 使用手机上的二维码扫描软件扫描,直接下载百度网盘。©2017 Baidu 移动开放平台 | 服务协议
加载中... 免费发送短信下载移动客户端 发送短信 扫描二维码下载 使用手机上的二维码扫描软件扫描,直接下载百度网盘。©2017 Baidu 移动开放平台 | 服务协议
加载中... 免费发送短信下载移动客户端 发送短信 扫描二维码下载 使用手机上的二维码扫描软件扫描,直接下载百度网盘。©2017 Baidu 移动开放平台 | 服务协议

</div>

上述是两个正常排版的div元素,在浏览器正常的浮动。当左侧div设置了float:left之后界面变成了下图所示。

test01

上图中第二个div浮动到了原来left div所占的位置。但是发现left div占了right div的区域,被迫让right div自动缩进渲染。怎么解决这个问题呢?我们必须让right div的渲染独立出来。这就用到了我们所说的BFC.我们触发了right div的BFC之后(怎么触发我们之后会讲到)如下图:

test01

例子2:

test01

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
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box {
background: yellow;
border:2px solid red;
}

.inner-box {
background: #aaa;
height: 200px;
margin: 10px 0;
}

.inner-box2 {
height: 200px;
width: 100%;
background: #00abe4;
margin: 10px 0;
}

.inner-box3 {
height: 200px;
width: 100%;
background: #4cae4c;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="box">
<div class="inner-box2"></div>
<div class="inner-box"></div>
</div>
</body>

实际上inner-box、inner-box2都存在上下边距10px。在直觉来理解蓝色和灰色div中间应该有20px的间距。但是图中却只看到了10px。这就是我们平时所说的margin折叠问题。
强制让inner-box出于另外一个上下文中:

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
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box {
background: yellow;
border:2px solid red;
}

.inner-box {
background: #aaa;
height: 200px;
margin: 10px 0;
}

.inner-box2 {
height: 200px;
width: 100%;
background: #00abe4;
margin: 10px 0;
}

.inner-box3 {
height: 200px;
width: 100%;
background: #4cae4c;
margin: 10px 0;
}
.bfc-box{
overflow:hidden;
}
</style>
</head>
<body>
<div class="box">
<div class="inner-box2"></div>
<div class="bfc-box">
<div class="inner-box"></div>
</div>
</div>

此时的渲染结果是:

test01

从上图可以看到篮框和灰框之间margin立刻变成了20px。
例子也看完了,应该对BFC是什么有一些大致的了解了。接下来回顾一写相关概念。

2.1 盒子模型(框模型):

盒子模型是DOM元素的一个基本模型,是浏览器排版和渲染的基本块。在这里不对这个概念进行过多的讲解。

test01

2.2 两个元素外边距相邻的定义:

怎么样的两个盒子模型会产生margin折叠呢?这里引入一个概念就是相邻外边距,这个概念是一个进行过抽象的概念,不是实际直观的那个外边距。下面是一些定义:
双方都是同一个块格式化上下文中的浮动块级框。
双方的框边缘垂直相邻,例如下列一种形式:
没有行框、没有间隙、没有内边距且没有边框隔开它们(注意,某些零高度行框会为此被忽略。)
框的上外边距和其属于正常排版(normal flow)的第一个孩子的上外边距。
框的下外边距和其属于正常排版的下一个兄弟的上外边距。
属于正常排版的最后一个孩子的下外边距和其父亲的下外边距,如果其父亲的高度计算值为„auto‟。

2.3 margin折叠

两个以上的框(可能是兄弟,也可能不是)之间的相邻外边距可以被合并成一个单独的外边距。通过此方式合并的外边距被称为折叠,且产生的已合并的外边距被称为折叠外边距。水平外边距不会合并。

###2.4 视觉格式化模型

containing block(包含块):是视觉格式化模型的一个重要概念,它与框模型类似,也可以理解为一个矩形,而这个矩形的作用是为它里面包含的元素提供一个参考,元素的尺寸和位置往往是由该元素所在的包含块决定的。也就是说一个元素盒子的位置和大小有时是通过相对于一个特定的长方形来计算的,这个长方形就被称之为元素的 containing block。“框的包含块”表示“框所处的包含块”,而不是其产生的包含块。每个框会被给予一个相对于其包含块的位置,但它不会被局限在其包含块内;它有可能会溢出。
并不是每个元素都能为其后辈元素生成一个包含块,所以每一个元素都会根据浏览器视口有一个坐标。

二、触发BFC

1.float:数值不为none的时候。float的w3c文档中描述道:

test01

2.overflow 值不为visible的时候

3.position 不为relative和static

4.display的值为inline-block、table-cell、table-caption

三、解决的问题

2.1 margin 折叠

2.2 文字环绕、防止内容被浮动元素覆盖

test01

这个div元素并没有移动,但是它却出现在浮动元素的下方。div元素的line boxes(指的是文本行)进行了移位。此处line boxes的水平收缩为浮动元素提供了空间。
随着文字的增加,因为line boxes不再需要移位,最终将会环绕在浮动元素的下方,因此出现了那样的情况。这就解释了为什么即使在浮动元素存在时,段落也将紧贴在包含块的左边框上,还有为什么line boxes会缩小以容纳浮动元素。

2.3 包含浮动块

参考文档:

http://www.yangyong.me/css2-bfc%E6%A8%A1%E5%9E%8B%E5%92%8Cifc%E6%A8%A1%E5%9E%8B/

https://segmentfault.com/a/1190000004466536

http://www.w3cplus.com/css/understanding-block-formatting-contexts-in-css.html

http://www.zhangxinxu.com/wordpress/2015/02/css-deep-understand-flow-bfc-column-two-auto-layout/

http://www.cnblogs.com/xiaohuochai/p/5248536.html

12
Yourz

Yourz

17 日志
2 分类
25 标签
GitHub 微博 Instagram
© 2015 - 2018 Yourz
由 Hexo 强力驱动
主题 - NexT.Mist