柚木

精读《Javascript 事件循环与异步》

本期精读的文章是:

How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await

1 引言

我为什么要选这篇文章呢?

sessionstack最近接连发了好几篇文章, 深入探讨JS, 以及 JS 中一些内部原理. 文中也讲到了, 伴随深入了解 JS 中的一些工作原理, 才有可能写出更好的代码和程序.

而 JS 中 Event Loop, 我的感觉就像 JS 中的一门内科, 我们平时只注意外科创伤,却忽视了内科问题往往容易莫名其妙的生病。了解 JS Event Loop 的原理,对 setTimeout Promise 这种基础概念不再浮在表层,可以写出更可靠的代码,如果你是前端新人,不要总是因为这个问题挂在一面 :p。

2 内容概要

从前我对 Event Loop 的理解也并不透彻,通过仔细阅读此文后, Event Loop 、宿主环境、js线程三者之间关系更加透明了,希望读者读完后也能有所体会。

文中 Promise、async/await 部分就忽略了,本篇重点介绍 Event Loop 。

Event Loop 与 Call Stack、Web APIs 之间的关系

原文通过 16 个图表达了 5 行代码的执行过程,太长就只贴第一张图了。

Call Stack 是调用栈,Event Loop 就是本期的主角 - 事件循环,Web APIs 泛指宿主环境,比如 nodejs 中的 c++,前端中的浏览器。

任何同步的代码都只存在于 Call Stack 中,遵循先进后出,后进先出的规则,也就是只有异步的代码(不一定是回调)才会进入 Event Loop 中,哪些是异步代码呢?比如:

setTimeout()
setInterval()
Promise.resolve().then()
fetch().then()

所有这些异步代码在执行时,都不会进入 Call Stack,而是进入 Event Loop 队列,此时 JS 主线程执行完毕后,且异步时机到了,就会将异步回调中的代码推入 Call Stack 执行。

而控制异步什么时机开始执行,是由宿主环境决定的,因为此时 js 主线程已经调用完毕,除非 Event Loop 队列有内容,推送到 Call Stack 中,否则 js 引擎也不会再执行任何代码。比如通过 fetch 发送请求,当 js 调用浏览器发送请求后,直到浏览器主动告诉 js 请求完成了,期间 js 是无法干预任何的。

最终效果如下 gif 图所示:

Microtask 与 Macrotask

Event Loop 处理异步的方式也分两种,分别是 setTimeout 之流的 Macrotask,与 Promise 之流的 Microtask

异步队列是周而复始循环执行的,可以看作是二维数组:横排是一个队列中的每一个函数,纵排是每一个队列。

Macrotask 的方式是将执行函数添加到新的纵排,而 Microtask 将执行函数添加到当前执行到队列的横排,因此 Microtask方式的插入是轻量的,最快被执行到的。

3 精读

Event Loop 内容不多,内容概要部分已经讲的比较彻底了,原文最后扯到了 Promise, async/await 的用法和注意点,不然是不会这么长的。

我最近写了一些 dob-react tests 测试文件,发现 componentWillMount 函数在 Microtask 时机 setState 不会触发 rerender:

class Hello extends React.Component {
	async componentWillMount() {
  	await immediate(()=>{
    	this.setState({a:1})
    })
  }

  render() { /**/ }
}

这种 immediate 函数的写法只会 render 一次:

function immediate(fn) {
    return new Promise(resolve => {
        fn()
        resolve()
    });
}

在线 Demo:http://jsfiddle.net/69z2wepo/90440/

如果再套一层 setTimeout,哪怕是一层 Promise, 就会 render 两次:

function immediate(fn) {
    return new Promise(resolve => Promise.resolve().then(() => {
        fn()
        resolve()
    }));
}

在线 Demo:http://jsfiddle.net/69z2wepo/90441/

ps: 感谢读者们的回复,其实第一个 immediate 函数根本就写错了,执行 fn 的时机是同步的,还是老老实实的使用 Promise().resolve().then() 吧。

4 总结

理解了事件循环之后,才是第一步,比如我就对 React 的生命周期中异步 setState 合并机制时而生效,时而不生效抱有疑问,所以想要写好稳健的业务代码还是挺难的,首先要理解这种 “内科” 知识,其次要读懂 react 源码,最后你还要保证不会忘。

讨论地址是:精读《Javascript 事件循环与异步》 · Issue #41 · dt-fe/weekly
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。