浏览器事件循环机制

126 阅读4分钟

JavaScript 本身是基于单线程的,这意味着在同一时间只能执行一个任务。这种设计主要是为了防止多个操作同时修改同一个数据导致数据不一致的问题,这样可以更好的处理用户界面的渲染和响应。

虽然 JavaScript 是单线程的,但是可以通过事件循环机制来处理异步操作,当一个函数执行时,它会将其他操作(定时器,网络请求等)放入一个任务队列,当主线程任务执行结束后,就会从任务队列取一个任务放入执行栈。

任务队列中的任务又分为 宏任务微任务

宏任务 主要包括

  • I/O
  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame(方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数)

微任务 主要包括

  • process.nextTick
  • MutationObserver(创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用)
  • Promise.then catch finally

事件循环执行的流程是:

  • 执行 宏任务(执行栈没有 宏任务 就从任务队列中取)
  • 执行过程中如果遇到 微任务,就将 微任务 放入到微任务的任务队列
  • 宏任务 执行结束,就立即执行当前微任务队列中的 微任务
  • 当前 宏任务 执行完毕,开始执行渲染,GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个 宏任务(从事件队列获取)

看看下面的执行顺序

console.log(1);

queueMicrotask(() => {console.log(2)});

Promise.resolve().then(() => console.log(3));

setTimeout(() => {console.log(4)})

分析:

  1. 第一行是同步代码直接放入执行栈,打印 1
  2. 第三行代码是微任务,放入微任务队列
  3. 第五行代码是微任务,放入微任务队列
  4. 第七行代码是宏任务,放入宏任务队列

此时执行栈没有任务在执行,然后去微任务队列中查是否有微任务,有微任务就把任务放入执行栈执行,打印23,此时就结束一轮循环,接着再去宏任务队列查是否有宏任务,将宏任务放入执行栈,打印 4

所以打印的顺序是 1234

可能有人会发现,为什么打印 1 之后,是去查微任务队列,而不是去查宏任务队列,上面不是说先查宏任务队列,再去查微任务队列吗?

这里是因为是将同步任务看作是一次宏任务队列的执行,也就是说 将打印 1 看作是执行的宏任务队列,所以接下来是去执行微任务,我们只需要记住 先执行宏任务再执行微任务

再看一个例子:

console.log(1);

setTimeout(() => console.log(2));

Promise.resolve().then(() => console.log(3));

Promise.resolve().then(() => setTimeout(() => console.log(4)));

Promise.resolve().then(() => console.log(5));

setTimeout(() => console.log(6));

console.log(7);

首先执行同步代码,打印 172 放入宏任务队列,3 放入微任务队列,4 是先将 setTimeout(() => console.log(4))放入微任务队列,5 放入微任务队列,6 放入宏任务队列,接着取微任务队列中的任务执行,打印354 放入宏任务队列,然后再将宏任务队列放入执行栈,打印26,这一轮的微任务队列清空了,接着执行下一轮循环,去宏任务队列取任务,打印 4,最终结果是 1735264

再看一个例子:

Promise.resolve().then(() => {
    // 微任务1
    console.log('Promise1')
    setTimeout(() => {
        // 宏任务2
        console.log('setTimeout2')
    }, 0)
})
setTimeout(() => {
    // 宏任务1
    console.log('setTimeout1')
    Promise.resolve().then(() => {
        // 微任务2
        console.log('Promise2')
    })
}, 0)

可以看到这段代码中没有同步代码,将 console.log('Promise1') 和 第四行的 setTimeout 放入微任务队列,将第二段代码的setTimeout中的函数体内容放入宏任务队列,先执行微任务,打印 Promise1,然后将 setTimeout2 放入宏任务队列,接着执行下面的宏任务,打印 setTimeout1,再执行和此轮循环的宏任务绑定的微任务队列中的任务,打印 Promise2 ,然后再去查询宏任务队列中的任务,打印 setTimeout2,最终执行顺序是:Promise1setTimeout1Promise2setTimeout2

再看一段代码:

console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
    setTimeout(() => {
        console.log('stack [4]')
        setTimeout(() => console.log("macro [5]"), 0);
        p.then(() => console.log('micro [6]'));
    }, 0);
    console.log("stack [7]");
});

console.log("macro [8]");

同步任务是 18,打印 1823放入宏任务队列,then 方法是微任务,里面的方法被放入到微任务队列,由于是循环3次,所以 7 被放入了三次,打印3次7,一轮循环结束,开始取宏任务队列中的任务放入执行栈执行,打印 23,这一轮没有微任务,接着进行下一轮循环,取宏任务队列中的任务,打印 45被放入宏任务队列,6是此轮循环微任务队列中的任务,接着打印 6,由于是循环三次,所以打印464646,此轮循环结束,接着取宏任务队列任务执行,此时宏任务队列中存了三个5,所以打印三次 5,最终结果:1877723464646555