setTimeout和Promise看eventloop

eloop

大家都知道JS是单线程的。设计的原因就是因为简单…

举了个栗子说,两个线程,有一个线程在添加一个dom元素 a,还有一个线程在删除一个dom元素a,那么浏览器就需要决策该听谁的,这样的话就增加了语言设计的复杂性。

JS并非只有一个线程,而只是主线程是单线程的

HTML5有一个api,webworker,利用它,能帮助我们创建子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//index.html
let worker = new Worker('./worker.js');
//把内容发给 工作线程,工作线程可以把结果丢回来

worker.postMessage('向你雇佣的工人发送指令');

worker.onmessage = function(e){
console.log(e.data); //数据在data属性下
}

//worker.js
window.onmessage = function(e){
console.log(e);
this.postMessage('你的工人告诉你他收到命令开始干活了!');
};

注意:他和js主线程不是平级的,主线程可以控制webworkerwebworker不能操作dom,不能获取document以及window

既然是单线程,那Js是怎么处理异步任务呢?停下来等待的话,CPU资源利用率得有多低啊…

异步任务以及事件循环

为了解决这个问题,Javascript将任务的执行方式分为两种:同步/synchronous异步/asynchronous

在浏览器内核中,除了JS线程用户界面后端/UI Backend线程,还有一些其它的线程,比如说浏览器事件触发线程(对应上图的events)定时器触发线程(timing,我的理解:在stack中如果遇到setTimeout的异步任务,就交给WebAPIs,定时到了,该线程就触发,把回调返回,推入到宏任务队列)异步HTTP请求线程(ajax),而异步任务就是分发给这些线程来处理的。

*EventLoop *- 事件的循环检测机制

事件环就是Javascript主线程从callback queue中不断读取事件到执行栈(stack)的这种循环的过程。

setTimeoutPromise出发深入了解一下同步异步的微任务宏任务

单独使用的执行模式

1.最初的试探

执行代码,Promise的基本使用:

1
2
3
4
5
6
7
8
9
10
11
let fn = () => {
console.log(1)
let a = new Promise((resolve, reject) => {
console.log(2)
resolve(3)
})
console.log(4)
return a
}
// 执行
fn().then(data => console.log(data))

以上代码,输出结果为:

1
2
3
4
1 // 同步
2 // 同步
4 // 同步
3 // 异步

注意 new Promise()是同步方法,resolve才是异步方法。
此外,上面的方法,可以有下面这种写法,效果等同,主要是把Promise精简了一下:

1
2
3
4
5
6
7
8
9
10
let fn = () => {
console.log(1)
console.log(2)
let a = Promise.resolve(3)
console.log(4)
return a
}

// 执行
fn().then(data => console.log(data))

因为现在讨论的是Promise的异步功能,所以下面均使用第二种写法的Promise

2.多个同级Promise

编辑器中,输入以下代码,多个同级的单层的Promise

1
2
3
4
5
6
7
8
9
10
11
console.log('同步-0.1')
Promise.resolve().then(() => {
console.log('P-1.1')
})
Promise.resolve().then(() => {
console.log('P-1.2')
})
Promise.resolve().then(() => {
console.log('P-1.3')
})
console.log('同步-0.2')

则会依次输出以下打印,毫无疑问的结果:

1
2
3
4
5
同步-0.1
同步-0.2
P-1.1
P-1.2
P-1.3

3.PromisePromise

复杂一点的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('同步-0.1')
Promise.resolve().then(() => {
console.log('P-1.1')
Promise.resolve().then(() => { // 新加行
console.log('P-2.1') // 新加行
}) // 新加行
})
Promise.resolve().then(() => {
console.log('P-1.2')
Promise.resolve().then(() => { // 新加行
console.log('P-2.2') // 新加行
}) // 新加行
})
Promise.resolve().then(() => {
console.log('P-1.3')
Promise.resolve().then(() => { // 新加行
console.log('P-2.3') // 新加行
}) // 新加行
})
console.log('同步-0.2')

输出结果如下:

1
2
3
4
5
6
7
8
同步-0.1
同步-0.2
P-1.1
P-1.2
P-1.3
P-2.1
P-2.2
P-2.3

可见,多层Promise是一层一层执行的。

4.为了最终确认,进行最后一次验证,在第一个Promise里面多加一层:

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
console.log('同步-0.1')
Promise.resolve().then(() => {
console.log('P-1.1')
Promise.resolve().then(() => {
console.log('P-2.1')
Promise.resolve().then(() => { // 新加行
console.log('P-3.1') // 新加行
}) // 新加行
Promise.resolve().then(() => { // 新加行
console.log('P-3.2') // 新加行
}) // 新加行
})
})
Promise.resolve().then(() => {
console.log('P-1.2')
Promise.resolve().then(() => {
console.log('P-2.2')
})
})
Promise.resolve().then(() => {
console.log('P-1.3')
Promise.resolve().then(() => {
console.log('P-2.3')
})
})
console.log('同步-0.2')

输出结果如下:

1
2
3
4
5
6
7
8
9
10
同步-0.1
同步-0.2
P-1.1
P-1.2
P-1.3
P-2.1
P-2.2
P-2.3
P-3.1
P-3.2

js确认完毕,的确是一层一层的执行。

而且这里可以告诉大家,setTimeoutsetInterval在单独使用的时候,和Promise是一样的,同样是分层执行,这里不再贴代码了(友情提醒:setInterval的话,需要第一次执行就把这个定时器清掉,否则就无限执行,卡死页面秒秒钟的事儿)

混合使用的执行模式

setTimeoutPromise进行混合操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log('同步-0.1')
Promise.resolve().then(() => {
console.log('P-1.1')
})
setTimeout(() => {
console.log('S-1.1')
});
Promise.resolve().then(() => {
console.log('P-1.2')
})
setTimeout(() => {
console.log('S-1.2')
});
console.log('同步-0.2')

执行结果如下。。。问题暴露出来了:

1
2
3
4
5
6
同步-0.1
同步-0.2
P-1.1
P-1.2
S-1.1
S-1.2

在同级情况下,是Promise执行完了setTimeout才会执行

这就要涉及到宏任务(macro task)微任务(micro task)

JS开始执行的时候,就开启一个宏任务(script),然后执行一条条的指令。

JS每次执行完当前的stack之后,都会从callback queue中的宏任务队列取出头部的一个宏任务加入到stack中(执行过程中遇到setTimeout()resolve()ajax.then()这种异步任务则给WebAPIs,异步任务完成后(如setTimeout定时到了、ajax返回了结果),就会触发相应的线程,将它们的回调推入到callback queue中,setTimeout的回调推入到宏任务队列,resolveajax.then的回调会被推入到微任务队列),每一个宏任务后面都跟着一个微任务队列,每次宏任务执行完,主线程就会去查微任务队列,如果不为空,则清空微任务队列,之后重复该循环。

现在如果执行下面的代码,结果也显而易见吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('同步-0.1')
Promise.resolve().then(() => {
console.log('P-1.1')
Promise.resolve().then(() => { // 新加行
console.log('P-2.1') // 新加行
Promise.resolve().then(() => { // 新加行
console.log('P-3.1') // 新加行
}) // 新加行
}) // 新加行
})
setTimeout(() => {
console.log('S-1.1')
});
Promise.resolve().then(() => {
console.log('P-1.2')
})
setTimeout(() => {
console.log('S-1.2')
});
console.log('同步-0.2')

执行结果如下:

1
2
3
4
5
6
7
8
同步-0.1
同步-0.2
P-1.1
P-1.2
P-2.1
P-3.1
S-1.1
S-1.2

无论Promise套用多少层,都会在下一个setTimeout之前执行。

Dom操作到底是同步,还是异步

这里直接说明:js里面的Dom操作代码,是同步执行,但浏览器进行的Dom渲染,是异步操作。

浏览器渲染Dom和执行js,同时只能二选一,渲染一次Dom的时机(GUI渲染)是,当前宏任务和小尾巴微任务执行完,下一个宏任务开始前

vue的方法,则是使用HTML5ApiMutationObserver,监听浏览器将Dom渲染完成的时机。

上面也说了,浏览器渲染一次Dom,是下一个宏任务开始前,这样使用了setTimeout,保证了Dom确实渲染完成。

这里也需要稍作提醒,javascript操作Dom是同步的,但操作Dom,毕竟超出了javascript本身语言的Api,每操作一次Dom,都需要消耗一定的性能,所以,在适合的情况下,最好先把要修改的Dom的内容,以字符串或者虚拟Dom的形式拼接好,然后操作一次Dom,把组装好的Dom字符串或虚拟Dom,一次性的塞进HTML页面的真实Dom中。

async/await

1
2
3
4
5
6
7
8
9
10
11
function A() {
return Promise.resolve(Date.now());
}
async function B() {
console.log(Math.random());
let now = await A();
console.log(now);
}
console.log(1);
B();
console.log(2)

其实,async/await 只是 Promise+generator 的一种语法糖而已。上面的代码我们改写为这样,可以更加清晰一点:

1
2
3
4
5
6
7
8
9
function B() {
console.log(Math.random());
A().then(function(now) {
console.log(now);
})
}
console.log(1);
B();
console.log(2);

这样我们就能明白输出的先后顺序了: 1, 0.4793526730678652(随机数), 2, 1557830834679(时间戳);

requestAnimationFrame

requestAnimationFrame也属于异步执行的方法,但该方法既不属于宏任务,也不属于微任务。按照MDN中的定义:

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

requestAnimationFrameGUI渲染之前执行,但在微服务之后,不过requestAnimationFrame不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在哪一帧执行。

-------------要说再见啦感谢大佬的光临~-------------