手写 mini-vue3 实现 nextTick(十二)

原理

可以这么来理解 nextTick,响应式数据发生改变是同步任务,nextTick 是异步微任务,根据 JS 的事件循环原理,是先执行同步任务,再执行微任务。

假如没有使用 nextTick 会怎样呢?如果不断触发响应式数据的改变,那么就会不断地触发 effect 副作用函数,导致多次执行 render 函数,而我们只是想要最终的那一次更新即可,中间过程的更新对于我们只想渲染最终结果到页面来说是多余的触发。

那么问题就是,怎么才可以在等响应式数据改变完之后,再去执行且只执行一次 render 函数呢?

异步微任务可以做到。

在响应式数据发生改变的时候,不执行 effect 副作用函数 fn,而是通过 scheduler 把当前的 fninstance.update 添加到异步微任务队列。

等到所有同步的任务执行完,也就是先让响应式数据最终改变完成后,再去执行异步微任务队列的任务。

而在执行了 instance.update 之后,DOM 的数据才是最新的。

vue3 抛出的 nextTick 内部的实现原理是接收一个函数 fn,返回一个 promise,该 promisePromise.resolve().then(fn) 的结果,也就是它也是内部实现的是一个异步微任务。

所以我们往往使用 nextTick 来获取 DOM 更新之后的数据。

这个例子也许可以帮我们更好地理解 JS 的微任务在 nextTick 中的使用:

js 复制代码
const instanceUpdate = () => {
  console.log('effect-update')
}
const p1 = Promise.resolve();
const queue1 = [];

function queueJobFn(job) {
  if(queue1.includes(job)) {
    return;
  }

  queue1.push(job);

  p1.then(() => {
    
    while(queue1.length) {
      const job = queue1.shift();
      job && job();
    }
  });
}

let i = 1; // 模拟响应式数据
i++; // 模拟响应式数据发生变化
queueJobFn(instanceUpdate); // 模拟触发 effect scheduler
i++; // 模拟响应式数据发生变化
queueJobFn(instanceUpdate); // 模拟触发 effect scheduler

// 模拟 nextTick
p1.then(() => {
  console.log(i);
});

执行的顺序是:

  1. i = 1,执行同步
  2. i++,执行同步,i = 2
  3. queueJobFn(instanceUpdate); 依据 JS 事件循环,内部遇到 Promise.then,先不执行,放到微任务队列中排队等候,而我们自己也把任务存到 queue1 队列中。
  4. i++,执行同步,i = 3
  5. queueJobFn(instanceUpdate); queue1 队列中已存在于 instanceUpdate,return
  6. 遇到 p1.then 微任务,依据 JS 事件循环,遇到 Promise.then,先不执行,放到微任务队列中排队等候
  7. 依据 JS 事件循环,同步任务执行完,开始执行异步微任务,先进入队列的是 instanceUpdate,执行,从 queue1 中一一取出任务来执行,打印 effect-update
  8. 接着还有 p1.then, 执行,打印 3

实现

scheduler

diff 复制代码
  function setupRenderEffect(instance, initialVNode, container, anchor) {
    instance.update = effect(() => {
      if(!instance.isMounted) {
        console.log('init');
        const { proxy } = instance;
        const subTree = (instance.subTree = instance.render.call(proxy, proxy));
        
        patch(null, subTree, container, instance, anchor);
      
        // vnode -> element
        initialVNode.el = subTree.el;

        instance.isMounted = true;
      } else {
        console.log('update');

        // 更新 props,需要一个 vnode
        const { next, vnode } = instance;
        if(next) {
          // 设置 新虚拟节点的 el
          next.el = vnode.el;
          updateComponentPreRender(instance, next);
        }

        const { proxy } = instance;
        const subTree = instance.render.call(proxy, proxy);
        const prevSubTree = instance.subTree;
        instance.sbuTree = subTree;

        patch(prevSubTree, subTree, container, instance, anchor);
      }
+    }, {
+      scheduler() {
+        queueJobs(instance.update); 
+      }
+    });
  }

queueJobs

queueJobs 的作用是把 instance.update 添加到微任务队列,等同步任务执行完之后,再执行 instance.update

外面手动添加 nextTick 写异步回调,则这个异步回调将会以异步微任务的形式执行。

如果要回获取 DOM 渲染后的数据,则在 nextTick 回调中可以获取到。

ts 复制代码
// scheduler.ts
const queue: any[] = []; // 微任务队列

const p = Promise.resolve(); // 可以防止多次创建
let isFlushPending = false;

export function nextTick(fn?) {
  return fn ? p.then(fn) : p; // 返回值是个 promise
}

export function queueJobs(job) {
  // 如果已经存在,则不需要再次添加,不存在则添加到队列
  if(!queue.includes(job)) {
    queue.push(job);
  }

  queueFlush();
}

function queueFlush () {
  if(isFlushPending) return; // 有在执行微任务,则返回

  isFlushPending = true;

  nextTick(flushJobs); // 微任务
}

function flushJobs() {
  isFlushPending = false;

  let job;
  while(job = queue.shift()) { // 出队
    job && job(); // 执行
  }
}
相关推荐
不会敲代码14 小时前
从零到一:用 Vue3 + Kimi 大模型打造「拍照记单词」AI 应用
vue.js·typescript·aigc
隔壁的大叔4 小时前
Markdown 渲染如何穿插自定义组件
前端·javascript·vue.js
RONIN4 小时前
脚手架搭建项目框架(create-vite、vue-cli、create-vue、quasar-cli)
vue.js
愚者Pro4 小时前
Flutter基础学习
前端·javascript·vue.js
RONIN5 小时前
mock模拟后端,生成伪数据接口
vue.js
Hello-Mr.Wang6 小时前
【保姆级教程】MasterGo MCP + Cursor 一键实现 UI 设计稿还原
前端·javascript·vue.js·ai编程
RONIN6 小时前
UI组件库vant3(移动端用的比较多)
vue.js
本末倒置1836 小时前
Vue 3 开发者转型 React 指南:保姆级教程
前端·javascript·vue.js
卜凡.8 小时前
Vue是对HTML、CSS、JS的标准化、组件化和响应式的上层抽象与增强
javascript·vue.js·html
Ww.xh10 小时前
鸿蒙系统中HTML与Vue集成方案
vue.js·html·harmonyos