
大家都知道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不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在哪一帧执行。