We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
建议先学习以下知识:
JavaScript是一个单线程的脚本语言。
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
javascript异步任务异步任务包含两种:宏任务和微任务
宏任务:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering(DOM更新)、DOM事件回调、AJAX事件回调。
AJAX事件回调
const request = XMLHttpRequest(); request.open("GET","baidu.com"); request.onreadystatechange = ()=>{ if(request.readyState === 4){ if(request.status >= 200 && request.status <400){ //success }else{ //error } } } request.send(); // 代码执行到最后,这段代码就即将要完成了。V8 将要销毁这段代码的环境对象。这里就是 V8 执行微任务的一个检查点
定时器回调
requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行。
requestAnimationFrame
微任务开始执行的契机是什么呢? 如上面代码所示,整段代码就即将要完成。V8 将要销毁这段代码的环境对象。这里就是 V8 执行微任务的一个检查点
修改完DOM后什么时候进行渲染? 根据HTML Standard,一轮事件循环执行结束之后,下轮事件循环执行之前开始进行UI render。即:宏任务执行完毕,接着执行完所有的微任务后,此时本轮循环结束,开始执行UI render。UI render完毕之后接着下一轮循环。
UI render
setTimeout(_ => console.log(4)) new Promise(resolve => { resolve() console.log(1) }).then(_ => { console.log(3) }) console.log(2)
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
setTimeout
Promise.then
所有会进入的异步都是指的事件回调中的那部分代码 也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。 在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。 所以就得到了上述的输出结论1、2、3、4。
new Promise
then
1、2、3、4
+部分表示同步执行的代码
+setTimeout(_ => { - console.log(4) +}) +new Promise(resolve => { + resolve() + console.log(1) +}).then(_ => { - console.log(3) +}) +console.log(2)
本来setTimeout已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise的处理(临时添加业务)。
Promise
所以进阶的,即便我们继续在Promise中实例化Promise,其输出依然会早于setTimeout的宏任务:
setTimeout(_ => console.log(4)) new Promise(resolve => { resolve() console.log(1) }).then(_ => { console.log(3) Promise.resolve().then(_ => { console.log('before timeout') }).then(_ => { Promise.resolve().then(_ => { console.log('also before timeout') }) }) }) console.log(2) // 1 // 2 // 3 // before timeout // also before timeout // 4
当然了,实际情况下很少会有简单的这么调用Promise的,一般都会在里边有其他的异步操作,比如fetch是基于Promise的,是微任务。
fetch
在上边简单的说明了两种任务的差别,以及Event Loop的作用,那么在真实的浏览器中是什么表现呢? 首先要明确的一点是,宏任务必然是在微任务之后才执行的(因为微任务实际上是宏任务的其中一个步骤)
Event Loop
I/O这一项感觉有点儿笼统,有太多的东西都可以称之为I/O,点击一次button,上传一个文件,与程序产生交互的这些都可以称之为I/O。
I/O
button
假设有这样的一些DOM结构:
DOM
<div id="outer"> <div id="inner"></div> </div>
const $inner = document.querySelector('#inner') const $outer = document.querySelector('#outer') function handler () { console.log('click') // 直接输出 Promise.resolve().then(_ => console.log('promise')) // 注册微任务 setTimeout(_ => console.log('timeout')) // 注册宏任务 requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务 $outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务 } new MutationObserver(_ => { console.log('observer') }).observe($outer, { attributes: true }) $inner.addEventListener('click', handler) $outer.addEventListener('click', handler)
如果点击#inner,其执行顺序一定是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout。 因为一次I/O创建了一个宏任务,也就是说在这次任务中会去触发handler。 按照代码中的注释,在同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现了Promise和MutationObserver两个微任务,遂执行之。 因为click事件会冒泡,所以对应的这次I/O会触发两次handler函数(一次在inner、一次在outer),所以会优先执行冒泡的事件(早于其他的宏任务),即inner执行完同步代码和微任务后向上冒泡,inner再执行完同步代码和微任务,直至最后一层冒泡结束后再开始执行宏任务,也就是说会重复上述的逻辑。 在执行完同步代码与微任务以后,这时继续向后查找有木有宏任务。 需要注意的一点是,因为我们触发了setAttribute,实际上修改了DOM的属性,这会导致页面的重绘,而这个set的操作是同步执行的,也就是说requestAnimationFrame的回调会早于setTimeout所执行。
#inner
click
promise
observer
animationFrame
timeout
handler
MutationObserver
inner
outer
setAttribute
set
使用上述的示例代码,如果将手动点击DOM元素的触发方式变为$inner.click(),那么会得到不一样的结果。 在Chrome下的输出顺序大致是这样的: click -> click -> promise -> observer -> promise -> animationFrame -> animationFrame -> timeout -> timeout。 与我们手动触发click的执行顺序不一样的原因是这样的,因为并不是用户通过点击元素实现的触发事件,而是类似dispatchEvent这样的方式,我个人觉得并不能算是一个有效的I/O,在执行了一次handler回调注册了微任务、注册了宏任务以后,实际上外边的$inner.click()并没有执行完。 所以在微任务执行之前,还要继续冒泡执行下一次事件,也就是说触发了第二次的handler。 所以输出了第二次click,等到这两次handler都执行完毕后才会去检查有没有微任务、有没有宏任务。
$inner.click()
1、.click()的这种触发事件的方式类似dispatchEvent,可以理解为同步执行的代码
.click()
dispatchEvent
document.body.addEventListener('click', _ => console.log('click')) document.body.click() document.body.dispatchEvent(new Event('click')) console.log('done') // > click // > click // > done
2、MutationObserver的监听不会说同时触发多次,多次修改只会有一次回调被触发。
new MutationObserver(_ => { console.log('observer') // 如果在这输出DOM的data-random属性,必然是最后一次的值,不解释了 }).observe(document.body, { attributes: true }) document.body.setAttribute('data-random', Math.random()) document.body.setAttribute('data-random', Math.random()) document.body.setAttribute('data-random', Math.random()) // 只会输出一次 ovserver
Node也是单线程,但是在处理Event Loop上与浏览器稍微有些不同。
就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的process.nextTick以及宏任务的setImmediate。
process.nextTick
setImmediate
在官方文档中的定义,setImmediate为一次Event Loop执行完毕后调用。 setTimeout则是通过计算一个延迟时间后进行执行。
但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。 因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs,而这时定时器已经处于可执行回调的状态了。 所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop,这时才会执行setImmediate。
XXs
setTimeout(_ => console.log('setTimeout')) setImmediate(_ => console.log('setImmediate'))
执行多次会得到不同的结果。
但是如果后续添加一些代码以后,就可以保证setTimeout一定会在setImmediate之前触发了:
setTimeout(_ => console.log('setTimeout')) setImmediate(_ => console.log('setImmediate')) let countdown = 10000000 while(countdown--) { } // 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行setTimeout再结束这一轮循环,也就是说开始执行setImmediate
如果在另一个宏任务中,必然是setImmediate先执行:
require('fs').readFile(__dirname, _ => { setTimeout(_ => console.log('timeout')) setImmediate(_ => console.log('immediate')) }) // 如果使用一个设置了延迟的setTimeout也可以实现相同的效果
就像上边说的,这个可以认为是一个类似于Promise和MutationObserver的微任务实现,在代码执行的过程中可以随时插入nextTick,并且会保证在下一个宏任务开始之前所执行。
nextTick
在使用方面的一个最常见的例子就是一些事件绑定类的操作:
class Lib extends require('events').EventEmitter { constructor () { super() this.emit('init') } } const lib = new Lib() lib.on('init', _ => { // 这里将永远不会执行 console.log('init!') })
因为上述的代码在实例化Lib对象时是同步执行的,在实例化完成以后就立马发送了init事件。 而这时在外层的主程序还没有开始执行到lib.on('init')监听事件的这一步。 所以会导致发送事件时没有回调,回调注册后事件不会再次发送。
Lib
init
lib.on('init')
我们可以很轻松的使用process.nextTick来解决这个问题:
class Lib extends require('events').EventEmitter { constructor () { super() process.nextTick(_ => { this.emit('init') }) // 同理使用其他的微任务 // 比如Promise.resolve().then(_ => this.emit('init')) // 也可以实现相同的效果 } }
这样会在主进程的代码执行完毕后,程序空闲时触发Event Loop流程查找有没有微任务,然后再发送init事件。
因为,async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似:
async/await
await
setTimeout(_ => console.log(4)) async function main() { console.log(1) await Promise.resolve() console.log(3) } main() console.log(2) // 1 // 2 // 3 // 4
async函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调
async
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
建议先学习以下知识:
正文
JavaScript是一个单线程的脚本语言。
同步任务和异步任务
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
微任务与宏任务
javascript异步任务异步任务包含两种:宏任务和微任务
宏任务
宏任务:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering(DOM更新)、DOM事件回调、AJAX事件回调。
微任务
requestAnimationFrame
姑且也算是宏任务吧,requestAnimationFrame
在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行。微任务开始执行的契机是什么呢?
如上面代码所示,整段代码就即将要完成。V8 将要销毁这段代码的环境对象。这里就是 V8 执行微任务的一个检查点
修改完DOM后什么时候进行渲染?
根据HTML Standard,一轮事件循环执行结束之后,下轮事件循环执行之前开始进行
UI render
。即:宏任务执行完毕,接着执行完所有的微任务后,此时本轮循环结束,开始执行UI render
。UI render
完毕之后接着下一轮循环。经典面试题
setTimeout
就是作为宏任务来存在的,而Promise.then
则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。所有会进入的异步都是指的事件回调中的那部分代码
也就是说
new Promise
在实例化的过程中所执行的代码都是同步进行的,而then
中注册的回调才是异步执行的。在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
所以就得到了上述的输出结论
1、2、3、4
。+部分表示同步执行的代码
本来
setTimeout
已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise
的处理(临时添加业务)。所以进阶的,即便我们继续在
Promise
中实例化Promise
,其输出依然会早于setTimeout
的宏任务:当然了,实际情况下很少会有简单的这么调用
Promise
的,一般都会在里边有其他的异步操作,比如fetch
是基于Promise
的,是微任务。在浏览器中的表现
在上边简单的说明了两种任务的差别,以及
Event Loop
的作用,那么在真实的浏览器中是什么表现呢?首先要明确的一点是,宏任务必然是在微任务之后才执行的(因为微任务实际上是宏任务的其中一个步骤)
I/O
这一项感觉有点儿笼统,有太多的东西都可以称之为I/O
,点击一次button
,上传一个文件,与程序产生交互的这些都可以称之为I/O
。假设有这样的一些
DOM
结构:如果点击
#inner
,其执行顺序一定是:click
->promise
->observer
->click
->promise
->observer
->animationFrame
->animationFrame
->timeout
->timeout
。因为一次
I/O
创建了一个宏任务,也就是说在这次任务中会去触发handler
。按照代码中的注释,在同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现了
Promise
和MutationObserver
两个微任务,遂执行之。因为
click
事件会冒泡,所以对应的这次I/O
会触发两次handler函数(一次在inner
、一次在outer
),所以会优先执行冒泡的事件(早于其他的宏任务),即inner
执行完同步代码和微任务后向上冒泡,inner
再执行完同步代码和微任务,直至最后一层冒泡结束后再开始执行宏任务,也就是说会重复上述的逻辑。在执行完同步代码与微任务以后,这时继续向后查找有木有宏任务。
需要注意的一点是,因为我们触发了
setAttribute
,实际上修改了DOM
的属性,这会导致页面的重绘,而这个set
的操作是同步执行的,也就是说requestAnimationFrame
的回调会早于setTimeout
所执行。换种触发方式
使用上述的示例代码,如果将手动点击DOM元素的触发方式变为
$inner.click()
,那么会得到不一样的结果。在Chrome下的输出顺序大致是这样的:
click
->click
->promise
->observer
->promise
->animationFrame
->animationFrame
->timeout
->timeout
。与我们手动触发click的执行顺序不一样的原因是这样的,因为并不是用户通过点击元素实现的触发事件,而是类似dispatchEvent这样的方式,我个人觉得并不能算是一个有效的
I/O
,在执行了一次handler
回调注册了微任务、注册了宏任务以后,实际上外边的$inner.click()
并没有执行完。所以在微任务执行之前,还要继续冒泡执行下一次事件,也就是说触发了第二次的
handler
。所以输出了第二次
click
,等到这两次handler
都执行完毕后才会去检查有没有微任务、有没有宏任务。注意
1、
.click()
的这种触发事件的方式类似dispatchEvent
,可以理解为同步执行的代码2、
MutationObserver
的监听不会说同时触发多次,多次修改只会有一次回调被触发。在Node中的表现
Node也是单线程,但是在处理
Event Loop
上与浏览器稍微有些不同。就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的
process.nextTick
以及宏任务的setImmediate
。setImmediate与setTimeout的区别
在官方文档中的定义,
setImmediate
为一次Event Loop
执行完毕后调用。setTimeout
则是通过计算一个延迟时间后进行执行。但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。
因为如果主进程中先注册了两个任务,然后执行的代码耗时超过
XXs
,而这时定时器已经处于可执行回调的状态了。所以会先执行定时器,而执行完定时器以后才是结束了一次
Event Loop
,这时才会执行setImmediate
。执行多次会得到不同的结果。
但是如果后续添加一些代码以后,就可以保证
setTimeout
一定会在setImmediate
之前触发了:如果在另一个宏任务中,必然是
setImmediate
先执行:process.nextTick
就像上边说的,这个可以认为是一个类似于
Promise
和MutationObserver
的微任务实现,在代码执行的过程中可以随时插入nextTick
,并且会保证在下一个宏任务开始之前所执行。在使用方面的一个最常见的例子就是一些事件绑定类的操作:
因为上述的代码在实例化
Lib
对象时是同步执行的,在实例化完成以后就立马发送了init
事件。而这时在外层的主程序还没有开始执行到
lib.on('init')
监听事件的这一步。所以会导致发送事件时没有回调,回调注册后事件不会再次发送。
我们可以很轻松的使用
process.nextTick
来解决这个问题:这样会在主进程的代码执行完毕后,程序空闲时触发
Event Loop
流程查找有没有微任务,然后再发送init
事件。async/await函数
因为,
async/await
本质上还是基于Promise
的一些封装,而Promise
是属于微任务的一种。所以在使用await
关键字与Promise.then
效果类似:async
函数在await
之前的代码都是同步执行的,可以理解为await
之前的代码属于new Promise
时传入的代码,await
之后的所有代码都是在Promise.then
中的回调The text was updated successfully, but these errors were encountered: