nextTick用过吗?聊聊实现思路

米阳 2024-6-19 556 6/19

Vue 的 nextTick 通过维护回调队列和异步调度机制,确保用户回调在 DOM 更新后执行。其核心是利用微任务的高优先级特性,结合降级策略实现跨平台兼容。例如,当我们修改数据后,Vue 会将 DOM 更新和用户定义的回调放入同一异步任务中,避免多次渲染。

废话不多说先直接上vue源码节选再做总结

// 存储所有的回调函数
const callbacks = [];
/* 类似于节流的标记位,标记是否处于节流状态。防止重复推送任务 */
let pending = false;

/* 遍历执行数组 callbacks 中的所有存储的 cb 回调函数 */
function flushCallbacks() {
  // 重置标记,允许下一个 nextTick 调用
  pending = false;
  /* 执行所有 cb 回调函数 */
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i](); // 依次调用存储的回调函数
  }
  // 清空回调数组,为下一次调用做准备
  callbacks.length = 0;
}

// 判断最终支持的 API:Promise / MutationObserver / setImmediate / setTimeout
let timerFunc;

if (typeof Promise !== "undefined") {
  // 创建一个已resolve的 Promise 实例
  var p = Promise.resolve();
  // 定义 timerFunc 为使用 Promise 的方式调度 flushCallbacks
  timerFunc = () => {
    // 使用 p.then 方法将 flushCallbacks 推送到微任务队列
    p.then(flushCallbacks);
  };
} else if (
  typeof MutationObserver !== "undefined" &&
  MutationObserver.toString() === "[object MutationObserverConstructor]"
) {
  /* 新建一个 textNode 的 DOM 对象,用 MutationObserver 绑定该 DOM 并指定回调函数。
   在 DOM 变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),
   即 textNode.data = String(counter) 时便会加入该回调 */
   var counter = 1; // 用于切换文本节点的值
   var observer = new MutationObserver(flushCallbacks); // 创建 MutationObserver 实例
   var textNode = document.createTextNode(String(counter)); // 创建文本节点
   observer.observe(textNode, {
      characterData: true, // 监听文本节点的变化
   });
   // 定义 timerFunc 为使用 MutationObserver 的方式调度 flushCallbacks
  timerFunc = () => {
    counter = (counter + 1) % 2; // 切换 counter 的值(0 或 1)
    textNode.data = String(counter); // 更新文本节点以触发观察者
  };
} else if (typeof setImmediate !== "undefined") {
  /* 使用 setImmediate 将回调推入任务队列尾部 */
  timerFunc = () => {
    setImmediate(flushCallbacks); // 将 flushCallbacks 推送到宏任务队列
  };
} else {
  /* 使用 setTimeout 将回调推入任务队列尾部 */
  timerFunc = () => {
    setTimeout(flushCallbacks, 0); // 将 flushCallbacks 推送到宏任务队列
  };
}

function nextTick(cb) {
  // 用于存储 Promise 的解析函数
  let _resolve; 
  // 将回调函数 cb 添加到 callbacks 数组中
  callbacks.push(() => {
    // 如果有 cb 回调函数,将 cb 存储到 callbacks
    if (cb) {
      cb();
    } else if (_resolve) {
      // 如果参数 cb 不存在,则保存 Promise 的成功回调 resolve
      _resolve();
    }
  });

  // 第一次使用 nextTick 时,pending 为 false,下面的代码才会执行
  if (!pending) {
    // 改变标记位的值,如果有 nextTickHandler 被推送到任务队列中去则不需要重复推送
    pending = true;
    // 调用 timerFunc,将 flushCallbacks 推送到合适的任务队列
    timerFunc(flushCallbacks);
  }

  // 如果没有 cb 且环境支持 Promise,则返回一个 Promise
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      // 保存 resolve 到 callbacks 数组中
      _resolve = resolve;
    });
  }
}

核心思路总结:

  1. 核心机制
    • 维护 callbacks 数组存储所有回调函数。
    • 通过 flushCallbacks 一次性执行所有回调。
    • timerFunc 负责将 flushCallbacks 推入异步队列(根据环境选择微任务/宏任务)。
  2. 调用流程
    • this.$nextTick(cb) 将回调加入 callbacks
    • 首次调用触发 timerFunc 安排异步任务。
    • 后续调用仅追加回调到 callbacks
    • 支持 Promise 语法(无回调时返回 Promise)。
  3. 流程图关键点
    • 回调收集 → 异步任务调度 → 批量执行。

补充知识点:

  1. 异步任务优先级
    • Vue 优先使用微任务(如 PromiseMutationObserver),确保在 DOM 渲染前执行回调。
    • 降级机制:在不支持微任务的环境(如 IE11)下,依次降级到 setImmediate 或 setTimeout(宏任务)。
  2. 事件循环中的执行时机
    // 微任务场景(默认)
    timerFunc = () => Promise.resolve().then(flushCallbacks)
    
    // 宏任务场景(降级)
    timerFunc = () => setTimeout(flushCallbacks, 0)
    • 微任务会在当前事件循环的 ​本轮末尾 执行,早于 UI 渲染。
    • 宏任务会在下一轮事件循环执行,可能导致回调延迟到渲染后。
  3. 回调队列的锁定与清空
    • 执行 flushCallbacks 前会 ​复制并清空 callbacks 数组,避免递归调用导致无限循环。
      function flushCallbacks() {
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
          copies[i]()
        }
      }
  4. 与 Vue 更新机制的关系
    • Vue 的异步更新队列(queueWatcher)同样依赖 nextTick,确保所有数据变更后的 DOM 更新在一次异步任务中完成。
    • 示例:修改数据后立即获取 DOM 状态,需通过 nextTick 等待更新完成:
      this.message = 'updated'
      this.$nextTick(() => {
        console.log(document.getElementById('text').innerHTML) // 正确获取最新 DOM
      })
  5. Promise 语法支持
    • 当不传回调时,返回 Promise 对象,支持 async/await
      await this.$nextTick()
      // 后续代码可访问更新后的 DOM

       

完整实现流程图:

graph TD
A[调用 this.$nextTick(callback)] --> B{是否有回调函数?}
B -->|是| C[将 callback 加入 callbacks 数组]
B -->|否| D[返回 Promise 实例]
C --> E{是否首次调用?}
E -->|是| F[调用 timerFunc 安排异步任务]
E -->|否| G[等待当前异步任务执行]
F --> H[根据环境选择微任务/宏任务]
H --> I[将 flushCallbacks 加入任务队列]
I --> J[事件循环执行 flushCallbacks]
J --> K[复制并清空 callbacks]
K --> L[依次执行所有回调]
D --> M[Promise.then 触发 flushCallbacks]

 

 

- THE END -

米阳

3月19日17:34

最后修改:2025年3月19日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论