使用事件循环

和大多数现代框架一样,NX 在后台处理 DOM 操作和数据绑定。它批量操作并异步执行它们以提升性能。为了让这些操作按正确地时序执行,它依赖于 PromisesMutationObserversrequestAnimationFrame()

预想的定时系统如下:

  • 开发人员编码
  • 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 之前执行。

  1. Promise.resolve().then().then(microtask1)
  2. Promise.resolve().then(microtask2)

使用事件循环 - 图1

规则 3 和 4

把开发者代码执行时间窗口和内部的操作分开是很重要的。把这两个混淆将会引起看起来不可预知的行为,并且最终它将迫使开发者来学习框架的内部工作机制。我认为许多的前端开发者已经有类似的经历。