简单介绍
js的是单线程的,如果一个任务执行时间过长,那程序的执行就会被阻塞。
为了防止它执行的堵塞,js将任务分为两种
- 同步任务
- 异步任务
网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
主线程会在执行完同步任务后去执行异步任务。
js的事件循环机制
转自掘金 ssssyoki
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
常见的 setTimeout
,Promise
,dom
事件等都是异步任务。
setTimeout(fn)和Promise.resolve().then(fn) 仍然是异步执行的,它们在执行后立即将异步任务(执行回调函数)推入异步的队列中
宏任务与微任务
为什么要区分宏任务和微任务?
如果不将任务进行划分,按照队列方式执行,当大量任务执行时,某些任务的回调迟迟得不到执行(都在队尾),就会造成应用效果上的卡顿。
所以设计者将任务分为宏任务和微任务,微任务可以穿插在宏任务中执行。
宏任务和微任务有什么?
- macro-task(宏任务):
- 整体代码sript
- I/O
- setTimeout
- setInterval
- requestAnimationFrame (浏览器)
- dom事件(浏览器)
- micro-task(微任务):
- Promise的方法及其派生
- MutationObserver(浏览器)
- process.nextTick(node)
process.nextTick不完全属于微任务,它在一轮微任务执行完后执行,(或按照语义,它总是在下一轮任务开始前执行)node内部维护着另一个队列而不是与Promise的微任务队列共享
你可能会疑惑为什么srcipt也被分入宏任务了,广义上讲script也属于宏任务,这里不区分同步和异步执行(本质是开启一个队列执行)。
宏任务与微任务的执行顺序
宏任务任务执行结束后,执行微任务直至微任务队列清空,一轮事件循环结束。新的循环开始检测是否有宏任务可以执行...
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
可能人会认为,先执行完所有宏任务,再执行所有微任务,这在浏览器环境下是完全错误的。
正确的执行顺序:执行完一个宏任务后,就去执行微任务队列,直至微任务队列清空,接着执行下一个宏任务。
一般来说,微任务队列中的第一个任务,都是由前面的执行宏任务过程中添加的。
我们在微任务中可能添加新的微任务,那么应该怎么执行呢?
考虑以下代码
new Promise((resolve) => resolve()).then(() => {
for(let i = 0;i< 10 ;i++) new Promise.resolve().then(()=>console.log('1'))
})
setTimeout(()=>console.log('2'))
// 1 * 10
// 2
微任务执行过程中添加的微任务还是会一直被执行直至清空,而不是放入下一个循环中,我们应该避免这种情况。
栗子
触发MutationObserver和Dom事件(chrome环境下)来源
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
inner是内层的盒子,outer是外层盒子,他们都注册了事件,点击内层时会显示什么呢?
click
promise
mutate
click
promise
mutate
timeout
timeout
解释过程(下面表格的状态都是一次宏任务或微任务结束时的状态)
-
点击内层盒子,内层事件冒泡至外层
宏任务队列 微任务队列 内层click事件 外层click事件 console:
-
新的事件循环开始,第一个宏任务出队执行。执行过程中打印click,设置定时器宏任务,设置promise微任务,修改被
MutationObserver
监测的属性(添加MutationObserver微任务)宏任务队列 微任务队列 外层click事件 promise 定时器 MutationObserver console:click
-
宏任务执行结束,检查执行微任务队列 打印了promise 和 mutate
宏任务队列 微任务队列 外层click事件 定时器 console:click promise mutate
-
新的事件循环开始 因为同2,3步骤一样不再复述
宏任务队列 微任务队列 定时器 定时器 console:click promise mutate click promise mutate
-
最后两个定时器相继两轮宏任务
- console:click promise mutate click promise mutate timeout timeout
下面可以不用看= =
/*不用看开始
不过有一个有趣的现象,如果在微任务中添加script呢(chrome浏览器环境下)?
// chrome环境下 new Promise((resolve) => resolve()).then(() => { console.log('0') let b = document.createElement('script') new Promise((resolve)=> resolve()).then(()=>console.log('1')) b.text = ` setTimeout(()=>{ console.log('2') }) console.log('3') ` document.body.appendChild(b) new Promise((resolve)=> resolve()).then(()=>console.log('4')) console.log('5') }) // 0 // 3 // 5 // 1 // 4 // Promise {<resolved>: undefined} // 2
- 微任务执行
- 添加script进入html,直接暂停微任务的执行,直接跳去执行插入script的内容
- 执行完script内容后,没有清除微任务队列,而是返回暂停的微任务执行后面的语句
- 清空微任务队列,开启下一轮循环...
let b = document.createElement('script') b.text = ` let d = document.createElement('script') d.text = 'console.log(9)' console.log(10) document.body.appendChild(d) console.log(11) ` document.body.appendChild(b) // 10 // 9 // 11
上面的代码套了个娃,第一层添加为了模拟同步的执行环境。
虽然对原理不太清楚,不过我们发现无论是在执行同步还是异步的任务中,插入script后都会中断当前执行,去执行script中的代码。
不用看结束*/
node的宏任务执行
因为node有大量异步操作,它的宏任务也复杂得多,执行的优先级如下图
简单说下node宏任务执行的三个阶段
(按顺序)
- 定时器阶段,
setTimeout,setInterval
时间到了后回调被推入此队列 - 轮询(poll)阶段,文件I/O,网络I/O等异步操作完成后,会触发事件,这些事件的回调被推入此队列
- check阶段,
setImmediate
回调事件被推入此队列
补充
上面的阶段仍不完整
- 在定时器阶段之后,会先处理I/O异常的回调,这里称它为异常阶段
- 异常阶段结束后,又会进入定时器阶段(异常阶段可能有定时器到时间了,回调被推入队列),如此反复,直至既没有异常又没有定时器回调,才进入poll阶段
- poll阶段队列可能为空,那么线程会等待callback加入,等待的时间有上限( 相当于阻塞了一段时间 ),过后自动进入check阶段
- 在check阶段结束后,还有一个关闭事件阶段, socket 或句柄(handle)被突然关闭,如
socket.destroy
,close回调事件被推入此队列
经过补充,我们列出最后的宏任务执行顺序
- timer阶段
- I/O异常回调阶段
- 空闲和准备阶段,如果有定时器时间到了回到第一阶段,如此反复直到没有定时器了
- poll阶段,如果没有事件,就会等待callback一段时间。到达时间上限后进入check阶段
- check阶段,有
setImmediate
回调则执行,后进入下一阶段 - 关闭事件阶段
Node微任务与宏任务执行顺序
node11及之后
每一次宏任务结束后,清空微任务队列,然后执行nextTick队列。
与chrome浏览器表现基本一致
node10及之前
- 执行完一个阶段的所有任务
- 执行完nextTick队列里面的内容
- 然后执行完微任务队列的内容
process.nextTick(node11及之后的环境)
process.nextTick不完全属于微任务,它在一轮微任务执行完后执行,(或按照语义,它总是在下一轮任务开始前执行),node内部维护着另一个队列而不是与Promise的微任务队列共享
考虑以下执行
console.log('0')
setTimeout(() => {
console.log('1')
for(let i = 0; i <3;i++){
new Promise.resolve().then(() => console.log('2'))
process.nextTick(() => console.log('3'))
}
})
如果你的答案是 0 1 2 3 2 3 2 3 ,那么你没有理解process.nextTick的解释
正确的答案是 0 1 2 2 2 3 3 3
process.nextTick的任务会一直放在微任务执行完后才执行
拓展阅读:vue的异步批量更新
异步批量更新流程
简单提及Vue更新的流程
-
数据被修改
this.xx = '被修改了'
-
数据被修改触发setter函数,修改被Watcher收集了,Watcher把自己放入待更新的数组
-
在次任务中的调用了
this.$nextTick
中的回调函数被收集入回调的数组中 -
宏任务结束后,nextTick回调数组在微任务执行。nextTick回调数组中的第一个执行的函数就是Watcher数组去通知更新dom(下图的flushBatcherQueue函数),之后就按顺序执行被收集的
nextTick
回调。我们定义nextTick回调总是在更新dom之后执行,所以一定可以保证回调是在dom的更改后执行
这样做有什么好处呢?如果是同步更新数据,就无法做到优化了。
如果是异步批量更新,可以将一次宏任务中的数据更新收集起来,进行patch和去重等操作,尽可能少地更新dom。
微任务中执行更新和nextTick回调
Vue在异步选择的策略为以下优先级
-
Promise.resolve().then(nextTickHandler)
-
new MutationObserver(flushCallbacks)
MutationObserver在上面的例子已经演示怎么使用了。此处Vue不是为了监听dom的变化(textNode根本没被插入dom中),而是利用MutationObserver的特性创建一个微任务。
let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) }
-
setImmediate
-
setTimeout
后面两种不是微任务的情况了,是浏览器不兼容迫不得已的情况。
如果宏任务更新,因为会被放入队尾,需要等待前面的任务完成,这可能会导致dom更新的延迟;
而采用微任务,在一次宏任务完成后立即更新dom,就不会存在延迟。
拓展阅读:包含渲染过程的浏览器事件循环顺序
转载: segmentfault.com/a/119000000…
来源:segmentfault 作者:ma63d
第一步,从多个task queue中的一个queue里,挑出一个最老的task。(因为有多个task queue的存在,使得浏览器可以完成我们前面说的,优先、高频率的执行某些task queue中的任务,比如UI的task queue)。 然后2到5步,执行这个task。
第六步, Perform a microtask checkpoint. ,这里会执行完microtask queue中的所有的microtask,如果microtask执行过程中又添加了microtask,那么仍然会执行新添加的microtask,当然,这个机制好像有限制,一轮microtask的执行总量似乎有限制(1000?),数量太多就执行一部分留下的以后再执行?这里我不太确定。第七步,Update the rendering:
7.2到7.4,当前轮次的event loop中关联到的document对象会保持某些特定顺序,这些document对象都会执行需要执行UI render的,但是并不是所有关联到的document都需要更新UI,浏览器会判断这个document是否会从UI Render中获益,因为浏览器只需要保持60Hz的刷新率即可,而每轮event loop都是非常快的,所以没必要每个document都Render UI。
7.5和7.6 run the resize steps/run the scroll steps不是说去执行resize和scroll。每次我们scoll的时候视口或者dom就已经立即scroll了,并把document或者dom加入到 pending scroll event targets中,而run the scroll steps具体做的则是遍历这些target,在target上触发scroll事件。run the resize steps也是相似的,这个步骤是触发resize事件。
7.8和7.9 后续的media query, run CSS animations and send events等等也是相似的,都是触发事件,第10步和第11步则是执行我们熟悉的requestAnimationFrame回调和IntersectionObserver回调(第十步还是挺关键的,raf就是在这执行的!)。
7.12 渲染UI,关键就在这了。第九步 继续执行event loop,又去执行task,microtasks和UI render。
参考阅读