JS引擎的执行机制:探究EventLoop(含Macro Task和Micro Task)

在我看来理解好JS引擎的执行机制对于理解JS引擎至关重要,今天将要好好梳理下JS引擎的执行机制。

首先解释下题目中的名词:(阅读本文后你会对这些概念掌握了解)

Event Loop:事件循环
Micro Task:微任务
Macro Task:宏任务

阅读本文前,我们要知道两个重点。

(1) JS是单线程语言

(2) JS的Event Loop是JS的执行机制。深入了解JS的执行,就等于深入了解JS里的event loop

一、JS为什么是单线程语言。

javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

二、什么是任务队列?

2.1 同步任务与异步任务

单线程就意味着,所有任务需要排队。所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。异步执行的运行机制如下:

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

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

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

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

2.2 JS引擎执行模型

从宏观角度讲, js 的执行是单线程的. 所有的异步结果都是通过 “任务队列(Task Queue)” 来调度被调度. 消息队列中存放的是一个个的任务(Task). 规范中规定, Task 分为两大类, 分别是 Macro Task(宏任务) 和 Micro Task(微任务), 并且每个 Macro Task 结束后, 都要清空所有的 Micro Task.

宏观上讲, Macrotask 会进入 Macro Task Queue, Microtask 会进入 Micro Task Queue。而 Micro Task 被分到了两个队列中. ‘Micro Task Queue’ 存放 Promise 等 microtask. 而 ‘Tick Task Queue’ 专门用于存放 process.nextTick 的任务.现在先来看看规范怎么做的分类.

  • Macrotask 包括:
    • setImmediate
    • setTimeout
    • setInterval
  • Microtask 包括:
    • process.nextTick
    • Promise
    • Object.observe
    • MutaionObserver

所说的, ‘每个 Macro Task 结束后, 都要清空所有的 Micro Task‘. 引擎会遍历 Macro Task Queue, 对于每个 Macro Task 执行完毕后都要遍历执行 Tick Task Queue 的所有任务, 紧接着再遍历 Micro Task Queue 的所有任务. (nextTick 会优于 Promise执行)

三、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

三种任务队列中的代码执行流程图如下:

来个例子检验一下吧:

console.log('main1');

process.nextTick(function() {
    console.log('process.nextTick1');
});

setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);

new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});

console.log('main2');

分析如下

  1. 开始执行代码,输出 main1,process.nextTick 放入tickTaskQueen,setTimeout放入 macroTaskQueen, new Promise 执行 输出 promise,then 方法 放入 MicroTaskQueen , 接着 最后一行代码 console.log 输出 main2
  2. 当前的 宏任务执行完毕,开始清空微任务,先清空tickTaskQueen ,执行 console.log('process.nextTick1'); 输出'process.nextTick1;再清空MicroTaskQueen执行 console.log('promise then'); 输出promise then;微任务全部清空。
  3. 开始下次 eventLoop; 执行 setTimeout; 第一行 console.log('setTimeout'); 输出setTimeout; process.nextTick 将任务放入了tickTaskQueen;当前宏任务执行完毕;开始清空MicroTaskQueen,清空tickTaskQueen ,执行 console.log('process.nextTick2');输出process.nextTick2;

四、Node.js中的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTicksetImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。

猜你喜欢

转载自www.cnblogs.com/wuguanglin/p/EventLoop.html