网站首页 » 前端开发 » JavaScript » JavaScript 事件循环运行机制解密
上一篇:
下一篇:

JavaScript 事件循环运行机制解密

前言

JavaScript 是单线程,这个地球人都知道,但 JavaScript 却可以做到类似多线程的感觉,这是怎么实现的呢,比如:你的程序中通过 ajax 发起了一个请求,此时你会发现,即使请求还没有返回结果,程序竟然继续往下执行了,JavaScript 的魅力就在于此,虽然我是单线程,但我可以让你有“多线程”的感觉。

上面说到的“多线程”的感觉其实就是通过 JavaScript 的事件循环机制实现的。这也就是事件循环在 JavaScript 中称王称霸(如果你想学好 JavaScript 你就必需深入了解事件循环)的原因。

我理解的事件循环由两个部分组件:

  • 1、循环(动词)
  • 2、事件(名词)
  • 3、重复上面步骤1和步骤2

循环就是循环,它跟我们的常识里的循环是同一个意思。

事件就是事件,它跟我们的常识里的事件是同一个意思。

上面两句话说了不等于白说吗?但我觉得没白说,至少会让你知道循环、事件也就那么回事,没什么特别的,即使把它们有机的撮合在一起。

在 JavaScirpt 中任务分为同步任务和异步任务,同步任务会直接放到执行栈(主线程)中依次执行,异步任务则会在同步全部任务执行完后再执行(并且每一次事件循环都只会执行队列中的首个任务)。异步任务(task),可以分为 micro-task 和 macro-task。

micro-task 包括:

  • process.nextTick
  • Promises
  • Object.observe
  • MutationObserver

macro-task 包括:

  • script 代码
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

换句话说,属于 process.nextTick, Promises, Object.observe, MutationObserver 的任务都会放到 micro-task 任务队列中。属于 script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering 会放到 macro-task 队列中。

事件循环就是在 micro-task 和 macro-task 这两个队列之间不间断地来回执行它们中的任务。

但是事件循环机制会先把 micro-task 队列中的所有任务依次放到执行栈中执行,执行完之后,才会去 macro-task 中取出队首任务(第一个任务,注意不是全部,只是一个),放到执行栈中执行,执行完了这个之后,就又回到 micro-task 中,执行 micro-task 里的所有任务(如果有的话),执行完之后,接着又去 macro-task 取出队首任务,放到执行栈中执行,如此循环。这个就是传说中的事件循环。上面只是热身,下面才是本文的重点。

精彩演绎

下面我们来看看这个看似复杂,实则非常简单的实现过程。

  • 1、事件分拣
  • 2、macro-task 首事件入栈(执行栈)并执行
  • 3、micro-task 所有事件入栈(执行栈)并执行
  • 4、重复步骤1、步骤2 和步骤3

下面是一个简单的示例:

console.log(new Date().getTime(),"start"); // 行1

setTimeout(function(){ // 行2
    console.log(new Date().getTime(),"setTimeout");
},0);

// 同步阻塞一秒后再往下执行
const ms= new Date().getTime() + 1000;
for(var i=0; i < ms;i = new Date().getTime()){} // 行3

new Promise(function(resolve){ // 行4
    resolve();
    console.log(new Date().getTime(),"Promise"); // 行5
}).then(function(){ // 行6
    console.log(new Date().getTime(),"Promise-then"); // 行7
});

setImmediate(function() { // 行8
    console.log(new Date().getTime(),'setImmediate'); // 行9
    new Promise(function(resolve){
        resolve();
        console.log(new Date().getTime(),"setImmediate-Promise"); // 行10
    }).then(function(){ // 行11
        console.log(new Date().getTime(),"setImmediate-Promise-then"); // 行12
    });
})

console.log(new Date().getTime(),"end"); // 行13

程序一进来,先把上面的代码归为 script 代码,并把它放到 macro-task 任务队列中,视为任务队列中的一个任务。此时 macro-task 中有且只有一个任务,即上面的 script 代码。

事件循环开始,首先从 macro-task 中取出这个任务(script 代码)放到执行栈中执行,一行一行的从上往下执行。当遇到同步代码(比如: console.log())时,直接执行,而遇到异步代码时就会暂时把它挂起,将它交给对应的处理模块去处理。

我们不妨用这个逻辑来分析上面的代码。

第一轮事件循环

1、执行 macro-task 中的一个任务(首个任务)

遇到行1 ,代码直接执行,遇到行2 把它挂起,继续往下执行,行3 代码, for 循环同步阻塞一秒后再继续往下执行,到行4 直接执行 Promise 里的回调,执行行5 代码,完了之后把行6 (then(……) 挂起),遇到行8 ,此代码为异步操作,二话不说直接挂起,往下走,直接执行行13 代码,在这个过程当中完成了同步代码的执行,事件的分拣归类。值得注意的是 for 循环既不属于 micro-task 也不属于 macro-task,因为它是一个同步任务,所以 macro-task 中首个任务执行完后,分拣结果如下:

  • micro-task  中有一个 .then() 的异步任务
  • macro-task 有一个 异步任务,分别是  setTimeout() 和 setTmmediate()

2、执行 micro-task 中的所有任务

由于通过前面一轮的分拣归类后,micro-task 中只有一个任务 then(……) ,所以执行完 .then() 中的回调函数后 micro-task 中就已经没有其它任务了,即 micro-task 中的所有任务已经执行完毕。

过程1、过程2 执行完,第一轮事件循环结束。进入第二轮事件循环,第二轮事件循环跟第一轮事件循环一样。

第二轮事件循环

1、从 macro-task 队列中取出首个任务(setTimeout() )并把与之对应的回调函数放到执行栈中执行。

2、执行 micro-task 任务队列中的所有任务(在这轮事件循环中,micro-task 已没有任务了)。

第三轮事件循环

1、从 macro-task 队列中取出首个任务(setTmmediate() )并把与之对应的回调函数放到执行栈中执行,直接执行行9 代码,接着往下走,遇到 Promise(),先执行 Promise() 中的回调,执行行10 代码,再把 .then() 挂起。此时:micro-task 任务队列中又有了一个 任务 .then(……)。

2、执行 micro-task 任务队列中的所有任务(此轮循环中只有 .then(……) 一个任务)。

然后再进入下一轮事件循环……,一直这样循环下去,即使 micro-task 和 macro-task 中都没有任务了,事件循环也不会停止。

micro-task 比 macro-task 的优先级高,只有  micro-task 任务队列中的任务全部执行完之后,才会进入下一轮事件循环。

执行结果:

// 第一轮
1525419172225 start
1525419173226 Promise
1525419173226 end
1525419173226 Promise-then
// 第二轮
1525419173233 setTimeout
// 第三轮
1525419173236 setImmediate
1525419173236 setImmediate-Promise
1525419173236 setImmediate-Promise-then
注意

setTmmediate()  方法最近刚刚被微软提出, 可能不会被w3c批准成为标准, 目前只有 Internet Explorer 10 实现了该方法,所以需要在 IE 最新的浏览器 Edge 中运行

在示例中我们把 setTimeout() 的延迟时间写成 0 即使它不是放在代码的最后,但它却是示例中最后执行打印的,而 .then() 中的回调会在 setTimeout() 之前执行,这也就印证了之前所说的任务归类及两个任务( micro-task 和 macro-task)之间的优先级猜想,注意 Promise() 中的回调函数为同步调用。

如果你能耐得住寂寞,把精彩演绎部分看明白了,那么你就发现事件循环也就那么回事,没什么了不起的。

有关“事件循环” 类文章在网上多不胜数,很多文章把高逼格的图片都献上尝试用图片来说明事件循环的原理,我觉得没多大用处,看完之后,更让人迷惑不解,更让人摸不着头脑。还不如静下心来读读本文,感爱文字的力量。

 

  • 微信扫一扫,赏我

  • 支付宝扫一扫,赏我

声明

原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

本文永久链接:http://yunkus.com/javascript-event-loop/

发表评论

电子邮件地址不会被公开。 必填项已用*标注

评论 END