JavaScript Eventloop | 事件循环机制详解

发布于 2022-03-11  63 次阅读


Eventloop机制是Javascript实现伪“多线程”的关键所在。这一部分也是面试常常会被问到的问题,做个笔记以备查询。

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,将回调放入宏任务队列。
  • 同步任务执行完毕,按顺序执行所有微任务,输出68
  • 执行第一个setTimeout宏任务console.log(2),输出2
  • 碰到process.nextTick,将函数放到微任务队列。
  • 碰到new Promise,立即执行console.log(4)输出 4,并且把.then的回调函数放到微任务队列。
  • 宏任务执行完毕,按顺序执行所有微任务,输出35
  • 执行第二个setTimeout宏任务console.log(9),输出9
  • 碰到process.nextTick,将函数放到微任务队列。
  • 碰到new Promise,立即执行console.log(11)输出 11,并且把.then的回调函数放到微任务队列。
  • 宏任务执行完毕,按顺序执行所有微任务,输出1012

后记

最近在处理react-redux的useSelector和useState两个hook结合使用的问题时遇到了相关问题。在同一useEffect里的多次setState,且初始state和终了state一致时,并不会触发其他hook里的相应的dependency。使用new Promise(resolve => setTimeout(resolve, 0))后问题不再出现,就次引发了写下这篇笔记的动力。猜测可能和react的调和机制有关,需要更深入研究react的代码。