Eventloop机制是Javascript实现伪“多线程”的关键所在。这一部分也是面试常常会被问到的问题,做个笔记以备查询。
JavaScript的执行机制
首先明确一点,JavaScript是一个单线程语言。所有的任务在JavaScript中都被划分为以下三类之一:
- 同步任务
- 异步宏任务
- 异步微任务
JavaScript通过频繁地切换这些任务,来实现异步任务。
从整体的执行机制而言,JavaScript按照以下顺序处理手头的任务:
- 如果是同步任务,直接在主线程中执行。
- 如果是异步任务,在Event Table中注册该函数,当异步事件完成后(I/O, timeout等),将一步操作的回调函数移入Event Queue中。
- 当主线程任务执行完毕后,开始执行Event Queue中的异步回调任务(同一个EventLoop内永远是先微任务后宏任务),直到执行完毕。
- 如此往复循环,称之为Event Loop。
宏任务?微任务?
如何区分
- 宏任务
setTimeout
,setInterval
,setImmediate
- 微任务
- 原生
Promise
process.nextTick
(node.js)Object.observe
MutationObserver
- 原生
执行机制
Event Queue中的异步事件按照以下流程图执行:
- 先从Event Queue中的宏任务队列中拿出一个宏任务并执行。
- 执行完每个宏任务后,检查微任务队列,并执行所有微任务。
一些例子
简单难度
console.log(1)
new Promise((resolve, reject) => {
console.log(2)
resolve()
}).then(res => {
console.log(3)
})
console.log(4)
输出:1 2 4 3
解释:
- 首先执行同步任务
console.log(1)
,输出1
。 - 碰到
new Promise
,立即执行console.log(2)
输出2
,并且把.then
的回调函数在Event Queue中注册。 - 继续执行同步任务
console.log(4)
,输出4
。 - 同步任务执行完毕,执行微任务
console.log(3)
,输出3
。
中等难度
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('then1')
}).then(() => {
console.log('then2')
})
console.log('end')
输出:start promise end then1 then2 setTimeout
解释:
- 首先执行同步任务
console.log(start)
,输出start
。 - 碰到
setTimeout
,将回调放入宏任务队列。 - 碰到
new Promise
,立即执行console.log(promise)
输出promise
,并且把两个.then
的回调函数放到微任务队列。 - 继续执行同步任务
console.log(end)
,输出end
。 - 同步任务执行完毕,执行微任务
console.log(then1, then2)
,输出then1, then2
。 - 最后执行宏任务
console.log(setTimeout)
,输出setTimeout
。
复杂难度
console.log('start')
setTimeout(() => {
console.log('setTimeout')
new Promise((resolve) => {
console.log('promise2')
resolve()
}).then( () => {
console.log('then3')
})
}, 0)
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('then1')
}).then(() => {
console.log('then2')
})
console.log('end')
输出:start promise1 end then1 then2 setTimeout promise2 then3
解释:
- 首先执行同步任务
console.log(start)
,输出start
。 - 碰到
setTimeout
,将回调放入宏任务队列。 - 碰到
new Promise
,立即执行console.log(promise1)
输出promise1
,并且把两个.then
的回调函数放到微任务队列。 - 继续执行同步任务
console.log(end)
,输出end
。 - 同步任务执行完毕,执行微任务
console.log(then1, then2)
,输出then1, then2
。 - 执行宏任务
console.log(setTimeout)
,输出setTimeout
。 - 执行上一个宏任务中的
new Promise
,立即执行console.log('promise 2')
输出promise 2
,并把.then
放入微任务队列。 - 执行微任务
console.log(then3)
,输出then3
。
地狱难度
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
输出:1 7 6 8 2 4 3 5 9 11 10 12
解释:
- 首先执行同步任务
console.log(1)
,输出1
。 - 碰到
setTimeout
,将回调放入宏任务队列。 - 碰到
process.nextTick
,将函数放到微任务队列。 - 碰到
new Promise
,立即执行console.log(7)
输出7
,并且把.then
的回调函数放到微任务队列。 - 碰到
setTimeout
,将回调放入宏任务队列。 - 同步任务执行完毕,按顺序执行所有微任务,输出
6
,8
。 - 执行第一个
setTimeout
宏任务console.log(2)
,输出2
。 - 碰到
process.nextTick
,将函数放到微任务队列。 - 碰到
new Promise
,立即执行console.log(4)
输出4
,并且把.then
的回调函数放到微任务队列。 - 宏任务执行完毕,按顺序执行所有微任务,输出
3
,5
。 - 执行第二个
setTimeout
宏任务console.log(9)
,输出9
。 - 碰到
process.nextTick
,将函数放到微任务队列。 - 碰到
new Promise
,立即执行console.log(11)
输出11
,并且把.then
的回调函数放到微任务队列。 - 宏任务执行完毕,按顺序执行所有微任务,输出
10
,12
。
后记
最近在处理react-redux的useSelector和useState两个hook结合使用的问题时遇到了相关问题。在同一useEffect里的多次setState,且初始state和终了state一致时,并不会触发其他hook里的相应的dependency。使用new Promise(resolve => setTimeout(resolve, 0))后问题不再出现,就次引发了写下这篇笔记的动力。猜测可能和react的调和机制有关,需要更深入研究react的代码。