关于javascript:搞懂JS的事件循环Event-Loop和宏任务微任务

7次阅读

共计 5791 个字符,预计需要花费 15 分钟才能阅读完成。

在之前的一篇文章中简略理了下 JS 的运行机制,顺着这条线深刻就又遇到了几个概念,什么是事件循环,什么又是宏工作、微工作呢,明天用这篇文章梳理一下。
以下是我本人的了解,如有谬误,还望不吝赐教。

事件循环与音讯队列

首先大家都晓得 JS 是一门单线程的语言,所有的工作都是在一个线程上实现的。而咱们晓得,有一些像 I /O,网络申请等等的操作可能会特地耗时,如果程序应用 ” 同步模式 ” 等到工作返回再继续执行,就会使得整个工作的执行特地迟缓,运行过程大部分事件都在期待耗时操作的实现,效率特地低。

为了解决这个问题,于是就有了 事件循环(Event Loop)这样的概念,简略来说就是在程序自身运行的主线程会造成一个 ” 执行栈 ”,除此之外,设立一个 ” 工作队列 ”, 每当有异步工作实现之后,就会在 ” 工作队列 ” 中搁置一个事件,当 ” 执行栈 ” 所有的工作都实现之后,会去 ” 工作队列 ” 中看有没有事件,有的话就放到 ” 执行栈 ” 中执行。

这个过程会一直反复,这种机制就被称为事件循环(Event Loop)机制。

宏工作 / 微工作

宏工作能够被了解为每次 ” 执行栈 ” 中所执行的代码,而浏览器会在每次宏工作执行完结后,在下一个宏工作执行开始前,对页面进行渲染,而宏工作包含:

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI 交互事件
  • postMessage
  • MessageChannel
  • setImmediate
  • UI rendering

微工作, 能够了解是在以后 ” 执行栈 ” 中的工作执行完结后立刻执行的工作。而且早于页面渲染和取工作队列中的工作。宏工作包含:

  • Promise.then
  • Object.observe
  • MutaionObserver
  • process.nextTick

他们的运行机制是这样的:

  • 执行一个宏工作(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微工作,就将它增加到微工作的工作队列中
  • 宏工作执行结束后,立刻执行以后微工作队列中的所有微工作(顺次执行)
  • 以后宏工作执行结束,开始查看渲染,而后 GUI 线程接管渲染
  • 渲染结束后,JS 线程持续接管,开始下一个宏工作(从事件队列中获取)

在理解了宏工作和微工作之后,整个 Event Loop 的流程图就能够用上面的流程图来概括:

例子

如无非凡阐明,咱们用 setTimeout 来模仿异步工作,用 Promise 来模仿微工作。

主线程上有宏工作和微工作

console.log('task start');

setTimeout(()=>{console.log('setTimeout')
},0)

new Promise((resolve, reject)=>{console.log('new Promise')
    resolve()}).then(()=>{console.log('Promise.then')
})

console.log('task end');

//---------------------- 执行后果 ----------------------
// task start
// new Promise
// task end
// Promise.then
// setTimeout

这个例子比较简单,就是在主工作上加了一个宏工作(setTimeout),加了一个微工作(Promise.then), 看执行的程序,打印出了主工作的 task start、new Promise、task end,主工作实现,接下来执行了微工作的 Promise.then,到此第一轮事件循环完结,去工作队列里取出了 setTimeout 并执行。

在微工作中增加宏工作和微工作

跟上个例子相比,咱们在 Promise.then 里加上一个 setTimeout 和一个 Promise.then。

console.log('task start');

setTimeout(()=>{console.log('setTimeout1')
},0)

new Promise((resolve, reject)=>{console.log('new Promise1')
    resolve()}).then(()=>{console.log('Promise.then1')
    setTimeout(()=>{console.log('setTimeout2')
    },0)
    new Promise((resolve, reject)=>{console.log('new Promise2')
        resolve()}).then(()=>{console.log('Promise.then2')
    })
})

console.log('task end');

//---------------------- 执行后果 ----------------------
// task start
// new Promise1
// task end
// Promise.then1
// new Promise2
// Promise.then2
// setTimeout1
// setTimeout2

猜对了么,失常的主工作没有变动,只是在执行第一次微工作的时候,发现了一个宏工作,于是被加进了工作对了。遇到了一个微工作,放到了微工作队列,执行完之后又扫了一遍微工作队列,发现有微工作,于是接着执行完微工作,到这,第一遍事件循环才完结,从工作队列里拿出了两次 setTimeout 执行了。

在异步宏工作中增加宏工作和微工作

其余无异,把方才增加到 Promise.then 中的内容增加到 setTimeout 中。

console.log('task start')

setTimeout(()=>{console.log('setTimeout1')
    setTimeout(()=>{console.log('setTimeout2')
    },0)
    new Promise((resolve, reject)=>{console.log('new Promise2')
        resolve()}).then(()=>{console.log('Promise.then2')
    })
},0)

new Promise((resolve, reject)=>{console.log('new Promise1')
    resolve()}).then(()=>{console.log('Promise.then1')
})

console.log('task end')

//---------------------- 执行后果 ----------------------
// task start
// new Promise1
// task end
// Promise.then1
// setTimeout1
// new Promise2
// Promise.then2
// setTimeout2

第一遍主工作执行大家都很明确了,到 Promise.then1 完结,而后取工作队列中的 setTimeout,执行过程中又发现了一个 setTimeout,放到工作队列中,并且发现一个 Promise.then2,把这个微工作执行完之后,第二遍事件循环才完结,而后开始第三遍,打印出了 setTimeout2。

退出事件冒泡

事件循环遇到事件冒泡会产生什么?

<div class="outer">
  <div class="inner"></div>
</div>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

function onClick() {console.log('click');
  
  setTimeout(function() {console.log('setTimeout');
  }, 0);

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

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

点击 inner,后果:

click        //inner 的 click
promise        //inner 的 promise
click        //outer 的 click
promise        //outer 的 promise
timeout        //inner 的 timeout
timeout        //outer 的 timeout

我感觉解释应该是这样的:
1、开始执行,因为事件冒泡的缘故,事件触发线程会将向上派发事件的工作放入工作队列。接着执行,打印了 click, 把 timeout 放入工作队列,把 promise 放入了微工作队列。
2、执行栈清空,check 微工作队列,发现微工作,打印 promise,第一遍事件循环完结。
3、从工作队列里取出工作,执行 outer 的 click 事件,打印 click, 把 outer 的 timeout 放入工作队列,把 outer 的 promise 放入了微工作队列。执行 inner 放入工作队列的 timeout。
4、执行栈清空,check 微工作队列,发现微工作,打印 promise,第二遍事件循环完结。
5、从工作队列里取出工作,把 timeout 打印进去。

JS 触发下面的 click 事件

一样的代码,只不过用 JS 触发后果就会不一样。
对代码做了稍稍扭转,将 click 拆分成两个办法,不便追踪是谁被触发了。

var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

const onInnerClick = (e) => {console.log('inner cilcked');

  setTimeout(function() {console.log('inner timeout');
  }, 0);

  Promise.resolve().then(function() {console.log('inner promise');
  });
}

const onOuterClick = (e) => {console.log('outer clicked');

  setTimeout(function() {console.log('outer timeout');
  }, 0);

  Promise.resolve().then(function() {console.log('outer promise');
  });
}

inner.addEventListener('click', onInnerClick);
outer.addEventListener('click', onOuterClick);

inner.click();

执行后果:

inner cilcked
outer clicked
inner promise
outer promise
inner timeout
outer timeout

之所以会呈现这样的差别,我的了解是 JS 代码执行中的 click 事件,散发了一个同步的冒泡事件。所以在第一个 click 事件完结之后,调用栈中有 outer 的 click 事件,所以呈现了两个间断的 click。

这也是依据后果猜想过程,心里没底。

在 node 环境中执行

退出 node 环境特有的 process.nextTick,再看上面这个例子:

console.log(1);
setTimeout(() => {console.log(2);
  process.nextTick(() => {console.log(3);
  });
  new Promise((resolve) => {console.log(4);
    resolve();}).then(() => {console.log(5);
  });
});
new Promise((resolve) => {console.log(7);
  resolve();}).then(() => {console.log(8);
});
process.nextTick(() => {console.log(6);
});
setTimeout(() => {console.log(9);
  process.nextTick(() => {console.log(10);
  });
  new Promise((resolve) => {console.log(11);
    resolve();}).then(() => {console.log(12);
  });
});

以上代码会有两个后果
node <11: 1 7 6 8 2 4 9 11 3 10 5 12
node>=11: 1 7 6 8 2 4 3 5 9 11 10 12

NodeJS 中微队列次要有 2 个:

  • 1.Next Tick Queue:是搁置 process.nextTick(callback)的回调工作的
  • 2.Other Micro Queue:搁置其余 microtask,比方 Promise 等

在浏览器中,也能够认为只有一个微队列,所有的 microtask 都会被加到这一个微队列中,然而在 NodeJS 中,不同的 microtask 会被搁置在不同的微队列中。

Node.js 中的 EventLoop 过程:

  • 1. 执行全局 Script 的同步代码
  • 2. 执行 microtask 微工作,先执行所有 Next Tick Queue 中的所有工作,再执行 Other Microtask Queue 中的所有工作
  • 3. 开始执行 macrotask 宏工作,共 6 个阶段,从第 1 个阶段开始执行相应每一个阶段 macrotask 中的所有工作,留神,这里是所有每个阶段宏工作队列的所有工作,在浏览器的 Event Loop 中是只取宏队列的第一个工作进去执行,每一个阶段的 macrotask 工作执行结束后,开始执行微工作,也就是步骤 2
  • 4.Timers Queue -> 步骤 2 -> I/O Queue -> 步骤 2 -> Check Queue -> 步骤 2 -> Close Callback Queue -> 步骤 2 -> Timers Queue ……

**Node 11.x 新变动
当初 node11 在 timer 阶段的 setTimeout,setInterval… 和在 check 阶段的 immediate 都在 node11 外面都批改为一旦执行一个阶段里的一个工作就立即执行微工作队列。为了和浏览器更加趋同.**

参考资料:
什么是 Event Loop?
Tasks, microtasks, queues and schedules
js 中的宏工作与微工作

正文完
 0