使用事件循环
和大多数现代框架一样,NX 在后台处理 DOM 操作和数据绑定。它批量操作并异步执行它们以提升性能。为了让这些操作按正确地时序执行,它依赖于 Promises
,MutationObservers
和 requestAnimationFrame()
。
预想的定时系统如下:
- 开发人员编码
- NX 提供数据绑定和响应 DOM 操作
- 开发人员自定义的钩子
- 用户代理渲染
步骤 1
NX 同时使用 ES6 Proxies 来注册一个对象变动和使用 MutationObserver 来注册 DOM 变动(下一章将更多介绍这些内容)。它会延迟响应,作为微任务的第二步以提升性能。延迟响应对象变化是使用 Promise.resolve().then(reaction)
来实现的,然后由 MutationObserver 来自动处理,因为 MutationObserver 内部使用微任务。
步骤 2
开发者的代码(任务)运行结束。由 NX 注册的微任务响应开始执行。微任务是按顺序执行的。注意此时我们仍然在同一个循环 tick 中。
步骤 3
NX 使用 requestAnimationFrame(hook)
来运行由开发者传过来的钩子。这也许会发生在之后的循环 tick 之中。重要的是,这里的钩子会在下一次渲染之前和所有的数据,DOM 和 CSS 的更改都执行之后运行。
步骤 4
浏览器渲染接下来的视图。这也许会发生在之后的循环 tick 之中,但是它绝不可能发生在一个 tick 的前几个步骤之前。
注意事项
我们只是在原生的事件循环之上实现了一个简单但可用的定时系统。理论上会运行得很好,但是定时是一个很微妙的东西,一个微小的错误可能会导致一些非常奇怪的 bug。
在一个复杂的系统之中,设置一些关于定时的规则,并且在之后遵守它们是非常重要的。Nx 制定了如下规则。
- 绝对不要在内部操中作使用
setTimeout(fn, 0)
- 用相同的方法注册微任务
- 只为内部操作保留微任务
- 不要使用其它的东西来污染开发者钩子执行时间窗口
规则 1 和 2
对数据和 DOM 操作的响应,应该按照操作的发生顺序来执行。只要他们的执行顺序没有混淆,延迟它们的执行是可取的。把执行顺序搞混淆会让事情变得不可预知和难以找出原因。
setTimeout(fn, 0)
是完全不可预知的。用不同的方法注册微任务也会导致混淆执行顺序。例如,在下面的示例中, microtask2
将会错误地在 microtask1
之前执行。
Promise.resolve().then().then(microtask1)
Promise.resolve().then(microtask2)
规则 3 和 4
把开发者代码执行时间窗口和内部的操作分开是很重要的。把这两个混淆将会引起看起来不可预知的行为,并且最终它将迫使开发者来学习框架的内部工作机制。我认为许多的前端开发者已经有类似的经历。