本系列一共七章,Github 地址请查阅这里,原文地址请查阅这里。javascript
本章会阐述浏览器端执行异步代码的各类不一样方法。你将会了解到关于事件循环和定时技术好比 setTimeout 和 Promises 之间的差异。html
大多数人可能对诸如 Promise
,process.nextTick()
,setTimeout
,或许还有 requestAnimationFrame
的异步执行代码比较熟悉。它们内部都是使用的事件循环,可是在精准的定时上面,他们表现得很不同。前端
本文,将会阐述其中的不一样,而后让你明白如何去实现一个现代框架好比 NX 所须要的定时系统。与其重复造轮子,咱们将会使用原生的事件循环来实现咱们的目标。html5
事件循环在 ES6 规范 中并无的说起。JavaScript 自己只有任务和任务队列的概念。更复杂的事件循环的概念分别定义在 NodeJS 和 HTML5 规范中。由于此系列是讲的前端,因此在这里我将会阐述后者。java
事件循环被称为一个循环,是有一个缘由的。它无限循环并寻找新任务来执行。该循环的单次遍历叫作一个 tick。在tick 期间执行的代码叫一个任务(task)。react
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
复制代码
任务是指能够在循环中安排其余任务的同步代码片断。调度新任务的一种简单的编程方法是 setTimeout(taskFn)
。git
然而,任务可能来自其它几种来源,好比用户事件,网络请求或者 DOM 操做。es6
更复杂的就是,事件循环能够拥有多个任务队列。惟一的两个限制便是来自同一个任务源的事件必须属于同一个任务队列,而且每一个队列中的全部的任务必须按插入的顺序执行。除了这些,用户代理能够随意作任何操做。例如,它能够决定下一个执行的任务队列。github
While (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
}
复制代码
依据这个模型,咱们就失去了对时间的精确控制。在执行咱们用 setTimeout()
规划的任务以前,浏览器可能决定先彻底处理其它的几个任务队列。web
幸运的是,事件循环还有一个单一队列叫作微任务队列。在每一个 tick 之中,每当当前任务执行完毕以后,微任务队列都被彻底清空。
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
}
复制代码
设置一个微任务的最简单的方式是 Promise.resolve().then(microtaskFn)
。微任务会按插入的顺序执行,由于只有一个微任务队列,因此用户代理此次不能干扰咱们。
另外,微服务能够设置新的微服务插入到同一个微服务队列中,而且在同一个 tick 中执行。
最后一个须要注意的是渲染的时间表。不像事件处理或者解析,渲染不是由独立的后台任务来完成的。这是一种算法,能够运行在每个循环 tick 结束。
用户代理又有不少自由的选择:它可能在每一个任务以后渲染,可是它可能会决定执行成百上千的任务而不去渲染。
幸运的是有 requestAnimationFrame
函数,它会在下次渲染以前立刻执行传入的函数。咱们最终的事件循环模型以下所示:
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
if (shouldRender()) {
applyScrollResizeAndCSS()
runAnimationFrames()
render()
}
}
复制代码
如今让咱们利用全部这些知识来构建一个定时系统!
和大多数现代框架同样,NX 在后台处理 DOM 操做和数据绑定。它批量操做并异步执行它们以提高性能。为了让这些操做按正确地时序执行,它依赖于 Promises
,MutationObservers
和 requestAnimationFrame()
。
预想的定时系统以下:
NX 同时使用 ES6 Proxies 来注册一个对象变更和使用 MutationObserver 来注册 DOM 变更(下一章将更多介绍这些内容)。它会延迟响应,做为微任务的第二步以提高性能。延迟响应对象变化是使用 Promise.resolve().then(reaction)
来实现的,而后由 MutationObserver 来自动处理,由于 MutationObserver 内部使用微任务。
开发者的代码(任务)运行结束。由 NX 注册的微任务响应开始执行。微任务是按顺序执行的。注意此时咱们仍然在同一个循环 tick 中。
NX 使用 requestAnimationFrame(hook)
来运行由开发者传过来的钩子。这也许会发生在以后的循环 tick 之中。重要的是,这里的钩子会在下一次渲染以前和全部的数据,DOM 和 CSS 的更改都执行以后运行。
浏览器渲染接下来的视图。这也许会发生在以后的循环 tick 之中,可是它毫不可能发生在一个 tick 的前几个步骤以前。
咱们只是在原生的事件循环之上实现了一个简单但可用的定时系统。理论上会运行得很好,可是定时是一个很微妙的东西,一个微小的错误可能会致使一些很是奇怪的 bug。
在一个复杂的系统之中,设置一些关于定时的规则,而且在以后遵照它们是很是重要的。Nx 制定了以下规则。
setTimeout(fn, 0)
对数据和 DOM 操做的响应,应该按照操做的发生顺序来执行。只要他们的执行顺序没有混淆,延迟它们的执行是可取的。把执行顺序搞混淆会让事情变得不可预知和难以找出缘由。
setTimeout(fn, 0)
是彻底不可预知的。用不一样的方法注册微任务也会致使混淆执行顺序。例如,在下面的示例中, microtask2
将会错误地在 microtask1
以前执行。
Promise.resolve().then().then(microtask1)
Promise.resolve().then(microtask2)
复制代码
把开发者代码执行时间窗口和内部的操做分开是很重要的。把这两个混淆将会引发看起来不可预知的行为,而且最终它将迫使开发者来学习框架的内部工做机制。我认为许多的前端开发者已经有相似的经历。
一次事件循环的遍历叫作 tick,其中执行的代码称为任务。一个 tick 之中只能有一个微任务队列,一个 tick 之中能够有多个任务队列。
同一个任务源的产生的事件必须在同一个任务队列里面,而且在每一个任务队列里面全部的任务必须按插入的顺序执行。微任务队列中微任务也是会按插入的顺序执行。
微任务是在一个 tick 中当前任务结束执行以后开始执行。