前端知识总结

1、HTML

块级元素与行内元素

平时使用的标签主要分为两种:块级标签与行内标签,主要区别如下:

块级标签行内元素
独占一行(会自动换行)和其他行内元素在同一排显示
可以直接控制宽度、高度以及盒子模型的相关css属性不能直接控制宽度、高度以及盒子模型的相关css属性,但是直接设置内外边距的左右值是可以的
在不设置宽度的情况下,块级元素的宽度是它父级元素内容的宽度行内元素的宽高是由本身内容的大小决定(文字、图片等)
在不设置高度的情况下,块级元素的高度是它本身内容的高度行内元素只能容纳文本或者其他内联元素(此处请注意,不要在内联元素中嵌套块级元素)
display:blockdisplay:inline
常见块级元素:div、h1、hr、p常见的行内元素:a、img、input、span

2、CSS

2.1、盒子模型

所有HTML元素可以看作盒子,在CSS中,"box model"这一术语是用来设计和布局时使用。
CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:外边距,边框,内边距,和实际内容。
在这里插入图片描述
一般有两种盒模型:content-box 标准盒模型、border-box 怪异盒模型(实际上padding-box,margin-box的概念提出过,但未被实现)
盒模型主要影响宽高(width、height)属性的表现,content-box下,宽高等于内容的宽高,border-box下,宽高等于border+padding+content的宽高。在这里插入图片描述
在这里插入图片描述

2.2、定位position

static
absolute
relative
fixed
sticky

2.3、布局display

none
block
inline
inline-block
flex
grid

2.4、浮动

float

2.5、七阶层叠水平

在这里插入图片描述
在这里插入图片描述

2.6、CSS样式优先级(权重)
2.6.1、样式优先级规则

根据权重值排序,应用权重值第一的样式;
如果权重值相同,则应用最后定义的样式(应避免这种情况,若出现问题,溯源的时候比较麻烦)。

2.6.2、权重规则

权重分为四个等级(有一个重要级角色不在此列)
• 第一等:内联样式,权重1,0,0,0,即标签内的style属性设置的样式
• 第二等:ID选择器,权重0,1,0,0,例如#id{…}
• 第三等:类选择器,伪类选择器,属性选择器,权重0,0,1,0,例 如.class{…}、:hover{…}、[arrtibute=value]
• 第四等:标签选择器,伪元素选择器,权重0,0,0,1,例如div{…}、::after{…}
• 超然地位:!important(只要我出现,不好意思,权重就是无限,优先考虑,别的靠边站)
• 继承的样式没有权值。

PS:还有几个权重为0,不计入排名,他们就是通配选择器(*),子选择器(>),相邻同胞选择器(+),即使是0,也比继承的样式高。
根据样式根据以上规则,按照选择器累加计算权重,例如
#my-id .my-class div p{…}

对于p标签的来说,这个样式的权重就是100+10+1+1=112,如果p还有别的样式,只要小于(严格小于)112,就使用这个样式,别的样式无效。

3、JS

3.1、原型和原型链

原型的引用类似于java中的继承。JavaScripts中每个实例对象( object )都有一个私有属性(称之为 -proto- 指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( -proto- ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

3.1.2、基于原型链的属性继承

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
测试代码如下:

// 让我们从一个自身拥有属性a和b的函数里创建一个对象o:
let f = function () {
   this.a = 1;
   this.b = 2;
}
/* 这么写也一样
function f() {
  this.a = 1;
  this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}
// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;
// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
//  (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。
// 综上,整个原型链如下: 
// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1
console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"
console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4
console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined
3.1.2、基于原型链的方法继承

JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

var o = {
  a: 2,
  m: function(){
    return this.a + 1;
  }
};
console.log(o.m()); // 3
// 当调用 o.m 时,'this' 指向了 o.
var p = Object.create(o);
// p是一个继承自 o 的对象
p.a = 4; // 创建 p 的自身属性 'a'
console.log(p.m()); // 5
// 调用 p.m 时,'this' 指向了 p
// 又因为 p 继承了 o 的 m 函数
// 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a'
3.2、new运算符

new 运算符主要做了下面几件事
• 创建一个空对象{}
• 将这个空对象的__proto__成员指向了调用函数对象prototype成员对象
• 将函数对象的this指针替换成obj,然后再调用函数
类似下面的代码

var clazz = new Class();
=========================
var clazz = {};
clazz.__proto__ = Class.prototype;
Class.call(obj);
3.3、this

详细请参考这篇博客

  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定
  4. new绑定
function foo() { 
    console.log( this.a );
}

var a = 2; 
// 1.默认绑定
// 这种直接使用而不带任何修饰的函数调用 ,就 默认且只能 应用 默认绑定
foo();

var obj = { 
    a: 3,
    foo: foo 
};
// 隐性绑定,函数foo执行的时候有了上下文对象,即 obj。
// 这种情况下,函数里的this默认绑定为上下文对象,即obj.a
obj.foo();

var obj2 = { 
    a: 4,
    obj: obj
};
// 链式隐性绑定
obj2.obj.foo();

// 隐式丢失
var bar = obj.foo;
bar();
// 隐式丢失
setTimeout( obj.foo, 100 );

// 显示绑定
foo.call( obj ); 
foo.call( obj2 );

var bar = function(){
    foo.call( obj );
}
setTimeout( bar, 100 ); // 3
bar.call( obj2 );


function foo(a) { 
    this.a = a;
}

var a = 2;
// new 绑定
var bar1 = new foo(3);
console.log(bar1.a); // ?

var bar2 = new foo(4);
console.log(bar2.a);

绑定规则优先级
优先级是这样的,按照下面的顺序来进行判断:
变量是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
变量是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
变量是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。

// 1. 正常调用
var foo = () => {   
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); //2
foo.call(obj); //2 ,箭头函数中显示绑定不会生效

// 2. 函数回调
function foo(){ 
    return () => {
        console.log( this.a );
    }   
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //3
3.4、call、apply、bind
  • 在JavaScript中,call、apply和bind是Function对象自带的三个方法,都是为 了改变函数体内部 this 的指向。
  • apply 、 call 、bind 三者第一个参数都是 this 要指向的对象,也就是想指定的上下文;
  • apply 、 call 、bind 三者都可以利用后续参数传参;
  • bind 是返回对应 函数,便于稍后调用;apply 、call 则是立即调用 。
function foo(a,b){
    console.log(a+b);
}
foo.call(null,'海洋','饼干');
foo.apply(null, ['海洋','饼干'] );

function foo(){
    console.log(this.a);
}
var obj = { a : 10 };

foo = foo.bind(obj);
foo(); 
3.4.1、call语法
  • fun.call(thisArg, arg1, arg2, …)
  • thisArg: 在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。
  • arg1, arg2, … 指定的参数列表
3.4.2、apply语法
  • fun.apply(thisArg, [argsArray])
  • thisArg 在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。
3.4.3、bind语法
  • fun.bind(thisArg[, arg1[, arg2[, …]]])
  • thisArg 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。
  • arg1, arg2, … 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
3.5、typeof、instanceof、Object.prototype.call
3.5.1、typeof语法

typeof操作符返回一个字符串,表示未经计算的操作数的类型。

下表总结了typeof可能的返回值。有关类型和原始值的更多信息,可查看 JavaScript数据结构 页面。

类型结果
Undefined“undefined”
Null“object”(见下文)
Number“number”
Boolean“boolean”
String“string”
Symbol (ECMAScript 6 新增)“symbol”
宿主对象(由JS环境提供)Implementation-dependent
函数对象([[Call]] 在ECMA-262条款中实现了)“function”
任何其他对象“object”

JavaScript的值是由一个类型标签和实际值表示的,类型标签占3位,比如: 000就表示object。JavaScript中的null表示机器码中的NULL(空指针),而在大多数平台下NULL是0x00(十六进制),所以null是00000000,标签位也是000,故 typeof null === “object”。

// Numbers
typeof 37 === 'number';
typeof 3.14 === 'number';
typeof Math.LN2 === 'number';
typeof Infinity === 'number';
typeof NaN === 'number'; // 尽管NaN是"Not-A-Number"的缩写,意思是"不是一个数字"
typeof Number(1) === 'number'; // 不要这样使用!

// Strings
typeof "" === 'string';
typeof "bla" === 'string';
typeof (typeof 1) === 'string'; // typeof返回的肯定是一个字符串
typeof String("abc") === 'string'; // 不要这样使用!

// Booleans
typeof true === 'boolean';
typeof false === 'boolean';
typeof Boolean(true) === 'boolean'; // 不要这样使用!

// Symbols
typeof Symbol() === 'symbol';
typeof Symbol('foo') === 'symbol';
typeof Symbol.iterator === 'symbol';

// Undefined
typeof undefined === 'undefined';
typeof blabla === 'undefined'; // 一个未定义的变量,或者一个定义了却未赋初值的变量

// Objects
typeof {a:1} === 'object';
// 使用Array.isArray或者Object.prototype.toString.call方法可以从基本的对象中区分出数组类型
typeof [1, 2, 4] === 'object';
typeof new Date() === 'object';
// 下面的容易令人迷惑,不要这样使用!
typeof new Boolean(true) === 'object';
typeof new Number(1) === 'object';
typeof new String("abc") === 'object';
// 从JavaScript一开始出现就是这样的 
typeof null === 'object';
// 正则表达式
typeof /s/ === 'object'; // Chrome 12+ , 符合 ECMAScript 5.1
typeof /s/ === 'object'; // Firefox 5+ , 符合 ECMAScript 5.1

// 函数
typeof function(){} === 'function';
typeof Math.sin === 'function';
typeof /s/ === 'function'; // Chrome 1-12 , 不符合 ECMAScript 5.1
3.5.2、instanceof语法

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置。

// 定义构造函数
function C(){} 
function D(){} 

var o = new C();


o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype


o instanceof D; // false,因为 D.prototype不在o的原型链上

o instanceof Object; // true,因为Object.prototype.isPrototypeOf(o)返回true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype指向了一个空对象,这个空对象不在o的原型链上.

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为C.prototype现在在o3的原型链上
  • 需要注意的是,如果表达式 obj instanceof Foo 返回true,则并不意味着该表达式会永远返回true,因为Foo.prototype属性的值有可能会改变,改变之后的值很有可能不存在于obj的原型链上,这时原表达式的值就会成为false。另外一种情况下,原表达式的值也会改变,就是改变对象obj的原型链的情况,虽然在目前的ES规范中,我们只能读取对象的原型而不能改变它,但借助于非标准的__proto__伪属性,是可以实现的。比如执行obj.proto = {}之后,obj instanceof Foo就会返回false了。
  • 在浏览器中,我们的脚本可能需要在多个窗口之间进行交互。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。这可能会引发一些问题。比如,表达式 [] instanceof window.frames[0].Array 会返回false,因为 Array.prototype !== window.frames[0].Array.prototype,并且数组从前者继承。
    起初,你会认为这样并没有意义,但是当你在你的脚本中开始处理多个frame或多个window以及通过函数将对象从一个窗口传到另一个窗口时,这就是一个有效而强大的话题。比如,实际上你可以通过使用 Array.isArray(myObj) 或者Object.prototype.toString.call(myObj) === "[object Array]"来安全的检测传过来的对象是否是一个数组。
  • 比如检测一个Nodes在另一个窗口中是不是SVGElement,你可以使用myNode instanceof myNode.ownerDocument.defaultView.SVGElement
    下面的代码使用了instanceof来证明:String和Date对象同时也属于Object类型(他们是由Object类派生出来的)。
  • 但是,使用对象文字符号创建的对象在这里是一个例外:虽然原型未定义,但instanceof Object返回true。
var simpleStr = "This is a simple string"; 
var myString  = new String();
var newStr    = new String("String created with constructor");
var myDate    = new Date();
var myObj     = {};
var myNonObj  = Object.create(null);
simpleStr instanceof String; // 返回 false, 检查原型链会找到 undefined
myString  instanceof String; // 返回 true
newStr    instanceof String; // 返回 true
myString  instanceof Object; // 返回 true
myObj instanceof Object;    // 返回 true, 尽管原型没有定义
({})  instanceof Object;    // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一种创建对象的方法,这种方法创建的对象不是Object的一个实例
myString instanceof Date; //返回 false
myDate instanceof Date;     // 返回 true
myDate instanceof Object;   // 返回 true
myDate instanceof String;   // 返回 false

演示mycar属于Car类型的同时又属于Object类型
下面的代码创建了一个类型Car,以及该类型的对象实例mycar. instanceof运算符表明了这个mycar对象既属于Car类型,又属于Object类型。

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var mycar = new Car("Honda", "Accord", 1998);
var a = mycar instanceof Car;    // 返回 true
var b = mycar instanceof Object; // 返回 true
3.5.3、Object.prototype.call语法

使用typeof无法区分null、array、date
instanceof无法运用在值类型区分
没事,我们还有一种方法,Object.prototype.toString.call()
Object.prototype.toString.call()是一种比较常用,也是比较有效的方法

在这里插入代码片
3.6、IIFE
3.6.1、 定义
  • IIFE(Immediately Invoked Function Expression)是一种高级用法,考虑这样一种场景,你和几十个小伙伴开发一个大型软件,而你们在使用变量名时往往会用到重复的变量名,而js是可以重复声明变量的,噩梦来了,你的代码可能会在各种场合发生莫名其妙的错误,为了解决这一点,我们可以使用IIFE。
    IIFE的出现是为了弥补JS在scope方面的缺陷:JS只有全局作用域(global scope)、函数作用域(function scope),从ES6开始才有块级作用域(block scope)。对比现在流行的其他面向对象的语言可以看出,JS在访问控制这方面是多么的脆弱!那么如何实现作用域的隔离呢?在JS中,只有function,只有function,只有function才能实现作用域隔离,因此如果要将一段代码中的变量、函数等的定义隔离出来,只能将这段代码封装到一个函数中。
  • 在我们通常的理解中,将代码封装到函数中的目的是为了复用。在JS中,当然声明函数的目的在大多数情况下也是为了复用,但是JS迫于作用域控制手段的贫乏,我们也经常看到只使用一次的函数:这通常的目的是为了隔离作用域了!既然只使用一次,那么立即执行好了!既然只使用一次,函数的名字也省掉了!这就是IIFE的由来。
3.6.2、常见形式

根据最后表示函数执行的一对()位置的不同,常见的IIFE写法有两种,示例如下:
列表1:IIFE写法一

(function foo(){
  var a = 10;
  console.log(a);
})();

列表2:IIFE写法二

(function foo(){
  var a = 10;
  console.log(a);
}());

这两种写法效果完全一样,使用哪种写法取决于你的风格,貌似第一种写法比较常见。
其实,IIFE不限于()的表现形式[1],但是还是遵守约定俗成的习惯比较好。

3.6.3、实战

如果看过JQuery源码的话,就可以看到JQuery也是使用IIFE来实现变量隔离的。

// 非IIFE
for(var i=0; i<10; i++) {
	setTimeout(()=>{console.log(i);});
}

// IIFE
for(var i=0; i<10; i++) {
	(function(i){
    setTimeout(()=>{console.log(i);})
  })(i);
}
3.7、闭包

简单地理解,闭包就是函数内部的函数,他能访问到所有外部函数里的所有变量。
你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的的表达式(通常是函数)。
既然嵌套函数是一个闭包,就意味着一个嵌套函数可以"继承"容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。
可以总结如下:
• 内部函数只可以在外部函数中访问。
• 内部函数形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。

function outer() {
     var  a = '变量1';
     var  inner = function () {
       console.info(a);
     }
    return inner;    // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}

由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数。

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
fn_inside = outside(3); // Think of it like: give me a function that adds 3 to whatever you give it
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8

使用闭包,你甚至能模仿Java中的类:

var createPet = function(name) {
  var sex;
  
  return {
    setName: function(newName) {
      name = newName;
    },
    
    getName: function() {
      return name;
    },
    
    getSex: function() {
      return sex;
    },
    
    setSex: function(newSex) {
      if(typeof newSex == "string" 
        && (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
        sex = newSex;
      }
    }
  }
}

var pet = createPet("Vivie");
pet.getName();                  // Vivie

pet.setName("Oliver");
pet.setSex("male");
pet.getSex();                   // male
pet.getName();                  // Oliver
3.8、函数、变量提升
3.9、时间捕获与时间冒泡

事件捕获与事件冒泡

事件冒泡
事件冒泡是由IE开发团队提出来的,即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播。
事件捕获
事件捕获是由Netscape Communicator团队提出来的,是先由最上一级的节点先接收事件,然后向下传播到具体的节点。

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Event Bubbling Example</title>
    </head>
    <style type="text/css">
        #child {
            width: 100px;
            height: 100px;
            background-color: #FF0000;
        }
    </style>
    <body>
        <div id="parent">
            <div id="child"></div>
        </div>
    </body>
    <script type="text/javascript">
        var parent = document.getElementById("parent");
        var child = document.getElementById("child");
        child.onclick = function(event) {
            alert("child");
        };
        document.body.addEventListener("click", function(event) {
            alert("body:event bubble");
        }, false);
        parent.addEventListener("click", function(event) {
            alert("parent:event bubble");
        }, false);
        document.body.addEventListener("click", function(event) {
            alert("body:event catch");
        }, true);
        parent.addEventListener("click", function(event) {
            alert("parent:event catch");
        }, true);
    </script>
</html>

输出顺序:body:event catch—>parent:event catch—>child—>parent:event bubble—>body:event bubble
实际发生的情况如下图,DOM事件流一般分成三个阶段,捕获阶段–>目标阶段–>冒泡阶段。
在这里插入图片描述

3.10、promise
3.11、时间循环

JS从诞生之日起,就是一门单线程的非阻塞的脚本语言。这是由其最初的用途所决定的:与浏览器交互。基本上所有语言和UI相关的部分都是单线程的,比如安卓的UI线程绘制。如果是多线程的话,两个线程同时操作一个元素的属性,就会造成后续一系列问题。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
js引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。
消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。
实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
事件循环用代码表示大概是这样的:

while(true) {
    var message = queue.get();
    execute(message);
}

那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:
消息就是注册异步任务时添加的回调函数。
再次以异步AJAX为例,假设存在如下的代码:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是响应:', resp);
});
// 其他代码
...
...
...

主线程在发起AJAX请求后,会继续执行其他代码。AJAX线程负责请求segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构造一条消息:

// 消息队列中的消息就长这个样子
var message = function () {
    callbackFn(response);
}

其中的callbackFn就是前面代码中得到成功响应时的回调函数。
主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX线程在收到HTTP响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。
用图表示这个过程就是:
在这里插入图片描述
从上文中我们也可以得到这样一个明显的结论,就是:
异步过程的回调函数,一定不在当前这一轮事件循环中执行。

事件循环进阶:macrotask与microtask(宏观任务与微观任务)

一张图展示JavaScript中的事件循环:
在这里插入图片描述
一次事件循环: 先运行macroTask队列中的一个,然后运行microTask队列中的所有任务。接着开始下一次循环(只是针对macroTask和microTask,一次完整的事件循环会比这个复杂的多)。
JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task
它们的定义?区别?简单点可以按如下理解:
macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
每一个task会从头到尾将这个任务执行完毕,不会执行其它
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(task->渲染->task->…)
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
也就是说,在当前task任务后,下一个task之前,在渲染之前
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
那么什么样的场景会形成macrotask和microtask呢?
macroTask: 主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering(可以看到,事件队列中的每一个事件都是一个macrotask)
microTask: process.nextTick, Promise, Object.observe, MutationObserver (其实都是ES6内容)
补充: 在node环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。
另外,setImmediate则是规定:在下一次Event Loop(宏任务)时触发(所以它是属于优先级较高的宏任务),(Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面),所以setImmediate如果嵌套的话,是需要经过多个Loop才能完成的,而不会像process.nextTick一样没完没了。
实践: 上代码
我们以setTimeout、process.nextTick、promise为例直观感受下两种任务队列的运行方式。

console.log('main1');
//process.nextTick(function() {
//    console.log('process.nextTick1');
//});
setTimeout(function() {
    console.log('setTimeout');
    //process.nextTick(function() {
    //    console.log('process.nextTick2');
    //});
}, 0);
new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});
console.log('main2');

别着急看答案,先以上面的理论自己想想,运行结果会是啥?
最终结果是这样的:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

process.nextTick 和 promise then在 setTimeout 前面输出,已经证明了macroTask和microTask的执行顺序。但是有一点必须要指出的是。上面的图容易给人一个错觉,就是主进程的代码执行之后,会先调用macroTask,再调用microTask,这样在第一个循环里一定是macroTask在前,microTask在后。
但是最终的实践证明:在第一个循环里,process.nextTick1和promise then这两个microTask是在setTimeout这个macroTask里之前输出的,这是为什么呢?
因为主进程的代码也属于macroTask(这一点我比较疑惑的是主进程都是一些同步代码,而macroTask和microTask包含的都是一些异步任务,为啥主进程的代码会被划分为macroTask,不过从实践来看确实是这样,而且也有理论支撑:【翻译】Promises/A+规范)。
主进程这个macroTask(也就是main1、promise和main2)执行完了,自然会去执行process.nextTick1和promise then这两个microTask。这是第一个循环。之后的setTimeout和process.nextTick2属于第二个循环
别看上面那段代码好像特别绕,把原理弄清楚了,都一样 ~
requestAnimationFrame、Object.observe(已废弃) 和 MutationObserver这三个任务的运行机制大家可以从上面看到,不同的只是具体用法不同。重点说下UI rendering。在HTML规范:event-loop-processing-model里叙述了一次事件循环的处理过程,在处理了macroTask和microTask之后,会进行一次Update the rendering,其中细节比较多,总的来说会进行一次UI的重新渲染。
事件循环机制进一步补充
这里就直接引用一张图片来协助理解:(参考自Philip Roberts的演讲《Help, I’m stuck in an event-loop》)
在这里插入图片描述
上图大致描述就是:
• 主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
• 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
• 如此循环
• 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值