浅谈 JavaScript 异步编程(二):JS 异步编程的基石

引擎与运行环境的“角逐”

我们再来看看刚才的这段代码,Node.js 在读取文件结束后将 Callback 交给 JS 引擎执行,但这其中有一个问题:运行环境交还 Callback 的时机如何确定?

如果运行环境在任务结束时立刻将 Callback 交还引擎,而此时引擎很可能正在执行一个函数的中途,那么强行插入执行的 Callback 很可能会导致一些难以预料的后果。

例如这段代码,setTimeout 是运行环境提供的一个定时器,我们这里将定时器设为 0,也就是说运行环境得到这个任务后可以立即完成。假如运行环境完成后立即塞回引擎执行,那么counter is 0既可能被打印一次,也可能被打印两次。

JS 作为一门单线程语言竟然会出现多线程并发问题!

事件循环与任务队列

幸运的是,上述的情况是不可能在 JS 中发生的。为了让引擎和运行环境在恰当的时间点交互,运行环境都采用了一套基于事件循环与任务队列的设计。

上图为浏览器中的事件循环与任务队列模型,它最初由 HTML5 标准制定,后来 ES6 标准又作出了修改。

此模型除了引入事件循环和任务队列的概念,还规定了一个基本原则:每当引擎的 stack 被清空后才运行事件循环清空任务队列,由此往复。 先前的代码在 for 中执行时 stack 始终不为空,所以运行结果一定是:counter is 0只被打印了一次。

下面我们通过一个具体的例子来理解这个模型:

小结

事件循环与任务队列的引入为 JS 执行异步代码提供了一个安全的、稳定的、可预测的环境,成为了 JS 异步编程的基石。

另外,ES6 标准中提出了宏任务与微任务的概念,并对运行环境提出了进一步的要求,例如不完成所有微任务就不能进入下一次事件循环等,让 JS 在拓展异步编程的功能时有了更为安全可靠的保障。

接下来让我们继续前进,探索 JS 异步编程的发展轨迹。

链接