大家都知道JS
是单线程的。设计的原因就是因为简单…
举了个栗子说,两个线程,有一个线程在添加一个dom
元素 a
,还有一个线程在删除一个dom
元素a
,那么浏览器就需要决策该听谁的,这样的话就增加了语言设计的复杂性。
JS并非只有一个线程,而只是主线程是单线程的
HTML5有一个api,webworker,利用它,能帮助我们创建子线程
1 | //index.html |
注意:他和js
主线程不是平级的,主线程可以控制webworker
,webworker
不能操作dom
,不能获取document
以及window
。
既然是单线程,那Js
是怎么处理异步任务呢?停下来等待的话,CPU资源利用率得有多低啊…
异步任务以及事件循环
为了解决这个问题,Javascript
将任务的执行方式分为两种:同步/synchronous
和 异步/asynchronous
在浏览器内核中,除了JS线程
和用户界面后端/UI Backend线程
,还有一些其它的线程,比如说浏览器事件触发线程(对应上图的events)
、定时器触发线程(timing,我的理解:在stack中如果遇到setTimeout的异步任务,就交给WebAPIs,定时到了,该线程就触发,把回调返回,推入到宏任务队列)
、异步HTTP请求线程(ajax)
,而异步任务就是分发给这些线程来处理的。
*EventLoop *- 事件的循环检测机制
事件环就是Javascript
主线程从callback queue
中不断读取事件到执行栈(stack
)的这种循环的过程。
从setTimeout
和Promise
出发深入了解一下同步异步的微任务宏任务
单独使用的执行模式
1.最初的试探
执行代码,Promise
的基本使用:
1 | let fn = () => { |
以上代码,输出结果为:
1 | 1 // 同步 |
注意 new Promise()
是同步方法,resolve
才是异步方法。
此外,上面的方法,可以有下面这种写法,效果等同,主要是把Promise
精简了一下:
1 | let fn = () => { |
因为现在讨论的是Promise
的异步功能,所以下面均使用第二种写法的Promise
2.多个同级Promise
编辑器中,输入以下代码,多个同级的单层的Promise
:
1 | console.log('同步-0.1') |
则会依次输出以下打印,毫无疑问的结果:
1 | 同步-0.1 |
3.Promise
套Promise
复杂一点的栗子:
1 | console.log('同步-0.1') |
输出结果如下:
1 | 同步-0.1 |
可见,多层Promise
是一层一层执行的。
4.为了最终确认,进行最后一次验证,在第一个Promise
里面多加一层:
1 | console.log('同步-0.1') |
输出结果如下:
1 | 同步-0.1 |
js
确认完毕,的确是一层一层的执行。
而且这里可以告诉大家,setTimeout
和setInterval
在单独使用的时候,和Promise
是一样的,同样是分层执行,这里不再贴代码了(友情提醒:setInterval
的话,需要第一次执行就把这个定时器清掉,否则就无限执行,卡死页面秒秒钟的事儿)
混合使用的执行模式
将setTimeout
和Promise
进行混合操作
1 | console.log('同步-0.1') |
执行结果如下。。。问题暴露出来了:
1 | 同步-0.1 |
在同级情况下,是Promise
执行完了setTimeout
才会执行
这就要涉及到宏任务(macro task)
和微任务(micro task)
了
JS
开始执行的时候,就开启一个宏任务(script
),然后执行一条条的指令。
JS
每次执行完当前的stack
之后,都会从callback queue
中的宏任务队列取出头部的一个宏任务加入到stack
中(执行过程中遇到setTimeout()
、resolve()
和ajax.then()
这种异步任务则给WebAPIs
,异步任务完成后(如setTimeout
定时到了、ajax
返回了结果),就会触发相应的线程,将它们的回调推入到callback queue
中,setTimeout
的回调推入到宏任务队列,resolve
和ajax.then
的回调会被推入到微任务队列),每一个宏任务后面都跟着一个微任务队列,每次宏任务执行完,主线程就会去查微任务队列,如果不为空,则清空微任务队列,之后重复该循环。
现在如果执行下面的代码,结果也显而易见吧:
1 | console.log('同步-0.1') |
执行结果如下:
1 | 同步-0.1 |
无论Promise
套用多少层,都会在下一个setTimeout
之前执行。
Dom
操作到底是同步,还是异步
这里直接说明:js里面的Dom操作代码,是同步执行,但浏览器进行的Dom渲染,是异步操作。
浏览器渲染Dom和执行js,同时只能二选一,渲染一次Dom的时机(GUI渲染)是,当前宏任务和小尾巴微任务执行完,下一个宏任务开始前
vue
的方法,则是使用HTML5
的Api
—MutationObserver
,监听浏览器将Dom
渲染完成的时机。
上面也说了,浏览器渲染一次Dom,是下一个宏任务开始前,这样使用了setTimeout
,保证了Dom确实渲染完成。
这里也需要稍作提醒,javascript
操作Dom是同步的,但操作Dom,毕竟超出了javascript
本身语言的Api
,每操作一次Dom,都需要消耗一定的性能,所以,在适合的情况下,最好先把要修改的Dom的内容,以字符串或者虚拟Dom的形式拼接好,然后操作一次Dom,把组装好的Dom字符串或虚拟Dom,一次性的塞进HTML页面的真实Dom中。
async/await
1 | function A() { |
其实,async/await
只是 Promise+generator
的一种语法糖而已。上面的代码我们改写为这样,可以更加清晰一点:
1 | function B() { |
这样我们就能明白输出的先后顺序了: 1, 0.4793526730678652(随机数), 2, 1557830834679(时间戳);
requestAnimationFrame
requestAnimationFrame
也属于异步执行的方法,但该方法既不属于宏任务,也不属于微任务。按照MDN
中的定义:
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
requestAnimationFrame
是GUI
渲染之前执行,但在微服务之后,不过requestAnimationFrame
不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在哪一帧执行。