写一个 JavaScript 框架(二)
这是写一个 JavaScript 框架系列的第二篇。在这个章节中,我将会阐明多种在浏览器中执行异步代码的方式。你们将会了解到事件轮询,各种调度技巧的区别,比如 setTimeout
和 Promises
。
这个系列是关于一个叫 NX 的开源客户端框架。在这个系列中,主要是阐明在开发框架过程中遇见的一些我已克服的难题。如果你对 NX 感兴趣,请阅览NX 主页。
这个系列包括以下几个章节:
可能大部分的人都熟悉用 Promise
, process.nextTick()
, setTimeout()
以及 requestAnimationFrame()
执行异步代码。它们都会在内部执行事件的轮询,但是在时间精度上表现出来的差异性十分明显。
这个章节里,我将会解释它们之间的区别,然后向你们展示 NX 是如何实现一个时间系统的。我们不会重新造一个轮子,我们将使用原生的事件轮询去达成我们的目标。
事件轮询
事件轮询不仅仅是在 ES6 标准提及,JavaScript 有它自己的任务和任务队列。更复杂的事件轮询在 NodeJS 和 HTML5 标准中有具体说明。由于这是另一个关于前端的系列,我会在后续的文章中提及。
事件轮询之所以叫轮询的原因是,它会不断地循环寻找新的任务去执行。轮询的一次迭代我们称为一个时刻,在这个时刻中执行的代码我们称为一次任务。
在轮询中,一次任务的代码是可以安排其他任务同步执行的。一种简单的手动编程方式去安排一个新的任务是使用 setTimeout(taskFn)
。尽管如此,任务的来源是多种多样的,比如用户行为,网络请求或者 DOM 操作。
任务队列
事情更加复杂一点的话,事件轮询可以有多个任务队列。这里有两个限制,同一类的任务必须来自于同一个任务队列,并且任务在每个队列中必须是以插入顺序去执行。除去这些,用户可以自由的做他所想的事。比如,他可以决定下一次执行哪个任务队列。
|
|
在这个模型中,我们放松了对时间精度的控制。在我们的任务使用 setTimeout()
之前,浏览器可能已经决定完全清空好几个其他的任务队列了。
微任务队列
幸运的是,事件轮询还拥有一种单独的队列叫微任务队列。它在每个时刻的任务执行完成之后会完全清空。
|
|
最简单的安排一次微任务的方式是 Promise.resolve().then(microtaskFn)
。微任务是以插入顺序执行的,由于这里只有一个微队列,这个时候用户是不会混淆我们的。
此外,微任务可以安排新的微任务插入到同一个队列,并在同一个时刻处理。
渲染
最后一件被遗落的事情是渲染计划。不同于事件的处理和分析,渲染并不是由一个单独的后台任务完成的。它是一种可以运行在每一个轮询时刻最后面的算法。
用户因此又拥有许多自由:可以在每个任务之后渲染,也可以让许多任务不用去执行渲染。
幸运的是,我们有 requestAnimationFrame()
,它在下一次渲染之前执行传递的函数。最终我们的事件模型看上去是这样的:
|
|
现在让我们用所有的知识去建立一个调度系统。
使用事件轮询
和大部分现代框架一样,NX 在后台处理 DOM 操作和数据绑定。NX 分批处理这些操作,并且为了更好的性能,异步执行他们。为了能安排好处理这些操作,NX 依赖于 Promises
, MutationObservers
& requestAnimationFrame()
。
预期的安排有如下:
- 来自开发者的业务代码;
- NX 的数据绑定 & DOM 的回应操作;
- 用户定义的钩子函数;
- 客户端的渲染;
步骤一
NX 同步完成 ES6 Proxies 注册对象 & MutationObserver
完成 DOM 变化观察者(这些知识大部分都在下一个章节)。为了优化性能的反馈事件可以通过作为微任务延迟完成。为了观察对象变化的事件可以使用 Promise.resolve().then(reaction)
延迟实现,并且通过 MutationObserver (它在内部会调用微任务)自动执行。
步骤二
当用户的业务代码执行完成后,通过 NX 注册的响应微任务开始执行。因为它们是微任务,所以它们是按照顺序执行的。需要提醒的是,我们目前仍然是在同一个轮询的时刻里面。
步骤三
NX 用 requestAnimationFrame(hook)
运行用户传递进来的钩子事件。这个可能发生在下一个轮询的时刻。最重要的是,这些钩子事件运行于下一次渲染之前,所有数据、DOM & CSS 变化已经被处理之后。
步骤四
浏览器渲染下一个页面。这个仍然可能发生在下一个轮询时刻,但是它不能发生在上一个步骤的时刻之前。
需要牢记于心的事情
我们刚刚基于原生的轮询之上实现了一个简单但是高效的调度系统。理论上它是可以正常工作的,但是调度是非常容易出错的,一些轻微的误差可能造成严重的bug。
在一个复杂的系统中,设立一些调度规则,并且一直坚持是非常重要的。对于 NX,我有以下几个规则:
- 永远不要在内部操作中使用
setTimeout(Fn, 0)
; - 用同一个方法注册微任务;
- 微任务只用于存储内部操作;
- 不要用任何事情去污染开发者的钩子函数执行。
规则 1 & 2
数据和 DOM 操作上的响应行为应该按照变化的顺序执行,为了不造成执行顺序混淆,即使延迟执行也是 👌 的。混淆执行顺序使事情变得难以把握并且难以究其原因。setTimeout(Fn, 0)
就是完全不可预测的。
通过不同的方式注册微任务也会造成执行顺序混淆,比如下面的案例:微任务2错误的在微任务1之前执行了。
|
|
Rule 3 & 4
区分开发者的任务代码和系统内部操作是非常重要的。将这两个行为混淆在一起可能会导致一些意料之外的行为,而且它还可能会导致开发者不得不去了解框架内部的工作机制。我相信许多前端开发人员已经有过类似的体验了。
结论
如果你对 NX 框架感兴趣,可以参阅主页。热衷于探索的读者可以在GitHub Repo参阅源码。
我希望你能认为这一篇好的文章,下次将会讨论沙箱求值。
作者: Listen
来源: http://swarosky44.github.io
链接: http://swarosky44.github.io/2017/07/31/翻译 - 写一个 JavaScript 框架(二)/
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可