浏览器事件循环与node事件循环

浏览器事件循环与node事件循环

前言

最近看到一些关于 事件队列,浏览器执行机制的文章推荐,联想到很早以前遇到的一些面试题,才惊觉自己对这块一直都不怎么了解,借助这个机会好好记录一番。顺便感叹一句,阮一峰大神的 blog真是应有尽有,好好搜索一番就能找到想要的文章。还有每周前言科技推送,非常值得关注,怪不得能被封神。

一道题目

setTimeout(()=>{
    console.log('A');
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log('B')
        },0);
        return new Promise(function (resolve) {
            console.log('C');
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log('D')
});
console.log('E');

这道题的运行结果是:

C
E
D 
A
B

在讲解这道题目之前,先说下这道题涉及到一些的概念:(此题讲解在最后)

  • 同步任务与异步任务
  • 异步任务类型
  • 事件循环
  • macroTask与microTask

同步任务 VS 异步任务

这两个词相信所有的前端工程师都听过,并且最早接触前端的时候也没分过同步和异步。直到开始接触ajax请求,了解了异步这回事情,才直到与之相对的就是同步。

但到底什么是同步,什么是异步呢?

这里说下我的理解方式,线性执行下去的任务就是同步任务。而需要等待一定时间延后执行的就是异步任务。(这只是便于理解的简单解释)

官方解释是:

1)同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

2)异步任务:不进入主线程,而进入“任务列队”的任务,只有等主线程任务执行完毕,“任务队列”开始通知主线程,请求执行任务,该任务才会进入主线程执行。

这里有需要注意的是:

a. 同步任务与异步任务在一起时,主线程总是要等同步任务执行完成后,才有空闲去执行异步任务的“任务队列”中的任务。( js的单线程的体现 )

console.log('同步任务开始');
// 异步任务
setTimeout(() => {
   console.log('异步任务开始');
}, 0);
for(var i = 0; i < 10; i++ ) {
  console.log(i);
}

在这个例子中,异步任务就是setTimeout,当浏览器解释到这一句的时候,发现其是异步任务,将其回调函数放入task queue,等待执行。所以会先打印最后的0 ~9 ,当执行完同步任务后,主线程开始空闲,于是去执行任务队列中的task。

异步任务类型

Javascript是单线程运行,异步操作特别重要。为了协调异步任务,Node提供了四个定时器,让任务可以在指定的时间运行。

  • setTimeout (clearTimeout)
  • setInterval (clearInterval )
  • setImmediate (clearImmediate)
  • process.nextTick
  • ....

前两个定时器都是比较常用的,setTimeout是在指定时间段以后执行回调,setInterval是在指每间隔一段时间,就去执行一个任务。x

process.nextTick可以在当前“执行栈”的尾部,下一次Event Loop之前触发回调函数。即,它指定的任务总是在所有异步任务之前。

setImmediate也是一个定时任务,它总是在“任务队列”的尾部添加事件,即它指定的任务总是在下一次Event Loop执行时。

事件循环(event loop)

每一个“线程”都有一个独立的event loop, 每一个web worker也有一个独立的event loop, 所以它可以独立的运行。

事件循环的示意图

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。


Macrotask VS MicroTask

macroTask在有些文章中称为Task,这里为了和microTask形成一致性的对应,采用macroTask一词。

microTask指的就是事件循环中的“任务队列”中的task。

microtask通常来说就是在当前主线程任务执行结束后立即执行的任务,比如需要对一系列的任务作出回应,或者是需要异步的执行任务而又不需要分配一个新的task, 这样可以减小一点性能开销。

microtask与macrotask任务队列是相互独立的队列。每一个macrotask中产生的microtask都将会添加到microtask队列中。microtask中产生的microtask将会添加至当前队列的尾部,并且 microtask 会按序的处理完队列中的所有任务。

microtask 包括:process.nextTick , promise, Object.observer, MutationObserver

macrotask 包括(就是tasks):setTimeout, setInterval, setImmediate, I/O, UI 渲染

microtask 和 macrotask 并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:

  1. event-loop start
  2. microTasks 队列开始清空(执行)
  3. 检查 macrotask(Tasks) 是否清空,有则跳到 4,无则跳到 6
  4. 从 macrotask(Tasks) 队列抽取一个任务,执行
  5. 检查 microTasks 是否清空,若有则跳到 2,无则跳到 3
  6. 结束 event-loop

console.log('script start');
// macroTask
setTimeout(function() {
  console.log('setTimeout');
  // macroTask中的microTask会添加至 microTask队列中,等待下次执行
  process.nextTick(() => {
  	console.log('nextTick1');
  });
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
  // microTask中的microTask会添加至 当前microTask队列的尾部,在下一个macroTask执行之前执行
  process.nextTick(() => {
  	console.log('nextTick2');
  });
}).then(function() {
  console.log('promise2');
});

结果是:

script start
script end
promise1
promise2
nextTick2
setTimeout
nextTick1

题目解析

如果上面的例子都能够理解的话,相信第一题的答案就能够脱口而出了,现在可以回到第一题,再细细品以下

如果还是有困难的话,请接着往下看:

// macroTask
setTimeout(()=>{
    console.log('A');
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log('B')
        },0);
        // microtask
        return new Promise(function (resolve) {
            // 同步任务
            console.log('C');
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log('D')
});
// 同步任务
console.log('E');

由于同步任务优先于异步任务执行,所以C在obj.func()执行的时候输出,然后是E。

其实解释器在先执行第一段的setTimeout时,发现其是一个异步任务,故将其放入macrotask队列中,在执行obj.func()时,又有一个setTimeout,同样的将其放入 macrotask队列中,根据先进先出原则,先输出的是A,然后是B。对于promise函数来说,它是一个microTask,所以先于macrotask执行,故先输出D。

所以顺序就是: C,E,D,A,B

后记

在学习事件队列,microtask, macrotask的时候,查阅了很多技术文章,发现了阮一峰大神文章下面的评论,也发现了朴灵老师的犀利批评。这里我不发表对阮老师和朴灵老师的评价,只是告诫下大家,技术日新月异,具有实效性,希望大家在查阅文章的时候,能够积极的实践下。

阮老师的文章更多的是科普性质的,并且他非常高产,也经常性的查看外文资料。不过技术扎实性可能会有些偏差,不过这不影响他的封神,本来对于技术的理解每个人都是不一样的。朴灵老师的《深入浅出nodeJS》一书中也讲述了事件循环,不过更多的是用精简的语言描述,当然每句话中都有很多的深奥的专业术语。比如“红黑树”,“观察者”等等,如果真正要吃透,就需要花非常多的时间。

大家各取所需吧,并且标准是标准,在浏览器厂商实现方面可能又会有一些偏差。所以个人推荐还是“实践出真知吧”!


参考

深入理解 JavaScript 事件循环(一)- event loop

[译] 深入理解 JavaScript 事件循环(二)- task and microtask

Node 定时器详解 - 阮一峰的网络日志

JavaScript 运行机制详解:再谈Event Loop

笔试题--JavaScript事件循环机制(event loop、macrotask、microtask)

深入理解 Event Loop
【第1405期】浏览器的 Event Loop

js中的同步和异步的个人理解 - YinghaoGuo的博客 - CSDN博客

什么是浏览器的事件循环(Event Loop)?

编辑于 2018-10-07

文章被以下专栏收录

    知乎编程专栏里更值得关注和学习的web前端开发专栏!专栏主要技术栈:JavaScript,HTML,CSS,nodejs,Vuejs,reactjs,linux,Java,不辣不辣等各种开发工具

    学习算法