运行机制

# 运行机制

[TOC]

# 一、异步和单线程

因为是单线程,所以必须异步。

# 1.1 异步

console.log(1);
setTimeout(function () {
    console.log(2);
}, 1000);
console.log(3);
console.log(4);

// 一秒后才打印2
// 1 3 4 2
1
2
3
4
5
6
7
8
9
console.log(1);
setTimeout(function () {
    console.log(2);
}, 0);
console.log(3);
console.log(4);
// 1 3 4 2
1
2
3
4
5
6
7

js 是单线程(同一时间只能做一件事),而且有一个任务队列(**全部的同步任务执行完毕后,再来执行异步任务)。**其中,setTimeout是异步任务

于是,执行的顺序是:

  • 先执行同步任务console.log(1)
  • 遇到异步任务setTimeout,要挂起
  • 执行同步任务console.log(3)
  • 全部的同步任务执行完毕后,再来执行异步任务console.log(2)

如果同步任务没有执行完,异步任务是不会执行的

console.log('A');
while (1) {

}
console.log('B');
// A
1
2
3
4
5
6

while是同步任务,代码会陷入死循环里出不来,自然也就无法打印B

console.log('A');

setTimeout(function () {
    console.log('B');
})

while (1) {

}
// A
1
2
3
4
5
6
7
8
9
10

上方代码的打印结果仍然是A。因为while是同步任务,setTimeout是异步任务,所以还是那句话:如果同步任务没有执行完,队列里的异步任务是不会执行的

# 1.1.1 异步任务

  • setTimeout 和 setInterval
  • DOM事件
  • Promise

# 1.2 同步

console.log('A');

alert('haha'); //1秒之后点击确认

console.log('B');
1
2
3
4
5

alert函数是同步任务,只有点击了确认,才会继续打印B

# 1.3 同步和异步的对比

因为setTimeout异步任务,所以程序并不会卡在那里,而是继续向下执行(即使setimeout设置了倒计时一万秒);但是alert函数是同步任务,程序会卡在那里,如果它没有执行,后面的也不会执行(卡在那里,自然也就造成了阻塞)。

# 1.4 使用异步的场景

什么时候需要等待,就什么时候用异步。

  • 定时任务:setTimeout(定时炸弹)、setInterval(循环执行)
  • 网络请求:ajax请求、动态<img>加载
  • 事件绑定(比如说,按钮绑定点击事件之后,用户爱点不点。我们不可能卡在按钮那里,什么都不做。所以,应该用异步)
  • ES6中的Promise

代码举例:

console.log('start');
var img = document.createElement('img');
// 当页面载入完毕后执行
img.onload = function () {
    console.log('loaded');
}
img.src = '/xxx.png';
console.log('end');
1
2
3
4
5
6
7
8

上图中,先打印start,然后执行img.src = '/xxx.png',然后打印end,最后打印loaded

# 二、任务队列和事件循环

# 2.1 任务队列

# 2.1.1 同步任务

在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

# 2.1.2 异步任务

不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

  • 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制

# 2.2 Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

event loop

调用栈中遇到DOM操作、ajax请求以及setTimeout等WebAPIs的时候就会交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,之后等栈中的task执行完之后再去执行任务队列之中的回调函数,回调函数被执行引擎添加到调用栈中。

for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

// 3 3 3
1
2
3
4
5
6
7
// for是同步任务
for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}
console.log(i);

// 第 1 个 3 直接输出,1 秒之后,连续输出 3 个 3
1
2
3
4
5
6
7
8
9