Little H title

this is subtitle


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 公益404

静态作用域与动态作用域

发表于 2019-01-13 | 分类于 javascript教程

静态作用域与动态作用域

作用域有两种常见的模型:词法作用域(Lexical Scope,通常也叫做 静态作用域) 和 动态作用域(Dynamic Scope)。其中词法作用域更常见,被 JavaScript 等大多数语言采用。(愚人码头注:这里避开了with和eval特殊语句,不再做介绍)。

静态作用域与动态作用域:

  • 词法作用域:词法作用域是指在词法分析阶段就确定了,不会改变。变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。

  • 动态作用域:动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们在何处调用。

JavaScript的词法作用域:

如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:

  1. 读入第一个代码段(js执行引擎并非一行一行地分析程序,而是一段一段地分析执行的)
  2. 做词法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
  3. 对var变量和function定义做“预解析“(永远不会报错的,因为只解析正确的声明)
  4. 执行代码段,有错则报错(比如变量未定义)
  5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
  6. 完成
1
2
3
4
5
6
7
8
9
10
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???

假设JavaScript采用静态作用域,让我们分析下执行过程:(这部分要结合作用域链看)

执行 foo 函数,先从 foo 函数局部作用域中查找是否有变量 value,如果没有,就从全局作用域中查找变量value的值,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

参考

实例分析 JavaScript 作用域, 同时讲了形参, 实参, 同名局部变量

js作用域链和闭包

发表于 2019-01-10 | 分类于 javascript教程

js 作用域链和闭包

1.执行环境(execution context)

执行环境execution context和环境context不一样, 后面有介绍

执行环境定义了变量和函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有与之对应的变量对象(variable object)

变量对象就是执行环境中定义的变量和函数,活动对象是函数执行的时候被创建的,是属于某个函数的
保存着该环境中定义的所有变量和函数。我们无法通过代码来访问变量对象,但是解析器在处理数据时会在后台使用到它。
执行环境有全局执行环境(也称全局环境)和函数执行环境之分。执行环境如其名是在运行和执行代码的时候才存在的,所以我们运行浏览器的时候会创建全局的执行环境,在调用函数时,会创建函数执行环境。

1.1 全局执行环境

全局执行环境是最外围的一个执行环境,在 web 浏览器中,我们可以认为他是window 对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。代码载入浏览器时,全局环境被创建,关闭网页或者关闭浏览时全局环境被销毁。

但看 JavaScript 核心概念之作用域和闭包 666 这个链接的图, global 的变量和 window 是单独的, 这里谁对呢>

1.2 函数执行环境

每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就被推入一个环境栈中,当函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境。

2 作用域、作用域链

2.1 作用域(Scope)

作用域概念是理解JavaScript的关键所在,不仅仅从性能角度,还包括从功能角度。作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,换句话说,作用域决定了代码区块中变量和其他资源的可见性。在JavaScript中变量的作用域有全局作用域和局部作用域。JavaScript采用词法作用域(lexical scoping),也就是静态作用域。

静态作用域与动态作用域

在下面的图中, AO就是一个作用域, Global object也是一个作用域scope, 他们串一起就是scope chain咯

2.1 全局作用域(globe scope)和局部作用域(local scope)和块级作用域

在ECMAScript 5(包括 ECMAScript 5)之前的版本中,作用域只有全局作用域和局部作用域,不存在块级作用域;ECMAScript 6引入了let和const关键字,利用let和const可以形成块级作用域。(和 c go 那样的在{}里面表示块,不需要结合if for一起用才能形成块)

1、全局作用域:

在代码中任何地方都能访问到的对象拥有全局作用域。全局作用域的变量是全局对象的属性,不论在什么函数中都可以直接访问,而不需要通过全局对象,但加上全局对象,可以提供搜索效率。

a、没有用 var 声明的变量(除去函数的参数)都具有全局作用域,成为全局变量,所以声明局部变量必须要用 var。
b、window的所有属性都具有全局作用域
c、最外层函数体外声明的变量也具有全局作用域

2、局部作用域

局部变量的优先级高于全局变量。

a、函数体内用 var 声明的变量具有局部作用域,成为局部变量
b、函数的参数也具有局部作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
var a=3; // a全局变量
function fn(b){ // fn全局变量 b局部变量
c=2; // c全局变量
var d=5; // d局部变量
function subFn(){ // subFn局部变量
var e=d; // 父函数的局部变量对子函数可见
for(var i=0;i<3;i++){
console.write(i);
}
alert(i);// 3, 在for循环内声明,循环外function内仍然可见,没有块作用域
}
}
alert(c); // 在function内声明但不带var修饰,仍然是全局变量

3、块级作用域:

使用 let 和 const 关键字声明的变量,会在形成块级作用域。常见的是在if和for的{}语句块里面用, 可以单独使用{}作为块作用域哦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
// name 在全局作用域中,因为通过 'var' 关键字定义
var name = 'Hammad';
// likes 在局部(本地)作用域中,因为通过 'let' 关键字定义
let likes = 'Coding';
// skills 在局部(本地)作用域中,因为通过 'const' 关键字定义
const skills = 'JavaScript and PHP';
}

{
const a = 1
}
console.log(name); // logs 'Hammad'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defined
console.log(a); // Uncaught ReferenceError: a is not defined

上下文(context)不是执行上下文(execution scope)

许多开发人员经常混淆作用域(scope)和上下文(context),很多误解为它们是相同的概念。但事实并非如此。作用域(scope)我们上面已经讨论过了,而上下文(context)是用来指定代码某些特定部分中this的值。
作用域(scope) 是指变量的可访问性,上下文(context)是指this在同一作用域内的值。
我们也可以使用call()、apply()、bind()、箭头函数等改变上下文。
在浏览器中在全局作用域(scope)中上下文中始终是Window对象。在 Node.js 中在全局作用域(scope)中上下文中始终是Global 对象。

1
2
3
4
5
6
7
8
var name = "windowsName";
function a() {
var name = "Cherry";
console.log(this.name); // windowsName
console.log("inner:" + this);// inner: Window
}
a();
console.log("outer:" + this) // outer: Window

上下文始终坚持一个原理:this 永远指向最后调用它的那个对象参考javascript中this指向由函数调用方式决定,上例中调用a函数的是window,所以 a 函数中的this指向window对象。关于this以及改变this的指向,可以参考this、apply、call、bind

2.2 作用域链(scope chain)

JavaScript 中每个函数都都表示为一个函数对象(函数实例),函数对象有一个仅供 JavaScript 引擎使用的[[scope]] 属性。通过语法分析和预解析,将[[scope]] 属性指向函数定义时作用域中的所有对象集合。这个集合被称为函数的作用域链(scope chain),包含函数定义时作用域中所有可访问的数据。

1
2
3
4
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}

当定义 add 函数后,其作用域链就创建了。函数所在的全局作用域的全局对象被放置到 add 函数作用域链([[scope]] 属性)中。我们可以从下图中看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如 this , window , document 以及全局对象中的 add 函数,也就是他自己。这也就是我们可以在全局作用域下的函数中访问 window(this),访问全局变量,访问函数自身的原因。全局上下文中的变量对象(Variable object,VO)就是全局对象。

scopechain1.png

全局作用域和局部作用域中变量的访问权限,其实是由作用域链决定的。

每次进入一个新的执行环境(这里就表示程序执行起来了),都会创建一个用于搜索变量和函数的作用域链。作用域链是函数被创建的作用域中对象的集合。作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的最前端始终是当前执行的代码所在环境的变量对象(如果该环境是函数,则将其活动对象作为变量对象),下一个变量对象来自包含环境(包含当前还行环境的环境),下一个变量对象来自包含环境的包含环境,依次往上,直到全局执行环境的变量对象。全局执行环境的变量对象始终是作用域链中的最后一个对象。

标识符解析是沿着作用域一级一级的向上搜索标识符的过程。搜索过程始终是从作用域的前端逐地向后回溯,直到找到标识符。

1
2
3
4
5
6
7
8
9
10
11
12
var foo = 'foo';
function fName() {
var bar = 'bar';
function sName() {
console.log(foo); //foo
console.log(bar); //bar
var tName = 'tName';
console.log(tName); //tName
}
bName();
}
fName();

上述代码中,一共有三个执行环境:全局环境、fName()的局部环境和 sName() 的局部环境。所以,

  1. 函数 sName()的作用域链包含三个对象:自己的变量对象——->fName()局部环境的变量对象 ——->全局环境的变量对象。
  2. 函数 fName()的作用域链包含两个对象:自己的变量对象——->全局环境的变量对象。

就上述程序中出现的变量和函数来讲(不考虑隐形变量):

  1. sName() 局部环境的变量对象中存放变量 tName;
  2. fName() 局部环境的变量对象中存放变量 bar 和 函数sName();
  3. 全局环境的变量对象中存放变量 foo 、函数fName();

scope1.gif

作用域链相关知识的总结:

  1. 执行环境决定了变量的生命周期,以及哪部分代码可以访问其中变量和函数
  2. 执行环境有全局执行环境(全局环境)和局部执行环境之分。
  3. 每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链
  4. 函数的局部环境可以访问函数作用域中的变量和函数,也可以访问其父环境,乃至全局环境中的变量和环境。
  5. 全局环境只能访问全局环境中定义的变量和函数,不能直接访问局部环境中的任何数据。
  6. 变量的执行环境有助于确定应该合适释放内存。

execution context, scope chain, scope 三者关系

看闭包那个图图可以知道, 最左边的是execution context, 中间的是scope chain, 最右边的是scope

再说下执行器上下文(execution context)

执行具体的某个函数时,JS 引擎在执行每个函数实例时,都会创建一个执行期上下文(Execution Context)和激活对象(active Object)(它们属于宿主对象,与函数实例执行的生命周期保持一致,也就是函数执行完成,这些对象也就被销毁了,闭包例外。)

假设我们运行以下代码:

1
var total = add(5, 10);

执行该函数创建一个内部对象,称为 Execution Context(执行期上下文)。执行期上下文定义了一个函数正在执行时的作用域环境。

特别注意,执行期上下文execution context和我们平常说的上下文context不同,执行期上下文指的是作用域[[scope]]??。平常说的上下文是this的取值指向。

执行期上下文和函数创建时的作用域链对象 [[scope]] 区分,这是两个不同的作用域链对象。分开的原因很简单,函数定义时的作用域链对象 [[scope]] 是固定的,而 执行期上下文 会根据不同的运行时环境变化。而且该函数每执行一次,都会创建单独的 执行期上下文,因此对同一函数调用多次,会导致创建多个执行期上下文。一旦函数执行完成,执行期上下文将被销毁。

执行期上下文对象有自己的作用域链,当创建执期行上下文时,其作用域链将使用执行函数[[scope]]属性所包含的对象(即,函数定义时的作用域链对象)进行初始化。这些值按照它们在函数中出现的顺序复制到执行期上下文作用域链中。(所以要注意闭包的产生)

无论有多少个函数上下文,但是全局上下文只有一个。执行期上下文有创建和代码执行的两个阶段。

下面链接讲了函数定义时的作用域链,以及函数运行时的执行上下文的区别.
JavaScript 核心概念之作用域和闭包
前端基础进阶(四):详细图解作用域链与闭包

JavaScript 代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

1
2
3
4
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}

第一阶段:创建阶段

当一个函数被调用但是其代码还没有被执行的时候。在创建阶段主要做的三件事情是:

  1. 创建变量(激活)对象(VO == AO) 详看变量对象干了啥: 前端基础进阶(三):变量对象详解
  2. 创建作用域链
  3. 设置上下文(context)的值( this )

激活对象(Activation Object,AO)

当一个函数被调用但是其代码还没有被执行的时,在执行其上下文中创建一个名为 Activation Object(激活对象)的新对象。这个激活对象保存了函数中的所有形参,实参,局部变量,this 指针等函数执行时函数内部的数据情况。然后将这个激活对象推送到执行其上下文作用域链的顶部。

  1. 函数参数(若未传入,初始化该参数值为undefined)
  2. 函数声明(若发生命名冲突,会覆盖)
  3. 变量声明(初始化变量值为undefined,若发生命名冲突,会忽略。)

例如: add函数被调用,但是还未执行时的 VO(变量对象)==AO(激活对象)是:

1
2
3
4
5
6
7
8
9
10
AO(add) = {
arguments: {
0: 5,
1: 10
length: 2
},
num1: 5,
num2: 10,
sum: undefined
};

上面代码是不是少了this的值, 调用的时候this就可以确定了的啊, 图中就有this

激活对象 AO 是一个可变对象,里面的数据随着函数执行时的数据的变化而变化(比如进行赋值),当函数执行结束之后,执行期上下文将被销毁。也就会销毁Execution Context的作用域链,激活对象也同样被销毁。但如果存在闭包,激活对象就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。下图显示了执行上下文及其作用域链:

execution_scope1.png

从左往右看,第一部分是函数执行时创建的执行期上下文,它有自己的作用域链,第二部分是作用域链中的对象,索引为 1的对象是从[[scope]]作用域链中复制过来的,索引为 0的对象是在函数执行时创建的激活对象,第三部分是作用域链中的对象的内容Activation Object(激活对象)和Global Object(全局对象)。

函数在执行时,每遇到一个变量,都会去执行期上下文的作用域链的顶部,执行函数的激活对象开始向下搜索,如果在第一个作用域链(即,Activation Object 激活对象)中找到了,那么就返回这个变量。如果没有找到,那么继续向下查找,直到找到为止。如果在整个执行期上下文中都没有找到这个变量,在这种情况下,该变量被认为是未定义的。这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量,以及 JavaScript 中各种看似怪异的、有趣的作用域问题的答案。

第二阶段:代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值,并最终执行代码(这里只是变化变量的值, 但this是一直在的)。当代码执行完后,这时候的 AO 是:

1
2
3
4
5
6
7
8
9
10
AO(add) = {
arguments: {
0: 5,
1: 10
length: 2
},
num1: 5,
num2: 10,
sum: 15 // sum有了
};

闭包 Closure 重点看这里, 前面讲的不怎么细

前面讲的不怎么细, 这里重新开始把在函数定义时产生的scope chain和函数调用但未执行时和函数执行时的各个情况画图

闭包(Closure)是 JavaScript 最强大的特性之一,它允许函数访问局部作用域之外的数据。闭包在日常编码工作中非常常见。但是,它会对性能造成影响。了解闭包我们使用以下示例代码:

1
2
3
4
5
6
function assignEvents(){
var id = "666677";
document.getElementById("save-btn").onclick = function(event) {
saveDocument(id);
};
}

闭包是一种特殊的对象。
它由两部分组成。执行上下文(代号 A),以及在该执行上下文中创建的函数(代号 B)。
当 B 执行时,如果访问了 A 中变量对象中的值(不访问当然不产生闭包),那么闭包就会产生。
在大多数理解中,包括许多著名的书籍,文章里都以函数 B的名字代指这里生成的闭包。而在chrome中,则以执行上下文 A 的函数名代指闭包。

assignEvents 函数为 DOM 元素分配一个事件处理程序。这个处理函数就是一个闭包。为了使该闭包访问 id 变量,必须创建一个特定的作用域链。

我们一起来从作用域的角度分析一下闭包的形成过程:

assignEvents 函数创建并且词法解析后,函数对象assignEvents的[[scope]]属性被初始化,作用域链形成,作用域链中包含了全局对象的所有属性和方法(注意,此时因为 assignEvents 函数还未被执行,所以闭包函数并没有被解析)。

类似这图:
scopechain1.png

assignEvents 开始执行时,创建 Execution Context(执行期上下文),在执行期上下文的作用域链中创建 Activation Object(激活对象),并将 Activation Object(激活对象) 推送到作用域链顶部,在其中保存了函数执行时所有可访问函数内部的数据。激活对象包含 id 变量。

类似这图:
execution_scope1.png

当执行到闭包时,JavaScript 引擎发现了闭包函数的存在,按照通常的手法,将闭包函数解析,为闭包函数对象创建 [[scope]] 属性,初始化作用域链。特别注意的是,这个时候,闭包函数对象的作用域链中有两个对象,一个是 assignEvents 函数执行时的 Activation Object(激活对象) ,还有一个是全局对象,如下图

closure1.png

我们看到图中闭包函数对象的作用域链和 assignEvents 函数的执行期上下文的作用域链是相同的。为什么相同呢?我们来分析一下,闭包函数是在 assignEvents 函数执行的过程中被定义并且解析的,而函数执行时的作用域是 Activation Object(激活对象) ,闭包函数被解析的时候它的作用域正是 assignEvents 作用域链中的第一个作用域对象 Activation Object(激活对象) ,当然,由于作用域链的关系,全局对象作用域也被引入到闭包函数的作用域链中。

在词法分析的时候闭包函数的 [[scope]] 属性 就已经在作用域链中保存了对 assignEvents 函数的 Activation Object(激活对象) 的引用,所以当 assignEvents 函数执行完毕之后,闭包函数虽然还没有开始执行(执行后是另一个作用域链),但依然可以访问 assignEvents 的局部数据,并不是因为闭包函数要访问 assignEvents 的局部变量id,所以当 assignEvents 函数执行完毕之后依然保持了对局部变量id的引用。而是不管是否存在变量引用,都会保存对 assignEvents 的 Activation Object(激活对象)作用域对象的引用。因为在词法分析时,闭包函数没有执行,函数内部根本就不知道是否要对 assignEvents 的局部变量进行访问和操作,所以只能先把 assignEvents 的 Activation Object(激活对象) 作用域对象保存起来,当闭包函数执行时,如果需要访问 assignEvents 的局部变量,那么再去作用域链中查找。

也正是因为这种引用,造成了一个副作用。通常,当执行期上下文被销毁时,函数的激活对象也就被销毁了。当有闭包引用时,激活对象就不会被销毁,因为他仍然被引用。这意味着闭包比非隔离的函数需要更多的内存。

闭包函数执行时创建了自己的 Execution Context(执行期上下文),其作用域链使用了 [[scope]] 属性,其引用了 assignEvents 函数的 Activation Object(激活对象) 和 全局对象。然后为闭包本身创建一个新的 Activation Object(激活对象)。 所以在闭包函数的执行期上下文的作用域链中保存了自己的 Activation Object(激活对象),外层函数 assignEvents Execution Context(执行期上下文)的 Activation Object(激活对象),以及 Global Object(全局对象),如图:

closure2.png

3.提升(hoisting)

提升有变量提升和函数提升之分, 先提升函数声明, 在提升变量声明.

JavaScript 中的 Hoisting (变量提升和函数声明提升)

规则:

  1. 扫描当前函数声明中的代码。函数表达式和箭头函数会被跳过。对于每个被发现的函数,都会创建一个新的函数,并使用函数名称将其绑定到环境中。如果标识符的名称已经存在,那么它的值就会被覆盖。
  2. 然后扫描当前环境的变量。找到使用 var 定义的变量和放置在其他函数之外的变量,并注册一个标识符,其值初始化为 undefined 。如果存在标识符,则该值将保持不变(就是忽略后面的生命, 反正都是undefined)。

注意:用 let 和 const 定义的是块变量,与 var 的处理稍微不同, 不能重复定义。

JavaScript 深入之执行上下文栈

javascript 函数声明和变量声明会被解释器提升到最顶端,但是变量的初始化不会被提升 因为var foo = "变量"; foo被初始化了
其实主要是var foo;并不会覆盖之前的变量

1
2
3
var foo = "function";
var foo;//它只是定义,全不会覆盖变量
console.log(foo);//返回 function

例子: 如果先提升函数声明,在提升变量声明, 那么结果怎么不打印变量,而是函数. (注意只是提升声明而已, 不是提升变量的初始化)

1
2
3
4
5
console.log(foo);   // 这边是执行
function foo(){
console.log("函数声明");
}
var foo = "变量";

解答:

函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖,所以你上面的代码实际上是

1
2
3
4
5
6
function foo(){             // 函数声明提前了
console.log("函数声明");
}
var foo; // 然后是变量声明再提前
console.log(foo); // 执行在这里哦, 执行的语句不会提, 还是在原来的位置
foo = "变量";

在最后再加上打印就能看到函数已经被覆盖了。
注:初始化变量不会把值也提上上去,只会提升变量的声明。(只是提升声明, 运行还是在那行运行的)

再比如:

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

function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

bar();

// 结果是 ??? 1

提升后的结果是

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

var value;
value = 1; // 注意这里条赋值语句之前都是提升, 然后这行开始运行, 赋值了,

bar(); // 这边的bar()是执行前面声明过的函数, 运行前value的值就是1了, 所以最后打印1

这里注意作用域链

3.1 变量提升(variable hoisting)

1
2
3
4
5
6
7
var name="foo";
function fName(){
console.log(name); // undefined
var name="bar";
}
fName();
console.log(name); // foo

输出结果结果分别是 undefined 和 foo。为什么是undefined?

那我们先来分析一下代码 函数fName()的作用域链: 自己的变量对象 ——-> 全局变量对象。解析器在函数执行环境中发现变量 name,因此不会再向全局环境的变量对象中寻找。但是大家要注意的是,解析器在解析第 3 句代码时,还不知道变量name的值,也就是说只知道有变量name,但是不知道它具体的值(因为还没有执行第 4 句代码),因此输出是 undefined,第 7 行输出foo大家应该都理解把(作用域问题)。所以上述代码可以写成下面的形式:

1
2
3
4
5
6
7
8
var name="foo";
function fName(){
var name;
console.log(name); // undefined
name="bar";
}
fName();
console.log(name); // foo

这个现象就是变量提升!

变量提升,就是把变量提升到函数的顶部,需要注意的是,变量提升只是提升变量的声明,不会把变量的值也提升上来

3.2 函数提升

函数提升就是把函数提升到前面。

在JavaScript中函数的创建方式有三种:函数声明(静态的)、函数表达式(函数字面量)、函数构造法(动态的,匿名的)。

函数声明

1
2
3
function f(n1,n2){
//function body;
};

函数表达式的形式如下:

1
2
3
var func1 = function(n1,n2){
//function body;
};

函数构造法构造函数的形式如下:

1
var func2 = new Function("para1","para2",...,"function body");

总结下

再谈js作用域

参考

4 个一起看, 按顺序 1234
JavaScript 核心概念之作用域和闭包 666
上面的看完了可以看下经典题目用 var 的 for setTimeout 这个, 比较 var 定义为什么是 555. 还能学 macro taks 呢.
还有就是换了 let 之后块作用域的话为什么是 01234. 这个块作用域是怎么样的. 闭包的概念: 不只是函数套函数, 还要用到父函数的变量才行, 目的是从为了得到函数内的局部变量. 学习 Javascript 闭包(Closure) . 问为什么闭包执行结束后父函数的AO还在, 因为在闭包还没有执行前的编译阶段, 父函数的AO就在了, 所以闭包执行结束后仍然在, 造成内存. 注意区分编译时的, 然后执行时每次会创建一个执行上下文.
还有就是不加 var 的当做是是全局变量是错误的, 而是加到全局变量 window 的属性中. 省了 this, 看 再谈js作用域 的 551 行. 最大的区别还是定义时和执行时, 这个 a 执行时才有, 不然连 undefined 都不是. 是 not defined
深入理解 JavaScript 中的作用域和上下文 666
讲的大概, 注意了 this 的决定方法 javascript中this指向由函数调用方式决定, 看了作用域就大致知道 this 值, 谁决定谁, 编译是一个, 运行时 this 的变化, 然后决定 AO 中 this 的值(执行阶段又可以分没执行前和执行中). 然后还有 new 的 .JavaScript Prototype(原型) 新手指南
实例分析 JavaScript 作用域
JavaScript 中的 Hoisting (变量提升和函数声明提升) 666

JavaScript 中作用域和作用域链的简单理解(变量提升)
JavaScript 作用域、上下文、执行期上下文、作用域链、闭包 666
这个有个小例子,, 不要和前面闭包搞错哦, 闭包是在执行到那里进行解析才得到作用域链的, 而 foo 是早就得到作用域链了, 然后是执行

1
2
3
4
5
6
7
8
9
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();

但在说作用域链的时候有点出入, 还是以 4 连发为准

前端基础进阶(四):详细图解作用域链与闭包
前端基础进阶系列 贼 6

《高性能 JavaScript》第 2 章
闭包,是真的美 666

react-router

发表于 2019-01-04

react-router

参考

react-router-v4

React Router Tutorial

2019-TODO-list

发表于 2019-01-04 | 分类于 年计划

2019-TODO-list

收集了一些 github、掘金、segmentfault 等平台文章或小册中的内容,完成一些整理并总结!

英语

听力

每日英语听力

计算机基础

刷题

LeetCode

LeetCode solutions with JavaScript

打字

typingclub

JS 篇

导引

给 2019 前端的 5 个建议

专题

预计写四个系列:JavaScript 深入系列、JavaScript 专题系列、ES6 系列、React 系列。666
前端基础进阶系列 666
深入理解 JavaScript 系列 666666
给前端好文多一首歌的时间(九)
程序员练级攻略(2018):前端基础和底层原理
前端基础进阶目录

数据类型

JS 基本数据类型和引用数据类型的区别及深浅拷贝
的第七种数据类型
JS 中 typeof 与 instanceof 的区别
typeof null 为什么等于 object?
为什么用 Object.prototype.toString.call(obj)检测对象类型?
JS 显性数据类型转换和隐性数据类型转换
理解 Object.defineProperty 的作用

this

深入理解 js this 绑定 ( 无需死记硬背,尾部有总结和面试题解析 )
前端基础进阶(五):全方位解读 this
this、apply、call、bind
JavaScript 中的 call、apply、bind 深入理解

作用域链与闭包

我所认识的 JavaScript 作用域链和原型链 666
浅谈 JavaScript 闭包
JavaScript 中作用域和作用域链的简单理解(变量提升) nonono
JavaScript 作用域、上下文、执行期上下文、作用域链、闭包 666

4 连套
JavaScript 核心概念之作用域和闭包 666
深入理解 JavaScript 中的作用域和上下文 666
实例分析 JavaScript 作用域
JavaScript 中的 Hoisting (变量提升和函数声明提升) 666

前端基础进阶(四):详细图解作用域链与闭包 666 还带基础进阶的
JavaScript 闭包入门(译文)
JavaScript 深入之闭包
JavaScript 闭包
浏览器是怎么看闭包的。

原型与原型链

JavaScript 原型与继承的秘密
白话原型和原型链
前端基础进阶(九):详解面向对象、构造函数、原型与原型链
最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)
最详尽的 JS 原型与原型链终极详解,没有「可能是」。(二)
最详尽的 JS 原型与原型链终极详解,没有「可能是」。(三)

Promise

关于 ES6 中 Promise 的面试题

异步

8 张图让你一步步看清 async/await 和 promise 的执行顺序
理解 JavaScript 的 async/await
JavaScript-的-async-await

fetch

深度介绍:也许你对 Fetch 了解得不是那么多

JS 执行底层

前端基础进阶(一):内存空间详细图解
前端基础进阶(二):执行上下文详细图解
前端基础进阶(十二):深入核心,详解事件循环机制
js 中的事件委托或是事件代理详解

ES6/ES7..

ES6 系列之 let 和 const
前端基础进阶(十四):es6 常用基础合集
ES6 系列之箭头函数
JavaScript 初学者必看“箭头函数”
Promise 之你看得懂的 Promise
ES6 系列之我们来聊聊 Promise
Promise 原理讲解 && 实现一个 Promise 对象 (遵循 Promise/A+规范)
web 前端-js 继承的理解
js 深拷贝 vs 浅拷贝
深拷贝的终极探索(90%的人都不知道)
理解 async/await
ES6 系列之我们来聊聊 Async
近一万字的 ES6 语法知识点补充

除此之外强烈推荐冴羽老师的 ES6 系列)文章,深入骨髓的理解 ES6 中的核心

TypeScript

深入理解 TypeScript
TypeScript 体系调研报告
TypeScript 实践

Node

Node 入门
谈谈 Node 中的常见概念
Node & Express 入门指南
Express 使用手记:核心入门
node 进阶——之事无巨细手写 koa 源码
带你走进 koa2 的世界(koa2 源码浅谈)
fly.js—Node 下增强的 API

HTML/CSS 篇

CSS 常见布局方式
【整理】CSS 布局方案
CSS 查漏补缺
[布局概念] 关于 CSS-BFC 深入理解
[译]这些 CSS 命名规范将省下你大把调试时间
CSS 知识总结
前端开发规范:命名规范、html 规范、css 规范、js 规范

HTTP

HTTP 状态码(HTTP Status Code)

面试 — 网络 HTTP
HTTP 最强资料大全
我知道的 HTTP 请求

性能&优化篇

深入浅出浏览器渲染原理
浏览器的回流与重绘 (Reflow & Repaint)
浏览器缓存
浏览器前端优化
浏览器渲染引擎
JavaScript 浏览器事件解析
前端性能——监控起步
javascript 性能优化
浏览器性能优化-渲染性能
浏览器渲染过程与性能优化
现代浏览器性能优化-CSS 篇
浏览器工作原理及 web 性能优化

Webpack 篇

webpack 详解
Webpack4 优化之路
webpack4 之高级篇
webpack4-用之初体验,一起敲它十一遍
📚 免费的渐进式教程:Webpack4 的 16 篇讲解和 16 份代码
手写一个 webpack4.0 配置

React 篇

五星推荐的系列文章清单
胡子大哈 React.js 小书
TypeScript 2.8 下的终极 React 组件模式

面试篇

HTML&&css 面试题
Excuse me?这个前端面试在搞事!
80% 应聘者都不及格的 JS 面试题
2019 年前端面试都聊啥?一起来看看
一篇文章搞定前端面试
如何轻松拿到淘宝前端 offer | 掘金技术征文
腾讯前端面试篇(一)
腾讯前端面试篇(二)
30secondsofinterviews

后端

kubernetes

go

HTTP

js事件处理

发表于 2018-12-28 | 分类于 javascript教程

js事件处理

客户端js程序采用了异步事件驱动编程模型(有介绍, 搞个链接). 在这种程序设计风格下, 当文档, 浏览器, 元素或与之相关的对象发生某些有趣的事情时, web浏览器就会产生事件(event).

例如: 当web浏览器加载完文档, 用户把鼠标指针移到超链接上或敲击键盘时, web浏览器都会产生事件. 如果js应用程序关注特定类型的事件, 那么他可以注册当这类事件发生时要调用的一个或多个函数.

注意: 这种风格并不止应用于web编程, 所有使用图形用户界面的应用程序都采用了它, 他们静待某些事情发生(即, 它们等待事件发生), 然后它们响应.

事件就是web浏览器通知应用程序发生了什么事情. 事件不是js对象, 不会出现在程序源代码中. 当然, 会有一些事件相关的对象出现在源代码中, 他们需要技术说明.

事件类型(event type)是一个用来说明发生什么类型事件的字符串.
例如: mousemove表示用户鼠标移动, keydown表示键盘上某个键被按下, 而load表示文档(或某个其他资源)从网络上加载完毕.
由于事件类型只是一个字符串, 因此实际上有时会称之为事件名字(event name), 我们用这个名字来标识所谈论的特定类型的事件.

事件目标(event target)是发生的事件或与之相关的对象.当将事件时, 我们必须同时指明类型(type, name)和目标(target).

例如: window上的load事件或<button>元素的click事件. 在客户端的js应用程序中, Window, Document和Element对象是最常见的事件目标, 但某些事件是由其他类型的对象触发.比如XMLHttpRequest对象触发的readystatechange事件.

事件处理程序event handle或事件监听程序(event listener)是处理或响应事件的函数. 应用程序通过指明事件类型type和事件目标target, 在web浏览器中注册他们的事件处理程序handle, listener. 当在特定的目标上发生特定类型的事件时, 浏览器会调用对应的处理程序. 当对象上注册的事件处理程序被调用时, 我们有时会说浏览器”触发”(fire, trigger)和”派发”(dispatch)了事件. 有很多注册事件处理程序的方法.

事件对象(event object)是与特定事件相关且包含有关该事件详细信息的对象. 事件对象object作为参数传递给事件处理程序函数handle. 所有的事件对象object都有用来指定事件类型的type属性和指定事件目标的target属性. 每个事件类型都为其相关事件对象object定义一组属性.
例如: 鼠标事件的相关对象会包含鼠标指针的坐标, 而键盘事件的相关对象会包含按下的键和辅助键的详细信息. 许多事件类型仅定义了像type和target这样少量的标准属性, 就无法获取许多其他有用的信息.

事件传播(event propagation)是浏览器决定哪个对象触发其事件处理程序的过程. 对于单个对象的特定事件(比如Window对象的load事件), 必须是不能传播的. 当文档元素上发生某个类型的事件时, 然而, 他们会在文档树上向上传播或”冒泡(bubble)”, 如果用户移动鼠标指针到超链接上, 在定义这个链接的<a>元素上首先会触发mousemove事件, 然后是在这个容器元素上触发这个时间, 也许是<p>元素,<div>元素或Document对象本身. 有时, 在Document或其他容器元素上注册单个事件处理程序比在每个独立的目标元素上都注册程序要更方便. 事件处理程序能通过调用方法或设置事件对象属性来阻止事件传播, 这样它就能停止冒泡且将无法在容器元素上触发处理程序.

事件传播的另外一种形式称之为事件捕获(event capturing), 在容器元素上注册的特定处理程序有机会在事件传播倒真实目标之前拦截(或”捕获”)它., 但是,当处理鼠标拖放事件时, 捕获或”夺取”鼠标时间的能力是必须的.

一些事件有与之相关的默认操作. 例如: 当超链接上发生click事件时, 浏览器的默认操作是按照链接加载新页面. 事件处理程序可以通过返回一个适当的值, 调用时间对象的某个方法或设置事件对象的某个属性来阻止默认操作的发生. 这有时称为”取消”事件.

17.1事件类型

web初期,客户端程序员只能使用很少部分事件,load, click, mouseover, 现在有新事件,3个来源:

  • 3级DOM事件(DOM Level Events)规范,经过长期的停滞之后, 在W3Cde主持下又开始焕发生机.
  • HTML5规范及相关衍生规范的大量新API定义了新事件, 比如历史管理, 拖放, 跨文档通信,以及视频和音频的播放.
  • 基于触摸和支持JavaScript的移动设备的出现, 他们需要定义新的触摸和手势事件类型.

注意:许多新事件类型尚未广泛实现, 定义它们的标准也依旧处于草案阶段.

事件分类: 大致先分6类吧

1依赖与设备的输入事件:
有些事件和特定输入设备直接相关, 比如鼠标和键盘. 包括诸如mousedown, mousemove,mouseup,keypress,keyup这样传统事件类型, 也包括像touchmove,getsturcchange这样新的触摸事件类型

2独立于设备的输入事件:
有些输入事件没有直接相关的特定输入设备. 比如click事件表示激活了链接, 按钮或其他文档元素, 这通常是通过鼠标单击实现, 但也能通过键盘或触控感知设备上的手势来实现. 尚未广泛实现的textinput事件就是一个独立与设备的输入事件, 他既能取代按键事件并支持键盘输入, 也可以取代剪切和粘贴与手写识别的事件.

3用户界面事件:
用户界面事件是比较高级的事件, 通常出现在定义web应用用户界面的HTML表单元素上. 包括文本输入域获取键盘焦点的focus事件, 用户改变表单元素显示值的change事件和用户单击表单中的提交按钮的submit事件

4状态变化事件:
有些事件不是由用户活动而是由网络或浏览器活动触发, 用来表示某种生命周期或相关状态的变化. 当文档完全加载时, 在Window对象上会发生load事件, 这可能是这类事件中最常用的, DOMContentLoaded事件与此类似, HTML5历史管理机制会触发popstate事件来响应浏览器的后退按钮. HTML5离线web应用API包括online和offline事件. 当向服务器请求的数据准备就绪事, 如何利用readystatechange事件得到通知, 类似的, 用于读取用户选择本地文件中的新API使用像loadstate, progress和loadend事件来实现I/O过程的异步通知.

5特定API事件:
HTML5及相关规范定义的大量web API都有自己的事件类型. 拖放API定义了诸如dragstart, dragenter, dragover和drop事件, 应用程序想自定义拖放源(drag source)或拖放目标(drop target)就必须处理这些相关事件. HTML5的<video>和<audio>元素定义一长串像waiting, playibg, seeking和volumechange等相关事件, 这些事件通常仅用于web应用, 这些web应用希望为视频和音频的播放定义自定义控件.

6计数器和错误处理程序:
在第14章中介绍过计时器(timer)和错误处理程序(error handler)属于客户端JavaScript异步编程模型的部分, 并有相似的事件.

17.1.1传统事件类型

处理鼠标, 键盘, HTML表单和window对象的事件都是web应用中最常用的, 他们已经存在很长的时间并得到了广泛的支持.

1.表单事件

回到web和JavaScript的早期, 表单和超链接都是网页中最早支持脚本的元素. 这就意味着表单事件是所有事件类型中最稳定且得到良好支持的那部分.

  • 当提交表单和重置表单时, <form>元素会分别触发submit和reset事件.
  • 当用户和类按钮表单元素(包括单选按钮和复选框)交互时, 他们会发生click事件.
  • 当用户通过输入文字, 选择选项或选择复选框来改变相应表单元素的状态时, 这些通常维护某种状态的表单元素会触发change事件.
  • 对于文本输入域, 只有用户和表单元素完成交互并通过Tab键或单击的方式移动焦点到其他元素上时才会触发change事件.响应通过键盘改变焦点的表单元素在得到和失去焦点时会分别触发focus和blur事件

15.9.3节涵盖了所有表单相关事件的详细信息, 不过还有一些进一步说明. 通过事件处理程序能取消submit和reset事件的默认操作, 某些click事件也是如此. focus和blur事件不会冒泡, 但其他所有表单事件都可以. IE定义了focusin和focusout事件可以冒泡, 他们可以用于替代focus和blur事件. jQuery库为不持之focusin和focusout事件的浏览器模拟了这两个事件, 同时3级DOM事件规范也正在标准化他们.

最后注意, 无论用户何时输入文字(通过键盘或剪切和粘贴)到<textarea>和其他文本输入表单元素, 除IE外的浏览器都会触发input事件. 不像change事件, 每次文字插入都会触发input事件. 遗憾的是, input事件的事件对象没有指定输入文本的内容 (稍后介绍的textinput事件将会成为这个事件的有用替代方案)

2.Window事件

window事件是指事件的发生于浏览器窗口本身而非窗口中显示的任何特定文档内容相关. 但是, 这些事件中有一些会和文档元素发生的事件同名.

load事件是这些事件中最重要的一个, 当文档和其所有外部资源(比如图片)完全加载并显示给用户时就会触发它. 有关load事件的讨论贯穿整个第13章. DOMContentLoaded和readystatechange是load事件的替代方案, 当文档和其他元素为操作准备就绪, 但外部资源完全加载完毕之前, 浏览器就会尽在触发他们. 17.4有这些与文档加载相关事件的示例.

unload事件和load相对, 当用户离开当前文档转向其他文档时会触发它. unload事件处理程序可有用于保护用户的状态, 但他不能用于取消用户转向其他地方. beforeunload事件和unload事件类似, 但他能提供询问用户是否正确离开当前页面的机会. 如果beforeunload的处理程序返回字符串, 那么在新页面加载之前, 字符串会出现在展示给用户确认的对话框上, 这样用户就有机会取消其跳转而留在当前页上.

window对象的onerror属性有点像事件处理程序, 当JavaScript出错时会触发它. 但是, 他不是真正的事件处理程序, 因为他能用不同的参数来调用. 更多详细信息看14.6节

像<img>元素这样的单个文档元素也能为load和error事件注册处理程序. 当外部资源(例如图片)完全家在或发生阻止加载的错误时就会触发它们. 某些浏览器也支持abort事件(HTML5将其标准化), 当图片(或其他网络资源)因为用户停止加载进程而导致失败就会触发它.

前面介绍的表单元素的focus和blur事件也能用做Window事件, 当浏览器窗口从操作系统中得到或失去键盘焦点时会触发它们.

最后, 当用户调整浏览器窗口大小或滚动它时会触发resize和scroll事件.scroll事件也能在任何可以滚动的文档元素上触发, 比如那些设置CSS的overflow属性的元素. 传递给resize和scroll事件处理程序的事件对象是一个非常普遍的Event对象, 他没有制定调整大小或发生滚动的详细信息属性, 但可以通过15.8节介绍的技术来确定新窗口的尺寸和滚动条的位置.

3.鼠标事件

当用户在文档上移动或单击鼠标时都会产生鼠标事件. 这些事件在鼠标指针所对应的最深嵌套元素上触发, 但他们会冒泡直到文档最顶层. 传递给鼠标事件处理程序的事件对象有属性集, 它们描述了当事件发生时鼠标的位置和按键状态, 也指明了当时是否有任何辅助键按下. clientX和clientY属性制定了鼠标在窗口坐标中的位置. button和which属性指定了按下的鼠标键是哪个.(无论如何请看Event参考页, 因为这些属性难以简单使用.) 当键盘辅助键按下时, 对应的属性altkey, ctrlKey, metaKey和shiftKey会设置为true. 而对于click事件, detail事件指定了其是单击, 双击还是三击.

用户每次移动或拖动鼠标时, 会触发mousemove事件. 这些事件的发生非常繁琐, 所以mousemove事件处理程序一定不能触发计算密集型任务. 当用户按下或释放鼠标按键时, 会触发mousedown和mousemove事件. 通过注册mousedown和mousemove事件处理程序, 可以探测和响应鼠标的拖动. 合理地这样做能捕获鼠标事件, 甚至当鼠标从开始元素移出时我们都能持续地接收到mousemove事件.

在mousedown和mouseup事件队列之后, 浏览器也会触发click事件. 之前介绍过click事件是独立于设备的表单事件, 但实际上他不仅仅在表单元素上触发, 他可以在任何文档元素上触发, 同时传递拥有之前介绍的所有鼠标相关额外字段的事件对象. 如果用户在相当短的时间内连续两次单击鼠标按键, 跟在第二个click事件之后是dblick事件. 当单击鼠标右键时, 浏览器通常会显示上下文菜单(context menu). 在显示菜单之前, 他们通常会触发contextmenu事件, 而取消这个事件就可以阻止菜单的显示. 这个事件也是获得鼠标右击通知的简单方法.

当用户移动鼠标指针从而使他悬停到新元素上时, 浏览器就会在该元素上触发mouseover事件. 当鼠标移动指针从而使他不在悬停在某个元素上时, 李兰器就会在该元素上触发mouseout事件. 对于这些事件, 事件对象将有relatedTarget属性指明这个过程设计的其他元素. (到Event参考页查看relatedTarget属性的IE等效属性) mouseover和mouseout事件和这里介绍的素有鼠标事件一样会冒泡. 但这通常不方便, 因为当触发mouseout事件处理程序时, 你不得不检查鼠标是否真的离开目标元素还是仅仅是从这个元素的一个子元素移动到另一个. 正因为如此, IE提供了这些事件的不冒泡版本mouseenter和mouseleave, JQuery模拟非IE的浏览器中这些事件的支持, 同时3级DOM事件规范把它们标准化了.

当用户滚动鼠标滚轮时, 浏览器触发mousewheel事件(或在firefox中是DOMMouseScroll事件). 传递的时间对象属性指定滚轮转动的大小和方向. 3级DOM事件规范正在标准化一个更通用的多维wheel事件, 一旦实现将取代mousewheel和DOMMouseScroll事件.

4.键盘事件

当键盘聚焦到Web浏览器时, 用户每次按下或释放键盘上的按键时都会产生事件. 键盘快捷键对于操作系统和浏览器本身有特殊意义, 他们经常被操作系统或浏览器”吃掉”, 并对JavaScript事件处理程序不可见. 无论任何文档元素获取键盘焦点都会触发键盘事件, 并且他们会冒泡到Document和Window对象. 如果没有元素获取焦点, 可以直接在文档上触发事件. 传递给键盘事件处理程序的事件对象有keyCode字段, 他指定按下或释放的键是哪个. 除了keyCode, 键盘事件对象也有altKey, ctrlKey, metaKey和shiftKey, 描述键盘辅助键的状态.

keydown和keyup事件是低级键盘事件, 无论何时按下或释放按键(甚至是辅助键)都会触发他们.
当keydown事件产生可打印字符时, 在keydown和keyup之间会触发另外一个keypress事件.
当按下键重复产生字符时, 在keyup事件之前可能产生很多keypress事件. keypress是较高级的文本事件, 其事件对象指定产生的字符而非按下的键.

所有浏览器都支持keydown, keyup和keypress事件, 但有一些互用性问题, 因为事件对象的keyCode属性值从未标准化过. 3级DOM事件规范尝试解决之前的互用性问题, 但尚未实施.

17.1.2 DOM事件

W3C开发3级DOM事件规范已经长达十年之久. 现在终于处于标准化的”最后征集工作草案”阶段, 它标准化了前面介绍的许多传统事件, 同时增加了这里介绍的一些新事件. 这些新事件类型尚未得到广泛支持, 一旦标准确定, 我们就期望浏览器厂商能实现他们.

如上所述, 3级DOM事件规范标准化了不冒泡的focusin和focusout事件来取代冒泡的focus和blur事件. 此版本的标准也弃用了大量由2级DOM事件规范定义但未得到广泛实现的事件类型. 浏览器依旧允许产生像DOMActive, DOMFocusIn和DOMNodeInserted这样的事件, 但他们不在必要, 同时本书的文档也不会列出他们(在名字中使用”DOM”的唯一常用事件就是DOMContentLoaded, 这个事件由Mozilla引入, 但绝不属于DOM事件标准的一部分).

3级DOM事件规范中新增内容有通过wheel事件对二维鼠标滚轮提供标准支持, 通过textinput事件和传递新KeyboardEvent对象作为参数给keydown, keyup和keypress的事件处理程序来给文本输入事件提供更好的支持.

wheel事件的处理程序接收到的事件对象除了所以普通鼠标事件属性, 还有delatX, delatY和delatZ属性来报告三个不同的鼠标滚轴. 大多数鼠标滚轮是一维或二维的, 并不使用delatZ. 更多关于mousewheel事件的内容请参见17.6节.

如上所述, 3级DOM事件规范定义了keypress事件, 但不赞成使用它而使用称为textinput的新事件. 传递给textinput事件处理程序的时间对象不再有难以使用的数字keyCode属性值, 而有指定输入文本字符串的data属性. textinput事件不是键盘特定事件, 无论通过键盘, 剪切和粘贴, 拖放方式, 每当发生文本输入时就会触发它. 规范定义了时间对象的inputMethod属性和一组代表各种文本输入种类的常量(键盘, 粘贴, 拖放,手写和语音识别等). 在写本章时, Safari和Chrome使用混合大小写的textInput来支持这个事件版本, 其事件对象有data属性但没有inputMethod属性.

新DOM标准通过在事件对象中加入新的key和char属性来简化keydown, keyup和keypress事件, 这些属性都是字符串. 对于产生可打印字符的键盘事件, key和char值将等于生成的文本. 对于控制键, key属性将会是像标识键的”Enter“, “Delete“和”Left“这样的字符串, 而char属性将是null, 或对于像Tab这样的控制键有一个字符编码, 它将是按键产生的字符串. 在写本章时, 尚未有浏览器支持key和char属性.

17.1.3 HTML5事件

HTML5及相关标准定义了大量新的web应用API(第22章), 其中许多API都定义了事件. 本节列出并简要介绍这些HTML5和web应用事件. 其中一些事件现在已经可以开始使用.

广泛推广的HTML特性之一是加入用于播放音频和视频的<audio>和<video>元素. 这些元素有长长的事件列表, 他们触发各种关于网络事件, 数据缓冲状况和播放状态的通知:

1
2
3
4
5
canplay         loadeddata      playing     stalled
canplaythrough loadedmetadata progress suspend
durationchange loadstart ratechange timeupdate
emptied pause seeked volumechange
ended play seeking waiting

传递给美体时间处理程序的事件对象普通且没有特殊属性, target属性用于识别<audio>和<video>元素, 然而这些元素有多相关的属性和方法. 21.2节有更多关于这些元素及其属性和事件的详细内容.

HTML5的拖放API允许JavaScript应用参与基于操作系统的拖放操作, 实现web和原生应用间的数据传输. 该API定义了如下7个事件类型:

1
2
3
dragstart       drag        dragend
dragenter dragover dragleave
drop

触发拖放事件的事件对象和通过鼠标事件发送的对象类似, 其附加属性dataTransfer持有DataTransfer对象, 它包含关于传输的数据和其中可用的格式信息.

HTML5定义了历史管理机制, 它允许web应用同浏览器的返回和前进按钮交互. 这个机制涉及的事件是hashchange和popstate, 这些事件是类似load和unload的生命周期通知事件, 他在Window对象上触发而非任何单独的文档元素.

HTML5为HTML表单定义了大量的新特性. 除了标准化前面介绍的表单输入事件外, HTML5也定义了表单验证机制, 包括当验证失败时在表单元素上会触发invalid事件. 除Opera外的浏览器厂商已经慢慢实现HTML5的新表单特性和事件, 但本书没有涵盖他们.

HTML5包含了对离线web应用的支持, 他们可以安装到本地应用缓存中, 所以即使路蓝旗离线时它们依旧能运行, 比如当移动设备不在网络范围内时. 相关的两个最重要的事件是offline和online, 无论何时浏览器失去或得到网络连接都会在Window对象上触发它们. 标准还定义了大量其他事件来通知应用下载进度和应用缓存更新:

1
2
cached      checking        downloading     error
noupdate obsolete progress upateready

很多新web应用API都使用message事件进行异步通信. 跨文档通信API允许一台服务器上的文档脚本能和另一台服务器上的文档脚本交换信息. 其工作受限于同源策略这一安全方式. 发送的每一条消息都会在接受文档的Window上触发message事件. 传递给处理程序的事件对象包含data属性, 它有保存信息内容以及用于识别消息发送者的source属性和origin策略. message事件的使用方式与使用Web Worker通信, 通过Server-Sent事件和WebSocket进行网络通信相似.

HTML5及相关标准定义了一些不在窗口, 文档和文档元素的对象上触发的事件. XMLHttpRequest规范第2版和File API规范都定义了一系列事件来跟踪异步I/O的进度. 它们在XMLHttpRequest或FileReader对象上触发事件. 每次读取操作都是以loadstart事件开始, 接着是progress和loadend事件. 此外, 每个操作仅在最终loadend事件之前会有load,error或abort事件.

最后, HTML5及相关标准定义了少量庞杂的事件类型. 在Window对象上发生的web存储API定义了storage事件用于通知存储数据的改变. HTML5页标准化了最早由Microsoft在IE中引入的beforeprint和afterprint事件. 顾名思义, 当文档打印之前或之后立即在Window对象触发这些事件, 它提供了打印文档时添加或删除类似日期或事件等内容的机会. (这些事件不应该用于处理打印文档的样式, 因为CSS媒体类型更适合这个用途.)

17.1.4 触摸屏和移动设备事件

从输入URL到页面加载发生了什么?

发表于 2018-12-28 | 分类于 网络

从输入 URL 到页面加载发生了什么?

总体来说分为以下几个过程:

  1. DNS 解析
  2. TCP 连接
  3. 发送 HTTP 请求
  4. 服务器处理请求并返回 HTTP 报文
  5. 浏览器解析渲染页面
  6. 连接结束

在此之前的准备

从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系!666

level3:

基本能到这一步的,不是高阶就是接近高阶,因为很多概念并不是靠背就能理解的,而要理解这么多,需形成体系,一般都需要积累,非一日之功。

一般包括什么样的回答呢?(这里就以自己的简略回答进行举例),一般这个阶段的人员都会符合若干条(不一定全部,当然可能还有些是这里遗漏的):

  • 首先略去那些键盘输入、和操作系统交互、以及屏幕显示原理、网卡等硬件交互之类的(前端向中,很多硬件原理暂时略去。。。)
  • 对浏览器模型有整体概念,知道浏览器是多进程的,浏览器内核是多线程的,清楚进程与线程之间得区别,以及输入 url 后会开一个新的网络线程
  • 对从开启网络线程到发出一个完整的 http 请求中间的过程有所了解(如 dns 查询,tcp/ip 链接,五层因特尔协议栈等等,以及一些优化方案,如 dns-prefetch)
  • 对从服务器接收到请求到对应后台接收到请求有一定了解(如负载均衡,安全拦截以及后台代码处理等)
  • 对后台和前台的 http 交互熟悉(包括 http 报文结构,场景头部,cookie,跨域,web 安全,http 缓存,http2.0,https 等)
  • 对浏览器接收到 http 数据包后的解析流程熟悉(包括解析 html,词法分析然后解析成 dom 树、解析 css 生成 css 规则树、合并成 render 树,然后 layout、painting 渲染、里面可能还包括复合图层的合成、GPU 绘制、外链处理、加载顺序等)
  • 对 JS 引擎解析过程熟悉(包括 JS 的解释,预处理,执行上下文,VO,作用域链,this,回收机制等)

前端向知识的重点

此部分的内容是站在个人视角分析的,并不是说就一定是正确答案

首先明确,计算机方面的知识是可以无穷无尽的挖的,而本文的重点是梳理前端向的重点知识

对于前端向(这里可能没有提到 node.js 之类的,更多的是指客户端前端),这里将知识点按重要程度划分成以下几大类:

  • 核心知识,必须掌握的,也是最基础的,譬如浏览器模型,渲染原理,JS 解析过程,JS 运行机制, JS 引擎解析流程等,作为骨架来承载知识体系
  • 重点知识,往往每一块都是一个知识点,而且这些知识点都很重要,譬如 http 相关,web 安全相关,跨域处理等
  • 拓展知识,这一块可能更多的是了解,稍微实践过,但是认识上可能没有上面那么深刻,譬如五层因特尔协议栈,hybrid 模式,移动原生开发,后台相关等等(当然,在不同领域,可能有某些知识就上升到重点知识层次了,譬如 hybrid 开发时,懂原生开发是很重要的)

梳理主干流程

回到这道题上,如何回答呢?先梳理一个骨架

知识体系中,最重要的是骨架,脉络。有了骨架后,才方便填充细节。所以,先梳理下主干流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 从浏览器接收url到开启网络请求线程(这一部分可以展开浏览器的机制以及进程与线程之间的关系)

2. 开启网络线程到发出一个完整的http请求(这一部分涉及到dns查询,tcp/ip请求,五层因特网协议栈等知识)

3. 从服务器接收到请求到对应后台接收到请求(这一部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等)

4. 后台和前台的http交互(这一部分包括http头部、响应码、报文结构、cookie等知识,可以提下静态资源的cookie优化,以及编码解码,如gzip压缩等)

5. 单独拎出来的缓存问题,http的缓存(这部分包括http缓存头部,etag,catch-control等)

6. 浏览器接收到http数据包后的解析流程(解析html-词法分析然后解析成dom树、解析css生成css规则树、合并成render树,然后layout、painting渲染、复合图层的合成、GPU绘制、外链资源的处理、loaded和domcontentloaded等)

7. CSS的可视化格式模型(元素的渲染规则,如包含块,控制框,BFC,IFC等概念)

8. JS引擎解析过程(JS的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)

9. 其它(可以拓展不同的知识模块,如跨域,web安全,hybrid模式等等内容)

核心知识梳理

多进程的浏览器

浏览器是多进程的,有一个主控进程,以及每一个 tab 页面都会新开一个进程(某些情况下多个 tab 会合并进程)

进程可能包括主控进程,插件进程,GPU,tab 页(浏览器内核)等等

  • Browser 进程:浏览器的主进程(负责协调、主控),只有一个, 是其他进程的父进程
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU 进程:最多一个,用于 3D 绘制
  • 浏览器渲染进程(内核):默认每个 Tab 页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白 tab 会合并成一个进程)

例如查看 chrome 的 task manager: 在 window => task manager

taskManager1.png

多线程的浏览器内核(浏览器渲染进程)

每一个tab 页面可以看作是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程

  • GUI 线程
  • JS 引擎线程
  • 事件触发线程
  • 定时器线程
  • 网络请求线程

thread1.jpg

可以看到,里面的JS 引擎是内核进程中的一个线程,这也是为什么常说JS 引擎是单线程的

从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理

进程和线程区别

  • 进程是 cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是 cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

打开 mac 的 monitor 可以看到一个进程中有多少线程, 系统按进程来分配资源. 点开详情可以看到父进程是什么
process1.png
process2.png

对于 chrome 这个程序而言, 他有一个主进程Google Chrome, 以及对应 tab 页的Google Chrome Helper
mac 下 chrome 浏览器的标签页、进程和内存分配

这里可以看到一个主进程以及其他内核进程, 记得在 monitor 中按 PID 升序, 在 chrome 中的task manager中也是, 可以看到对应的.这里也更加确定了浏览器时多进程的(进程优化, 比如知乎的不是每个 tab 创建一个进程)
taskManager2.png
chromeHelper1.png

注意:在这里浏览器应该也有自己的优化机制,有时候打开多个 tab 页后,可以在 Chrome 任务管理器中看到,有些进程被合并了 (所以每一个 Tab 标签对应一个进程并不一定是绝对的, 比如知乎的那个进程)
通过 monitor 和 task Manager去研究 mac 下 chrome 浏览器的标签页和进程的关系以及标签页的内存分配,发现 chrome 默认会启动一个主进程和两个子进程,之后每启动一个标签页会启动 2 个进程,加载完成后会结束一个。每个标签页都会分配实际内存和虚拟内存,当实际内存达到 300M 左右时,之后就只会分配虚拟内存。因为这种机制的存在,mac 下页面还是没那么容易因为内存溢出而崩溃的,更多的要去关心 windows 下的内存占用情况。
浏览器多进程好处, 防止页面或第三方插件崩了就全崩了, 就是内存消耗多点.

浏览器内核(渲染进程)

对前端而言, 页面的渲染,JS 的执行,事件的循环,都在这个进程内进行。还是来说说这个进程里面 5 个主要线程.

1 GUI 渲染线程

  • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
  • 注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。

2 JS 引擎线程

  • 也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)
  • JS 引擎线程负责解析 Javascript 脚本,运行代码。
  • JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序
  • 同样注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

只是 GUI 渲染线程和 JS 引擎线程是互斥的

3 事件触发线程

  • 归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助)
  • 当 JS 引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
  • 注意,由于JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)

4 定时触发器线程

  • 传说中的setInterval与setTimeout所在线程
  • 浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
  • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行)
  • 注意,W3C 在 HTML 标准中规定,规定要求setTimeout中低于 4ms 的时间间隔算为 4ms。

5 异步 http 请求线程

  • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

3.4.5 都有事件队列哦, 可以并行的, 3 还控制事件循环机制, 就 1 和 2 是互斥的, 其他可以并行.

补充: HTML5 的 Web Worker线程
聊聊 JavaScript 与浏览器的那些事 - 引擎与线程

上面链接中的 一个浏览器的主要组件可分为如下几个部分这块不用看,看上面的多进程浏览器, 多线程内核就行, 免得糊涂, 最后也得出我们会更注重呈现引擎和 JavaScript 解释器的部分. ]
Web Worker 允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM 。所以,这个新标准并没有改变 JavaScript 单线程的本质。
然后 注意定时器和异步是不同的线程控制哦

Browser 进程和浏览器内核(Renderer 进程)的通信过程(父进程和子进程之间通信)

那么接下来,再谈谈浏览器的 Browser 进程(控制进程)是如何和内核通信的,这点也理解后,就可以将这部分的知识串联起来,从头到尾有一个完整的概念。

如果自己打开任务管理器(Activity Monitor),然后打开一个浏览器(chrome),就可以看到:任务管理器中出现了两个进程(一个是主控进程(Google chrome),一个则是打开 Tab 页的渲染进程Google Chrome Helper),
然后在这前提下,看下整个的过程:(简化了很多)

  • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
  • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染
    • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作 DOM(这样可能会造成回流并重绘)
    • 最后Render进程将结果传递给Browser进程
  • Browser进程接收到结果并将结果绘制出来

这里绘一张简单的图:(很简化)这里是进程不是线程

render1.png

深入浅出浏览器渲染原理

梳理浏览器内核中线程之间的关系

到了这里,已经对浏览器的运行有了一个整体的概念,接下来,先简单梳理一些概念, 在回顾下图.

thread1.jpg

GUI 渲染线程与 JS 引擎线程互斥

由于JavaScript是可操纵 DOM的,如果在修改这些元素属性同时渲染界面(即 JS 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起,
GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。

JS 阻塞页面加载

从上述的互斥关系,可以推导出,JS 如果执行时间过长就会阻塞页面。

譬如,假设 JS 引擎正在进行巨量的计算,此时就算 GUI 有更新,也会被保存到队列中,等待 JS 引擎空闲后执行, 就是后面的task => 渲染 => task。
然后,由于巨量计算,所以 JS 引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免 JS 执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

WebWorker,JS 的多线程?

前文中有提到 JS 引擎是单线程的,而且 JS 执行时间过长会阻塞页面,那么 JS 就真的对cpu密集型计算无能为力么?

所以,后来 HTML5 中支持了Web Worker。

MDN 的官方解释是:

Web Worker为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的 JavaScript 文件
这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的 window
因此,使用 window 快捷方式获取当前全局的范围 (而不是 self) 在一个 Worker 内将返回错误

这样理解下:

  • 创建Worker时,JS引擎线程向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作 DOM)
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响 JS 引擎主线程,
只待计算出结果后,将结果通信给主线程即可,perfect!

而且注意下,JS 引擎是单线程的,这一点的本质仍然未改变,Worker 可以理解是浏览器给 JS 引擎开的外挂,专门用来解决那些大量计算问题。

其它,关于 Worker 的详解就不是本文的范畴了,因此不再赘述。

Web Worker 使用教程

  • 主线程使用 worker.postMessage() 来发送, 使用 worker.onmessage 来监听 , 使用 worker.terminate() 来关闭. 事件返回的 e.data 是数据.
  • 子线程使用 self.postMessage() 来发送, 使用 self.onmessage 来监听 , 使用 self.close() 来关闭

WebWorker 与 SharedWorker

既然都到了这里,就再提一下SharedWorker(避免后续将这两个概念搞混)

WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享
所以 Chrome 在Render进程中(每一个 Tab 页就是一个render进程)创建一个新的线程来运行Worker中的 JavaScript 程序。

SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用

所以 Chrome 浏览器为SharedWorker单独创建一个进程来运行 JavaScript 程序,在浏览器中每个相同的 JavaScript 只存在一个SharedWorker进程,不管它被创建多少次。

看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker 只是属于render进程下的一个线程.

简单梳理下浏览器渲染流程

为了简化理解,前期工作直接省略成:

  • 浏览器输入 url,浏览器主进程接管,开一个下载线程,
  • 然后进行 http 请求(略去 DNS 查询,IP 寻址等等操作),然后等待响应,获取内容,
  • 随后将内容通过 RendererHost 接口转交给Renderer进程
  • 浏览器渲染流程开始

浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

  • 解析html建立dom 树
  • 再解析css一起构建render 树(将 CSS 代码解析成树形的数据结构,然后结合 DOM 合并成 render 树)
  • 布局render 树(Layout/reflow),负责各元素尺寸、位置的计算
  • 绘制render 树(paint),绘制页面像素信息
  • 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

所有详细步骤都已经略去,渲染完毕后就是load事件了,之后就是自己的 JS 逻辑处理了

既然略去了一些详细的步骤,那么就提一些可能需要注意的细节把。

这里重绘参考来源中的一张图:(参考来源第一篇)

render2.png

一篇文章说清浏览器解析和 CSS(GPU)动画优化 666

上面链接讲了 js 和会作用在DOM tree和style rules, 讲的细点.

load 事件与 DOMContentLoaded 事件的先后

上面提到,渲染完毕后会触发load事件,那么你能分清楚load事件与DOMContentLoaded事件的先后么?

很简单,知道它们的定义就可以了:

当 DOMContentLoaded 事件触发时,仅当 DOM 加载完成,不包括样式表,图片。
(譬如如果有 async 加载的脚本就不一定完成)

当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片都已经加载完成了。
(渲染完毕了)

所以,顺序是:DOMContentLoaded -> load

css 加载会造成阻塞吗 看这个链接里的DOMContentLoaded, css 会阻塞 Dom 渲染和 js 执行,而 js 会阻塞 Dom 解析。

  1. 如果页面中同时存在css和js,并且存在js在css后面,则DOMContentLoaded事件会在css加载完后才执行。(加载完全部 css 么?)
  2. 其他情况下,DOMContentLoaded都不会等待css加载,并且DOMContentLoaded事件也不会等待图片、视频等其他资源加载。

css 加载是否会阻塞 dom 树渲染?

css 加载会造成阻塞吗

这里说的是头部引入 css 的情况

首先,我们都知道:css 是由单独的下载线程异步下载的。

然后再说下几个现象:

  • css 加载不会阻塞 DOM 树解析(异步加载时 DOM 照常构建)
  • 但会阻塞 render 树渲染(渲染时需等 css 加载完毕,因为 render 树需要 css 信息)

这可能也是浏览器的一种优化机制。

因为你加载 css 的时候,可能会修改下面 DOM 节点的样式,
如果 css 加载不阻塞 render 树渲染的话,那么当 css 加载完之后,
render 树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。
所以干脆就先把 DOM 树的结构先解析完,把可以做的工作做完,然后等你 css 加载完之后,
在根据最终的样式来渲染 render 树,这种做法性能方面确实会比较好一点。

普通图层和复合图层

渲染步骤中就提到了composite概念。

可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层

首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)

其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层。

然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源
(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)

可以简单理解下:GPU 中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒

可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息

如何变成复合图层(硬件加速)

将该元素变成一个复合图层,就是传说中的硬件加速技术

  • 最常用的方式:translate3d、translateZ
  • opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),

作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)

  • <video><iframe><canvas><webgl>等元素
  • 其它,譬如以前的 flash 插件
absolute和硬件加速的区别

可以看到,absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。
所以,就算absolute中信息改变时不会改变普通文档流中 render 树,
但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。
(浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的)

而硬件加速直接就是在另一个复合层了(另起炉灶),所以它的信息改变不会影响默认复合层
(当然了,内部肯定会影响属于自己的复合层),仅仅是引发最后的合成(输出视图)

复合图层的作用?

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能

但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡

硬件加速时请使用 index

使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染

具体的原理时这样的:

webkit CSS3中,如果这个元素添加了硬件加速,并且index 层级比较低,
那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),
会默认变为复合层渲染,如果处理不当会极大的影响性能

简单点理解,其实可以认为是一个隐式合成的概念:如果 a 是一个复合图层,而且 b 在 a 上面,那么 b 也会被隐式转为一个复合图层,这点需要特别注意

另外,这个问题可以在这个地址看到重现(原作者分析的挺到位的,直接上链接):

CSS3 硬件加速也有坑

从 Event Loop 谈 JS 的运行机制

到此时,已经是属于浏览器页面初次渲染完毕后的事情,JS 引擎的一些运行机制分析。

注意,这里不谈可执行上下文,VO,scope chain等概念(这些完全可以整理成另一篇文章了),这里主要是结合Event Loop来谈 JS 代码是如何执行的。

读这部分的前提是已经知道了JS 引擎是单线程,而且这里会用到上文中的几个概念:(如果不是很理解,可以回头温习)

  • JS 引擎线程
  • 事件触发线程
  • 定时触发器线程

然后再理解一个概念:

  • JS 分同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

看图:

sync1.png

看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,所以自然有误差。

JavaScript 运行机制详解:再谈 Event Loop 666

当然看了microtask就知道task => 渲染 => task这个套路

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自 Philip Roberts 的演讲《Help, I’m stuck in an event-loop》)

loop1.png

one thread == one call stack == one thing at a time,
执行栈就是task, 任务队列就是task queue, 然后进行event loop

也有是 Event Table 和 Event Queue

上图大致描述就是:

  • 主线程运行时会产生执行栈,
  • 栈中的代码调用某些 api(定时, 异步, 事件)时,它们会在事件队列中添加各种事件(当满足触发条件后,如 ajax 请求完毕后才推入事件队列)
  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环

注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

单独说说定时器

上述事件循环机制的核心是:JS 引擎线程和事件触发线程

但事件上,里面还有一些隐藏细节,譬如调用setTimeout后,是如何等待特定时间后才添加到事件队列中的?

是 JS 引擎检测的么?当然不是了。它是由定时器线程控制(因为 JS 引擎自己都忙不过来,根本无暇分身)

为什么要单独的定时器线程?因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

什么时候会用到定时器线程?当使用setTimeout或setInterval时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

譬如:

1
2
3
setTimeout(function(){
console.log('hello!');
}, 1000);

这段代码的作用是当 1000 毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

1
2
3
4
5
setTimeout(function(){
console.log('hello!');
}, 0);

console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:

执行结果是:先begin后hello!
虽然代码的本意是 0 毫秒后就推入事件队列,但是W3C在 HTML 标准中规定,规定要求setTimeout中低于 4ms 的时间间隔算为4ms。
你所不知道的 setTimeout
(不过也有一说是不同浏览器有不同的最小时间设定)

就算不等待 4ms,就算假设 0 毫秒就推入事件队列,也会先执行 begin(因为只有可执行栈内空了后才会主动读取事件队列)

setTimeout 而不是 setInterval

用setTimeout模拟定期计时和直接用setInterval是有区别的。

因为模拟的话, 每次setTimeout计时到后就会去执行,然后执行一段时间后才会继续setTimeout,中间就多了误差
(误差多少与代码执行时间有关)

而setInterval则是每次都精确的隔一段时间推入一个事件
(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)

而且setInterval有一些比较致命的问题就是:

累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,
就会导致定时器代码连续运行好几次,而之间没有间隔。
就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)

譬如像 iOS 的webview,或者 Safari 等浏览器中都有一个特点,在滚动的时候是不执行 JS 的,如果使用了setInterval,会发现在滚动结束后会执行多次由于滚动不执行 JS 积攒回调,如果回调执行时间过长,就会非常容器造成卡顿问题和一些不可知的错误(这一块后续有补充,setInterval自带的优化,不会重复添加回调)
而且把浏览器最小化显示等操作时,setInterval并不是不执行程序,它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行.

所以,鉴于这么多但问题,目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame

补充:JS 高程中有提到,JS 引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。不过,仍然是有很多问题。。。

事件循环进阶:macrotask 与 microtask

强烈推荐有英文基础的同学直接观看原文,作者描述的很清晰,示例也很不错,如下:

Tasks, microtasks, queues and schedules
链接中对tasks, microtasks, JS stack, log有一个动画过程

上文中将 JS 事件循环机制梳理了一遍,在 ES5 的情况是够用了,但是在 ES6 盛行的现在,仍然会遇到一些问题,譬如下面这题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0);

new Promise((res, rej) => res(console.log('pp1'))).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

new Promise((res, rej) => res(console.log('pp2'))).then(function() {
console.log('promise3');
}).then(function() {
console.log('promise4');
});

console.log('script end');

嗯哼,它的正确执行顺序是这样子的:

1
2
3
4
5
6
7
8
9
10
script start
pp1
pp2
script end
promise1
promise2
promise3
promise4

setTimeout

为什么呢?因为Promise里有了一个一个新的概念:microtask

或者,进一步,JS 中分为两种任务类型:macrotask和microtask,在 ECMAScript 中,macrotask可称为task, microtask称为jobs

也就是tasks和microtask

它们的定义?区别?简单点可以按如下理解:

  • macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

    • 每一个task会从头到尾将这个任务执行完毕,不会执行其它
    • 浏览器为了能够使得 JS 内部task与 DOM 任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
      • (task->渲染->task->…)
  • microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务

    • 也就是说,在当前task任务后,下一个task之前,在渲染之前
    • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
    • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

分别怎么样的场景会形成macrotask和microtask呢?

macrotask:script主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask), IO事件, UI交互事件, postMessage, MessageChannel, SetImmediate(nodejs)
microtask:Promise.then,process.nextTick(nodejs), MutationObserver等
补充:在 node 环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

参考:process.nextTick()与 promise.then()
JavaScript 运行机制详解:再谈 Event Loop 666

再根据线程来理解下:

  • macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
  • microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由 JS 引擎线程维护

(这点由自己理解+推测得出,因为它是在主线程下无缝执行的)

所以,总结下运行机制:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)

注意, 如果用到了 await 这个 微队列会提前. 或者可以看成

如图:

tasks.png

再来一个例子, 里面还有 3 个例子
从 event loop 到 async await 来了解事件循环机制 666666

1
2
3
4
5
6
7
8
9
10
11
setTimeout(function() {
console.log('4')
})

new Promise(function(resolve) {
console.log('1') // 同步任务
resolve()
}).then(function() {
console.log('3')
})
console.log('2')
  1. 这段代码作为宏任务,进入主线程。
  2. 先遇到 setTimeout,那么将其回调函数注册后分发到宏任务 Event Queue。
  3. 接下来遇到了 Promise,new Promise 立即执行,then 函数分发到微任务 Event Queue。
  4. 遇到 console.log(),立即执行。
  5. 整体代码 script 作为第一个宏任务执行结束。查看当前有没有可执行的微任务,执行 then 的回调。
    (第一轮事件循环结束了,我们开始第二轮循环。)
  6. 从宏任务 Event Queue 开始。我们发现了宏任务 Event Queue 中 setTimeout 对应的回调函数,立即执行。
    执行结果:1 - 2 - 3 - 4

另外,请注意下Promise的polyfill与官方版本的区别:

  • 官方版本中,是标准的microtask形式
  • polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式
  • 请特别注意这两点区别

注意,有一些浏览器执行结果不一样(因为它们可能把microtask当成macrotask来执行了),
但是为了简单,这里不描述一些不标准的浏览器下的场景(但记住,有些浏览器可能并不标准)

20180126 补充:使用 MutationObserver 实现 microtask

MutationObserver可以用来实现microtask
(它属于microtask,优先级小于Promise,
一般是Promise不支持时才会这样做)

它是 HTML5 中的新特性,作用是:监听一个 DOM 变动,
当 DOM 对象树发生任何变动时,Mutation Observer会得到通知

像以前的 Vue 源码中就是利用它来模拟nextTick的,
具体原理是,创建一个TextNode并监听内容变化,
然后要nextTick的时候去改一下这个节点的文本内容,
如下:(Vue 的源码,未修改)

1
2
3
4
5
6
7
8
9
10
11
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))

observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}

对应 Vue 源码链接

不过,现在的 Vue(2.5+)的 nextTick 实现移除了 MutationObserver 的方式(据说是兼容性原因),
取而代之的是使用 MessageChannel
(当然,默认情况仍然是 Promise,不支持才兼容的)。

MessageChannel 属于宏任务,优先级是:MessageChannel->setTimeout,
所以 Vue(2.5+)内部的 nextTick 与 2.4 及之前的实现是不一样的,需要注意下。

这里不展开,可以看下Vue.js 升级踩坑小记

浏览器内核类型

KDE的开放原始码KHTML引擎在KDE的Konqueror网页浏览器使用,后来成为WebKit的基础,WebKit是Apple Safari、傲游浏览器和早期Google Chrome网页浏览器的渲染引擎,在StatCounter的统计当中是最被广泛使用的浏览器引擎。Chromium/Chrome(iOS 版除外)和Opera目前版本则是以Blink为基础,是WebKit的一个分支。

Mozilla开放原始码专案的网页浏览器引擎Gecko,被Mozilla代码库中的各种产品所使用,其中包括Firefox网页浏览器、Thunderbird 电子邮件客户端和 SeaMonkey 网路套件。Goanna是Gecko的一个分支。

Internet Explorer的网页浏览器引擎Trident,被 Microsoft Windows 平台的许多应用程式如 netSmart、Outlook Express、某些版本的 Microsoft Outlook 和 Winamp、RealPlayer 中的迷你浏览器所使用。Trident已经被EdgeHTML所取代。

Opera软体公司的专有的Presto引擎被授权给其他许多软体供应商,并在Opera浏览器所使用,直到它在 2013 年被Blink取代。

排版引擎

写在最后的话

看到这里,不知道对 JS 的运行机制是不是更加理解了,从头到尾梳理,而不是就某一个碎片化知识应该是会更清晰的吧?

同时,也应该注意到了 JS 根本就没有想象的那么简单,前端的知识也是无穷无尽,层出不穷的概念、N 多易忘的知识点、各式各样的框架、
底层原理方面也是可以无限的往下深挖,然后你就会发现,你知道的太少了。。。

另外,本文也打算先告一段落,其它的,如 JS 词法解析,可执行上下文以及 VO 等概念就不继续在本文中写了,后续可以考虑另开新的文章。

最后,喜欢的话,就请给个赞吧!

输入任意字符到地址栏

第一步是浏览器对用户输入的网址做初步的格式化检查,只有通过检查才会进入下一步。
这里会区分你最后用的是搜索还是去那个网站. 比如输入皮卡丘是搜索, 输入github.com是去 github 网站.

url 的组成

url的组成

DNS 解析(再具体就是 chrome 怎么识别网址, 然后是 DNS 开机怎么来的 DHCP/)

CDN原理

参考

在浏览器地址栏输入一个 URL 后回车,背后会进行哪些技术步骤?
前端经典面试题: 从输入 URL 到页面加载发生了什么?
当···时发生了什么?
what happens when you type in a URL in browser [closed]
当你在浏览器中输入“google.com”并回车,会发生什么?
从输入 cnblogs.com 到博客园首页完全展示发生了什么

浏览器输入 URL 后发生了什么?
从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系!666
从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理 666
mac 下 chrome 浏览器的标签页、进程和内存分配 666
CSS3 硬件加速也有坑
你所不知道的 setTimeout 666
Tasks, microtasks, queues and schedules 666
聊聊 JavaScript 与浏览器的那些事 - 引擎与线程 666
process.nextTick()与 promise.then()
深入浅出浏览器渲染原理

前端文摘:深入解析浏览器的幕后工作原理 666

图解浏览器的基本工作原理

浏览器进程?线程?傻傻分不清楚!有点多
聊聊 JavaScript 与浏览器的那些事 - 引擎与线程 一般般

JavaScript 运行机制详解:再谈 Event Loop 666
一篇文章说清浏览器解析和 CSS(GPU)动画优化 666

预加载系列一:DNS Prefetching 的正确使用姿势
css 加载会造成阻塞吗 666
从 event loop 到 async await 来了解事件循环机制 666666

DHCP原理

发表于 2018-12-27 | 分类于 网络

DHCP原理

DHCP,DNS和HTTP是3种常见的高层协议。

DHCP(Dynamic Host Configuration Protocol),动态主机配置协议,是一个应用层协议。当我们将客户主机ip地址设置为动态获取方式时,DHCP服务器就会根据DHCP协议给客户端分配IP,使得客户机能够利用这个IP上网。

首先client要设置为DHCP获取, server常见就是路由器咯

DHCP的前身是BOOTP协议(Bootstrap Protocol),BOOTP被创建出来为连接到网络中的设备自动分配地址,后来被DHCP取代了,DHCP比BOOTP更加复杂,功能更强大。后面可以看到,在用Wireshark过滤显示DHCP包,需要输入过滤条件BOOTP,而不是DHCP.

dhcp1

DHCP的实现分为4步,分别是:
第一步:Client端在局域网内发起一个DHCP Discover包,目的是想发现能够给它提供IP的DHCP Server。
第二步:可用的DHCP Server接收到Discover包之后,通过发送DHCP Offer包给予Client端应答,意在告诉Client端它可以提供IP地址。
第三步:Client端接收到Offer包之后,发送DHCP Request包请求分配IP。
第四步:DHCP Server发送ACK数据包,确认信息。

加了DHCP relay

dhcp_client_server

wireshark抓包

具体看包再分析下每步


wireshark1

一、发现阶段:客户机寻找DHCP服务器

截图分析:

  1. 客户端不知道自己的IP,以0.0.0.0标识,此时不知道DHCP服务器的IP地址,以255.255.255.255广播地址标识;MAC地址暂时不管.
  2. 其他主机接收到此包,直接丢弃;DHCP服务器搜到后会响应此包,(注可以被多台DHCP服务器接收,所以才有DHCP Request)
  3. 客户机端口为68,DHCP端口为67,为默认端口号;

wireshark2

二、提供阶段:DHCP服务器提供IP地址

  1. 此包从DHCP服务器到客户端路上,客户机并暂时还没有100.100.57.222的IP地址;

  2. DHCP服务器优先基于ARP协议与之通信(单播返回),如果失败,直接提供广播方式发送;


wireshark3

三、请求阶段:客户机请求DHCP服务器之一确认提供的IP地址

若多台DHCP服务器为其提供Offer信息,则客户机只接收第一台DHCP服务器的IP地址,那么第一台DHCP服务器如何知道自己提供的IP地址被接收?其他DHCP服务器如何知道自己提供的IP地址没有被接收呢?(所以要告诉所有的)

  1. 客户机虽然接收到分配的IP地址,但是没有与DHCP服务端进行确认,并不能开始使用;

  2. 这是一个与图1相同的广播形式request的数据包,目的在于与第一个DHCP服务器进行确认,与其他DHCP服务器进行通信,告知其分配的IP地址并未采用,这是如何实现的呢?截图分析:

wireshark3.1

  1. 图3的数据包,相应网络范围内的DHCP服务器均会收到,每台DHCP服务器检查DHCP Sever Identifier字段,如果是本机IP,则确认其分配的即Requested IP Address有效;如果不是本机IP,则其分配的IP地址则无效;

wireshark4

四、确认阶段:DHCP服务器确认IP字段有效

  1. 仅图4中确认IP地址有效的DHCP服务器,返回Ack数据包;

  2. 此数据包包含在本文开头时强调的上网的基本信息,实现动态上网;


故事到这里是否就可以圆满结束了呢?那动态主机配置如何体现其动态过程呢?看官切勿着急,工程师们热爱的是全面的系统,怎么会置之不理呢?

wireshark5

五、重新登录与更新租约

  1. 客户机重新启动后(或关闭WiFi再打开),不再直接发送Discover信息,而是发送Request信息;DHCP服务器会优先尝试,允许其继续使用IP地址,发送Ack数据包;如果该IP不能再使用,返回Nack数据包,客户机重新开始Discover阶段;
  2. DHCP客户机启动时和IP租约期限过一半时,DHCP客户机都会自动向DHCP服务器发送更新其IP租约的信息,与1)中过程相同;

总结

dhcp_client_server2

DHCP协议简析就到这里啦,本次的内容简单实用,重点在于Request阶段的那个具备广播属性的数据包,作用有二,其一是与第一个DHCP服务器确认其IP地址的有效性,其二是与其他DHCP服务器说明其IP地址并未被采用。

包结构分析

参考

Wireshark分析DHCP
DHCP协议原理及其实现流程(更详细)
DHCP协议简析
dhcp 交互流程

CDN原理

发表于 2018-12-26 | 分类于 网络

CDN原理

传统的网站访问过程为:

  • 用户在浏览器中输入要访问的域名;
  • 浏览器向域名解析服务器发出解析请求,获得此域名对应的IP 地址;
  • 浏览器利用所得到的IP 地址,向该IP对应的服务器发出访问请求;
  • 服务器对此响应,将数据回传至用户浏览器端显示出来。

dns.jpg
dns2.jpg

例子🌰:

  1. 在chrome中输入www.qiniu.com
  2. chrome向 DNS server发请求, 解析www.qiniu.com为一个IP地址 (其实就是多了这步)
  3. chrome知道www.qiniu.com的IP地址后, 访问这个地址
  4. 七牛 service 收到请求后, 处理完, 返回源IP地址.

然后有几个问题呗:
从Chrome源码看DNS解析过程

(1)浏览器是怎么知道DNS解析服务器,如上图的8.8.8.8这台?

(2)一个域名可以解析成多个IP地址吗,如果只有一个IP地址,在并发量很大的情况下,那台服务器可能会爆?

(3)把域名绑了host之后,是不是就不用域名解析了直接用的本地host指定的IP地址?

(4)域名解析的有效时间为多长,即过了多久后同一个域名需要再次进行解析?

(5)什么是域名解析的A记录、AAAA记录、CNAME记录?

其实域名解析和Chrome没有直接关系,即使是最简单的curl命令也需要进行域名解析
但是我们可以通过Chrome源码来看一下这个过程是怎么样的,并且回答上面的问题。


首先第一个问题,浏览器是怎么知道DNS解析服务器的,在本机的网络设置里面可以看到当前的DNS服务器IP,如我电脑的:

一句话, DHCP分配IP地址的时候就知道了

localDNS.png

这个DNS Server是某ISP宽带提供的:
DNS100

一般宽带服务商都会提供DNS服务器,谷歌还为公众提供了两个免费的DNS服务,分别为8.8.8.8和8.8.4.4,取这两个IP地址是为了容易记住,当你的DNS服务不好用的时候,可以尝试改成这两个。

DNS8.8

入网的设备(比如你的mac,前面是mac上的chrome)是怎么获取到这些IP地址的呢?是通过动态主机配置协议(DHCP),当一台设备连到路由器之后,路由器通过DHCP给它分配一个IP地址,并告诉它DNS服务器,如下路由器的DHCP设置:

这是个例子, 但不是本机的
router

通过wireshark抓包可以观察到这个过程:
打开wireshark后先断网, 然后再打开网络, 过滤bootp的就是

DHCP

当我的电脑连上wifi的时候,会发一个DHCP Request的广播,路由器收到这个广播后就会向我的电脑分配一个IP地址并告知DNS服务器。

DHCP原理

这个时候系统就有DNS服务器了,Chrome是调res_ninit这个系统函数(Linux)去获取系统的DNS服务器,这个函数是通过读取/etc/resolve.conf这个文件获取DNS:(总之到这里chrome是知道NDS服务器地址的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 1 #
2 # macOS Notice
3 #
4 # This file is not consulted for DNS hostname resolution, address
5 # resolution, or the DNS query routing mechanism used by most
6 # processes on this system.
7 #
8 # To view the DNS configuration used by this system, use:
9 # scutil --dns
10 #
11 # SEE ALSO
12 # dns-sd(1), scutil(8)
13 #
14 # This file is automatically generated.
15 #
# search DHCP HOST
16 nameserver 100.100.61.1
17 nameserver 8.8.8.8

search选项的作用是当一个域名不可解析时,就会尝试在后面添加相应的后缀,如ping hello,无法解析就会分别ping hello.DHCP/hello.HOST,结果最后都无法解析。

Chrome在启动的时候根据不同的操作系统去获取DNS服务器配置,然后把它放到DNSConfig的nameservers:

偏题了, 总之懂DHCP就行


那本地域名服务器是个啥???,路由器给你设的DNS是哪个的??(最终值你的ISP,比如电信, 移动)

第一个问题:本地DNS一般是指你电脑上网时IPv4或者IPv6设置中填写的那个DNS。这个有可能是手工指定的或者是DHCP自动分配的。如果你的电脑是直连运营商网络,一般默认设置情况下DNS为DHCP分配到的运营商的服务器地址。如果你的电脑和运营商之间还加了无线或者有线路由,那极有可能路由器本身还内置了一个DNS转发器,这玩意的作用是将发往他所有的DNS请求转发到上层DNS。此时由于路由器本身也接管了下挂电脑的DHCP服务,所以它分配给下面电脑的DNS地址就是它自身,所以你能看到电脑的DNS分配到的可能是192.168.1.1。实际上就是路由器自身,而路由器的DNS转发器将请求转发到上层ISP的DNS。所以这里说DNS是局域网或者是运营商的都可以(因为最终都是转发到运营商,小细节不用纠结)。

知乎韩晓答案

再说下DNS的递归和迭代查询:
(1)递归查询
递归查询是一种DNS 服务器的查询模式,在该模式下DNS 服务器接收到客户机请求,必须使用一个准确的查询结果回复客户机。如果DNS 服务器本地没有存储查询DNS 信息,那么该服务器会询问其他服务器,并将返回的查询结果提交给客户机。
(2)迭代查询
DNS 服务器另外一种查询方式为迭代查询,DNS 服务器会向客户机提供其他能够解析查询请求的DNS 服务器地址,当客户机发送查询请求时,DNS 服务器并不直接回复查询结果,而是告诉客户机另一台DNS 服务器地址,客户机再向这台DNS 服务器提交请求,依次循环直到返回查询的结果
为止。
DNS递归查询与迭代查询
【基础服务】简单理解DNS的递归、迭代查询 - DNS(一


与传统访问方式不同,CDN 网络则是在用户和服务器之间增加 Cache 层,将用户的访问请求引导到 Cache 节点而不是服务器源站点,要实现这一目的,主要是通过接管DNS 实现。

使用CDN 缓存后的网站访问过程演变为:

CDN.jpg

  1. 用户在浏览器中输入要访问的域名;
  2. 浏览器向域名解析服务器发出解析请求,由于CDN 对域名解析过程进行了调整,所以用户端一般得到的是该域名对应的 CNAME 记录,此时浏览器需要再次对获得的 CNAME 域名进行解析才能得到缓存服务器实际的IP 地址。(这里有2次)
    1. 注:在此过程中 得到CNAME后,指向的是全局负载均衡DNS解析服务器(第二次解析得到这个服务器IP地址),然后全局负载均衡DNS 解析服务器会根据用户端的源IP 地址,如地理位置(北京还是上海)、接入网类型(电信还是网通)将用户的访问请求定位到离用户路由最短、位置最近、负载最轻的Cache 节点(缓存服务器)上,实现就近定位。定位优先原则可按位置、可按路由、也可按负载等。这种技术也被称为DNS 重定向
  3. 再次解析后浏览器得到该域名CDN 缓存服务器的实际IP 地址,向缓存服务器发出访问请求;
  4. 缓存服务器根据浏览器提供的域名,通过Cache 内部专用DNS 解析得到此域名源服务器的真实IP 地址,再由缓存服务器向此真实IP 地址提交访问请求;
  5. 缓存服务器从真实IP 地址得到内容后,一方面在本地进行保存,以备以后使用,另一方面把得到的数据发送到客户端浏览器,完成访问的响应过程;
  6. 用户端得到由缓存服务器传回的数据后显示出来,至此完成整个域名访问过程。

通过以上分析可以看到,不论是否使用CDN 网络,普通用户客户端设置不需做任何改变,直接使用被加速网站原有域名访问即可。对于要加速的网站,只需修改整个访问过程中的域名解析部分,便能实现透明的网络加速服务。

CDN2.jpg

CDN客户使用CDN的方法:
对于CDN客户来说,不需要改动网站架构,只需要修改自己的DNS解析,设置一个CNAME指向CDN服务商即可。原理在下面会解释

通过上图,我们可以了解到,使用了CDN缓存后的网站的访问过程变为:

  • 用户向浏览器提供要访问的域名;
  • 浏览器调用域名解析库对域名进行解析,由于CDN对域名解析过程进行了调整,所以解析函数库得到的是该域名对应的CNAME记录(由于现在已经是使用了CDN服务,CNAME为CDN服务商域名),为了得到实际IP地址,浏览器需要再次对获得的CNAME域名进行解析以得到实际的IP地址;在此过程中,使用的全局负载均衡DNS解析,如根据地理位置信息解析对应的IP地址,使得用户能就近访问。(CDN服务来提供最近的机器)
  • 此次解析得到CDN缓存服务器的IP地址,浏览器在得到实际的IP地址以后,向缓存服务器发出访问请求;
  • 缓存服务器根据浏览器提供的要访问的域名,通过Cache内部专用DNS解析得到此域名的实际IP地址,再由缓存服务器向此实际IP地址提交访问请求;
  • 缓存服务器从实际IP地址得得到内容以后,一方面在本地进行保存,以备以后使用,二方面把获取的数据返回给客户端,完成数据服务过程;
  • 客户端得到由缓存服务器返回的数据以后显示出来并完成整个浏览的数据请求过程。

除了CND外有啥技术呢,就是是如何进行调度和进行定位的?

3种: DNS 调度、HTTP 302 调度,还有一种使用 HTTP 进行的 DNS 调度策略。

DNS 调度

肯定很多人好奇是如何进行调度和进行定位的?其实也是通过 LDNS(local DNS) 的具体地址来进行的,如上图所示。

假设网民是一个北京客户,那他所使用的 DNS 服务器去做递归的时会访问到CDN厂商的 GLB(Global Load Balance),它可以看到所访问的域名请求是来自于哪个 LDNS,根据一般人的使用习惯,网民所在位置和 LDNS 所在位置是一样的,因此 GLB 可以间接知道网民来自什么位置。

以上图为例,假如网民是一个北京联通的用户,它使用的 LDNS 地址也是北京联通的,而 LDNS 访问 GLB 也是北京联通的,则 GLB 则认为网民的位置在北京联通,那么会分配一个北京联通的 CDN 服务器地址给 LDNS,LDNS 将http:www.a.com解析出的 IP 地址返回给最终网民,那么在以后网民浏览器发起请求的时候,都会直接与北京联通的 CDN 节点进行流量通信,从而达到了加速的目的。

从这个调度理论上看,我们可以不难发现一个问题,就是重点标注出的“根据一般人的使用习惯”。假设网民所使用的 LDNS 地址和他自己在同一个区域,调度才有可能是准确的(后续篇章会重点描述为什么是“有可能”)。

但是举个例子来说,如果网民是北京联通的用户,但他却偏要使用深圳电信的 LDNS,LDNS 出口也同样是深圳电信的 IP 地址,那么 GLB 会误判网民位于深圳电信,分配给网民的 CDN 服务器也都是深圳电信的,后续网民会从北京联通访问到深圳电信,不但没加速,可能反而降速了。

HTTP 302 调度

如前文所述,由于用户使用习惯或一些其他原因,通过 LDNS 调度有可能是不准确的,因此又出现了另一种调度方式,HTTP 302 调度。

原理很简单,无论网民最初拿到的 IP 地址是否是正确的,但最终都是要和这个 IP 地址的 CDN 服务器通信的,因此 CDN 服务器可以在这时知道网民的真实地址(DNS 调度时只能间接知道网民地址,虽然 EDNS-Client-Subnet 技术可以解决问题,但尚未大规模使用)。

HTTP 协议中有一个特殊的返回状态:302。在 HTTP 服务器返回 302 状态码时,可以携带一个新的 URL(使用的是正确 IP),浏览器在拿到 302 返回状态码时,会提取其中新的 URL 地址发起请求,这样就可以做到重新调度了。

使用 HTTP 进行的 DNS 调度策略

那 CDN 是如何将用户的流量引入到 CDN 网络中的呢?重点诶 怎么接管的

在未做 CDN 时,我们访问某个域名,直接拿到的是一个真实的服务器 IP 地址,这个显示 IP 地址的 DNS 记录信息叫 A记录
当业务需要接入到 CDN 时,用户只需调整自己的 DNS 配置信息,将 A 记录改为 CNAME 记录,将内容改为 CDN 厂商所提供的接入域名即可。

cache服务器

参考

CDN加速原理
深度剖析:CDN内容分发网络技术原理
CDN的基本原理和基础架构
图解基于 HTTPS 的 DNS, 安全性
CDN工作原理
《CDN 之我见》系列一:原理篇(由来、调度)666
《CDN 之我见》系列二:原理篇(缓存、安全)
《CDN 之我见》系列三:详解篇(网络优化)

go简单入门

发表于 2018-12-20 | 分类于 go教程

go简单入门

go简介

go4个好处:

  1. 不依赖别的库, 拿来就可以跑.
  2. 静态语言, 又有动态语言的特性.
  3. 并发
  4. GC

用在: 服务器, 分布式, 云平台, 网络平台, 内存数据库cache

语法

变量声明/常量声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 变量
var a int = 5
var (
a int = 5
b float64 = 9.9
)
// 多一个:=
a := 5
a, b : = 4, 6

// 常量
const b int = 6
const {
a int = 5
b float64 = 8.98
}

匿名变量_: 用来丢弃不处理, 常用在处理函数返回值

常量自动生成iota: 4点

  1. 遇到const自动重新开始0
  2. 可以只写一行iota, 下面的行默认加上去
  3. 以行为单位递增

go的常用变量类型
bool int32 float64
byte string 类比c 一个是单个字符, 一个是字符串, 多一个\0 单双引号

%T
%c %s %d %f %t
%v

‘a’这种也就是整型 int32

输入输出使用

1
2
3
4
fmt.Printf()
fmt.Println()
fmt.Scanf()
fmt.Scan()

int() 强转, 注意不兼容类型 int 和 bool go的bool和int不兼容啊

byte int

类型别名, type来定义下

1
2
3
4
5
type bigint int64
type (
long int 64
char byte
)

操作符同c javascript 一样啦 没有 ===

流程控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 没括号
if s { // 不能换行哦

}

if a := 10; a == 10 { // 支持一个初始化语句, 按分号隔开

}

// 支持一个初始化语句, 同if 用分号隔开
switch num {
case 1 :
fmt
case 2:
break // fallthrough 是不跳出switch 接着往下执行
default:
}

switch后面也可以不加条件,只初始化, 加在case中判断
score: 85
switch {
case score > 90 :
fmt
}

for 初始条件; 判断条件; 条件变化 同c 就是少个括号()

range 迭代

1
2
3
4
5
6
7
// 返回一个元素位置和一个元素本身
for i, data := range str {
fmt()
}
for i := range str { // 只要下标
fmt()
}

go的不定参数, (a …int) 而(a…)是展开,
更高级的是指定从哪个下标开始到结束用[)

比如 (a[2:]...) 从下标2开始包括2到最后
比如 (a[:2]...) 到下标为2不包括2

1
2
3
func a()(res int) {     // 定义了返回的类型
return // 这里只写return表示返回前面的定义的格式吗但建议还是写全好了
}

函数名首字母是大写表示私有, 大写表示公有的函数.

函数类型

1
type FFF func(int, int) int     // 输入2个, 输出1个

格式对好就行, 然后相同参数返回值的函数, 到时候调用. 先声明赋值后, 直接就可以像原来的函数调用一样. 更具有普遍性.

t=add t(1,2)

涉及多态, 就是不写死. 比如sort()

闭包, 匿名函数, 立即调用
func () {}

闭包以引用的方式捕获外部变量,

传统的局部变量是调用时才分配空间,然后调用完就回收了

defer 延时调用,在调用完前处理东西,只能用在函数内部。
类似异步后放最后用,然后多个defer是按照后进先出,的顺序的!!就是入栈了先!

然后defer是肯定能执行,发生错误也能。就是栈。

defer func() 这种个匿名函数一起用。

注意传入的参数是那个时候的。


命令行参数,用导入os的包import 'os',然后os.Args 。字符数组。

局部变量和全局变量
在{}中就是局部,块

工作区src GOPATH

1
2
3
4
import (
"fmt"
"os"
)

import . "fmt" 不建议,这个调用就不用包名了, 直接用比如println

别名 import lll "fmt"

忽略包 import _ "fmt" 一般使用这个包的init函数

工程文件下要有src ,GOPATH就是工程的路径,不进去src
同一个路径下package <nane>这个<name>要一样。同一个目录下调用别的文件的,不需导入包名,直接调用好了。

不同目录包名不一样,同fmt一样导入好了,然后使用,但要注意,函数名要大写开头。私有公有的问题。

init函数,会在那个包的所有函数之前运行,执行一次好了,就是每个包运行前会执行init
所以那个import _ "fmt"只是用来执行init的

GOBIN设置后用go install生成pkg bin文件夹,一个放依赖,一个命令可执行程序。


复合结构

point array slice map struct

指针point不支持->这种算法
go中的是nil,不是null

  • & 这种和c语言一样(只是没有->)

new(int)分一个int大小的空间, 操作内存.

数组, 同一个类型的, 要固定长(常量 ), 不能变, 同c len() 初始化不同 {}

1
var a [12]int

数组声明同时赋值

1
2
3
4
5
6
var a [5]int = [5]int{1, 2, 3, 4, 5}

// 部分
c := [5]int{1,2,3}
// 指定下标
d := [5]int{2:10, 4:20}

二维数组(了解), 看有多少个[] 多少维 多少循环

同一维 是用来初始化, 和c不同

1
2
// 表示下标1的行那4个
e := [3][4]int{1: {5,6,7,8}}

数组比较== 和 != 比较的事每个数组元素是不是一样


随机数

  1. 设置种子 rand.Seed(666) 如果种子一样, 每次的随机数就一样, 所以用时间来做随机种子time.Now().UnixNano()
  2. 产生随机数 rand.Int() rand.Intn(100)限制在100内

数组做函数参数, 是值传递, 不是引用哦, 相当于copy一份, 注意哦. 当然可以用指针的方式去

1
2
3
4
// 按值
func a(p [5]int)
// 按引用
func a(p *[5]int)

slice 弥补数组缺点, 通过内部指针和相关属性引用数组片段, 以实现变长方案.

1
2
3
4
5
6
array := [...]int{10, 20, 30, 0, 0}
// 起始指针位置low, 终止位置high, 切完后要的总容量max
slice := array[0:3:5] //[low: high: max]
[) 起点,终点, 长度
len长度是high - low
cap容量是max - low

切片和数组的区别: 数组的len是固定常量, 不能修改. 切片 方括号里面是空或…

1
2
3
4
[6]int{}

[]int{}
[...]int{}

切片创建
// 自动推导

// 用make

1
2
3
4
5
6
// 类型, 长度, 容量
s2 := make([]int, 5, 10)
// len和max一样
s3 := make([]int, 5)

len() cap()

截取:

1
2
3
4
5
6
[low:high:max]
就是len是high-low
cap是max-low


[:6] // 长度是6 但容量是原来的数组容量, 不是len = max

切片会改变原来的数组

append在slice末尾加一个元素, 会扩容的, 超过cap会以2倍扩容.

copy

1
2
copy(dst, src)
是替换过去覆盖相应位置元素

当函数参数的话, slice是引用传递, 不是值传递. 不用加&, 直接变量传过去.

map就是无序key-value

1
2
3
4
// map[keyType] valueType  只有len没有cap, 用make的话那个是指定容量了, 还会扩容
map[int]string {
110: 'mike'
}

react-router-v4

发表于 2018-12-16 | 分类于 react教程

react-router-v4

app.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
import React, { Component } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Home from './components/home'
import About from './components/About'
import Contact from './components/Contact'
import Error from './components/Error'
import Navigation from './components/Navigation'

class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Navigation />
<Switch>
<Route path="/" component={Home} exact />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route component={Error} />
</Switch>
</div>
</BrowserRouter>
);
}
}

export default App;

Navigation.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react'
import { NavLink } from 'react-router-dom'

const Navigation = () => {
return (
<div>
<NavLink to="/">Home</NavLink>
<NavLink to="/about">About</NavLink>
<NavLink to="/contact">Contact</NavLink>
</div>
)
}

export default Navigation

基本组件 3种

  • router components
  • route matching components
  • navigation components
1
import { BrowserRouter, Route, Link } from "react-router-dom";

router components 2种

有2种<BrowserRouter> 和 <HashRouter> routers. 都会创建一个history对象
通常 <BrowserRouter>用在 a server that responds to requests and a <HashRouter>用在static file server.

Route Matching 2种

<Route> and <Switch>.

<Route>
就是比较 when location = { pathname: '/about' } 和 <Route path='/about' component={About}/>中的path='/about', 每个<Route />都会比较, 最小匹配, 匹配上的render, 没匹配上的render null, 没有写path的会始终匹配.

<Switch>
只会递归的匹配第一个非exact匹配上的(注意嵌套模式), 而没写path的<Route />当做兜底的情况.

exact是用来完全匹配的path

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Route, Switch } from "react-router-dom";
// when location = { pathname: '/about' }
<Route path='/about' component={About}/> // renders <About/>
<Route path='/contact' component={Contact}/> // renders null
<Route component={Always}/> // renders <Always/>

<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
{/* when none of the above match, <NoMatch> will be rendered */}
<Route component={NoMatch} />
</Switch>

Route Rendering Props

<Route>有3个props: component, render, and children.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Home = () => <div>Home</div>;
const App = () => {
const someVariable = true;
return (
<Switch>
{/* these are good */}
<Route exact path="/" component={Home} />
<Route
path="/about"
render={props => <About {...props} extra={someVariable} />}
/>
{/* do not do this 就render和component*/}
<Route
path="/contact"
component={props => <Contact {...props} extra={someVariable} />}
/>
</Switch>
);
};

Navigation

用<Link>, 然后会render成<a>

<NavLink> 是一种特殊的 <Link> 当match上location的的时候会加上activeClassName 就是class

强制navigation的话用<Redirect>

1
2
3
4
5
6
7
8
9
10
<Link to="/">Home</Link>
// <a href='/'>Home</a>

// location = { pathname: '/react' }
<NavLink to="/react" activeClassName="hurray">
React
</NavLink>
// <a href='/react' className='hurray'>React</a>

<Redirect to="/login" />

服务端的

参考

React Router tutorial for beginners | React Router v4 2018
React Router v4 Tutorial+quick Start

1…678…14
Henry x

Henry x

this is description

133 日志
25 分类
135 标签
GitHub E-Mail
Links
  • weibo
© 2019 Henry x