手写 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(); // 执行
  }
}
相关推荐
源码宝9 小时前
【智慧工地源码】智慧工地云平台系统,涵盖安全、质量、环境、人员和设备五大管理模块,实现实时监控、智能预警和数据分析。
java·大数据·spring cloud·数据分析·源码·智慧工地·云平台
dae bal10 小时前
关于RSA和AES加密
前端·vue.js
代码老y13 小时前
十年回望:Vue 与 React 的设计哲学、演进轨迹与生态博弈
前端·vue.js·react.js
前端snow13 小时前
前端无接口实现Table导出Excel的两种方案(附完整代码)
javascript·vue.js·react.js
yangholmes888815 小时前
如何为 Vue 组件提供 slots 静态类型检查
vue.js
借月15 小时前
高德地图绘制工具全解析:线路、矩形、圆形、多边形绘制与编辑指南 🗺️✏️
前端·vue.js
....49218 小时前
Vue3 + Element Plus 实现可搜索、可折叠、可拖拽的部门树组件
前端·javascript·vue.js
bitbitDown19 小时前
重构缓存时踩的坑:注释了三行没用的代码却导致白屏
前端·javascript·vue.js
Solon阿杰20 小时前
前端(react/vue)实现全景图片(360°)查看器
javascript·vue.js
前端程序猿i20 小时前
用本地代理 + ZIP 打包 + Excel 命名,优雅批量下载跨域 PDF
前端·javascript·vue.js·html