30-实现nextTick & 实现视图异步更新

66 阅读2分钟

先来个例子

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-09 13:18:11
 * @LastEditTime: 2022-04-09 13:26:01
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\example\nextTicker\NextTicker.js
 *
 */
// 测试 nextTick 逻辑
import { h, ref, getCurrentInstance } from "../../lib/mini-vue.esm.js";

// 如果 for 循环改变 count 的值 100 次的话
// 会同时触发 100 次的 update 页面逻辑
// 这里可以把 update 页面的逻辑放到微任务中执行
// 避免更改了响应式对象就会执行 update 的逻辑
// 因为只有最后一次调用 update 才是有价值的
window.count = ref(1);

// 如果一个响应式变量同时触发了两个组件的 update
// 会发生什么有趣的事呢?
const Child1 = {
  name: "NextTickerChild1",
  setup() {},
  render() {
    return h("div", {}, `child1 count: ${window.count.value}`);
  },
};

const Child2 = {
  name: "NextTickerChild2",
  setup() {
    const currentInstance = getCurrentInstance();

    function onClick() {
      for (let index = 0; index < 100; index++) {
        count.value++;
      }
      console.log("currentInstance", currentInstance);
    }

    return {
      onClick,
    };
  },
  render() {
    return h("div", {}, [
      h("div", {}, `child2 count: ${window.count.value}`),
      h(
        "button",
        {
          onClick: this.onClick,
        },
        "修改count"
      ),
    ]);
  },
};

export default {
  name: "NextTicker",
  setup() {},
  render() {
    return h(
      "div",
      { tId: "nextTicker" },
      [h(Child1), h(Child2)]
      //   `for nextTick: count: ${window.count.value}`
    );
  },
};

假如我们有个按钮,里面循环了100次,更改了count的值,会发生什么?

image.png

会触发100次的effect。。如果页面复杂了,浏览器就得被玩坏了!

Vue3的视图更新,其实是异步的!不会多次触发更新。那Vue3又是怎么做的异步更新呢?

  1. 把同步任务放到了微任务队列,同步执行完才去执行微任务
  2. 借助scheduler触发微任务队列来完成异步更新的
  3. 内部通过pendingFlag来判断调用状态

实现异步更新

render.ts

import { queueJobs } from "./scheduler";

function setupRenderEffect(instance, container, anchor) {
  instance.runner = effect(
    () => {
       // other code
    },
    {
      scheduler() {
         // 将本次 update 加入到任务队列中
         queueJobs(instance.runner);
      },
    }
  )
}

scheduler.ts

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-09 13:35:06
 * @LastEditTime: 2022-04-09 14:54:17
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\scheduler.ts
 *
 */

// 队列
const queue: any[] = [];
// 当前队列调用状态
let jobFlag = false;
export function queueJobs(job) {
  // 不存在时再添加进去
  if (!queue.includes(job)) {
    queue.push(job);
  }
  queueFlush();
}

function queueFlush() {
  if (jobFlag) return;
  jobFlag = true;
  Promise.resolve().then(() => {
    let job;
    while ((job = queue.shift())) {
      jobFlag = false;
      job && job();
    }
  });
}

实现nextTick

Vue3的nextTick也是把要执行的fn放进微任务队列执行的

// scheduler.ts

const P = Promise.resolve();
export function nextTick(fn) {
  return fn ? P.then(fn) : P;
}

基于nextTick优化微任务队列执行

// scheduler.ts

function queueFlush() {
  if (isFlushPending) return;
  // 通过pending flag来阻止多次调用
  isFlushPending = true;
  // 把当前任务放进了微任务队列
}

function flushJobs() {
  isFlushPending = false;
  let job;
  while ((job = queue.shift())) {
    job && job();
  }
}