调度器 | 简述 nextTick

105 阅读2分钟

核心实现分析

  1. 基础变量:
// 创建一个已解决的 Promise 实例作为基础 Promise
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
// 当前刷新队列的 Promise,初始为 null
let currentFlushPromise: Promise<void> | null = null
  1. nextTick 函数签名:
export function nextTick<T = void, R = void>(
  this: T,           // this 上下文类型
  fn?: (this: T) => R,  // 可选的回调函数
): Promise<Awaited<R>>  // 返回 Promise  {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

工作原理

  1. Promise 选择
  • 函数首先选择使用 currentFlushPromise 或 resolvedPromise

  • 如果当前有正在进行的更新队列(currentFlushPromise 存在),则使用它

  • 否则使用基础的 resolvedPromise

为什么在 nextTick 中基础 Promise 选择使用 resolvedPromise 而不是 new Promise()

1.性能考虑

Prmise.resolve() 会立即创建一个已经解决(resolved)的 Promise 对象,不需要等待异步操作完成。相比之下,使用 new Promise() 需要创建一个新的 Promise 实例并执行 executor 函数,这会带来额外的性能开销。

2.简化微任务调度

  • Promise.resolve() 创建的是一个已完成状态的 Promise,可以直接用于调度微任务

  • 在 nextTick 实现中,我们只需要一个简单的机制来将任务推入微任务队列

  • 不需要显式的 resolve/reject 处理

  1. 确定性
  • 使用 Promise.resolve() 可以确保得到一个处于 fulfilled 状态的 Promise

  • 这样可以保证调度器中的任务按预期执行,不会出现意外的 pending 状态

  1. 代码复用
const p = currentFlushPromise || resolvedPromise
  • 这个已解决的 Promise 可以被重复使用
  • 避免了每次调用 nextTick 都创建新的 Promise 实例
  1. 回调处理
// 如果提供了回调函数
return fn ? p.then(this ? fn.bind(this) : fn) : p
  • 如果传入回调函数 fn

    • 检查是否有 this 上下文

    • 如有,则绑定 this 上下文到回调函数

    • 将回调添加到 Promise 链中

  • 如果没有回调,直接返回 Promise

使用场景

  1. 无回调使用:
await nextTick()
// 等待下一个 DOM 更新周期
  1. 带回调使用:
nextTick(() => {
  // DOM 更新后执行的代码
})

重要特点

  1. 微任务调度
  • 使用 Promise 实现,确保在当前宏任务结束后执行

  • 属于微任务队列,优先级高于 setTimeout 等宏任务

  1. 更新同步
  • 如果当前有正在进行的更新(currentFlushPromise),会等待该更新完成

  • 确保回调在所有状态更新和 DOM 更新后执行

  1. 灵活性
  • 支持 Promise 方式和回调方式两种使用方法

  • 保持了 this 上下文的正确绑定