# Vue 异步更新源码解析

TIP

此篇主要讲了Vue中对于数据更新的性能优化,实现多次修改数据只更新一次视图。以及$nextTick方法的实现。

<div id="app">
  {{name}}
</div>
<script>
  const vm = new Vue({
    el: "#app",
    data: {
      name: "mrzhao",
    },
  });
  setTimeout(() => {
    vm.name = 1;
    vm.name = 2;
    vm.name = 3;
    vm.name = 4;
    vm.name = 5;
    vm.$nextTick(()=>{
      console.log(vm.$el)
    })
  }, 1000);
</script>

TIP

按照之前的逻辑,数据取值时会做依赖收集,数据更新时,会触发视图更新,只要新值与老值不一样,就会触发视图更新,这样很浪费性能。

# 升级 watcher

// src/observer/watcher.js

import { queueWatcher } from "./scheduler";

export default class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    // 省略不更改的代码....
  }

  // 修改update方法
  update() {
    // 每次更新时,不在立即调用,而是存到队列中,等待批量更新
    // 而且是异步更新
    queueWatcher(this);
  }
  // 新增run方法,真正更新时调用的方法
  run(){
    this.get()
  }
}

TIP

原本只要更改数据,就会调用watcherupdate方法立即更新,现在不立即执行,先存放到队列当中,最终批量更新。

// src/observer/scheduler.js

import { nextTick } from "../utils";

let queue = []; // 存放watcher
let has = {}; // 用来watcher去重

// 动画  滚动的频率高,节流 requestFrameAnimation
function flushSchedulerQueue() {
  for (let i = 0; i < queue.length; i++) {
    // 触发watcher真正的更新
    queue[i].run(); 
  }
  // 更新完成之后清空队列
  queue = [];
  has = {};
  pending = false;
}

let pending = false;

// 实现批量更新队列机制
export function queueWatcher(watcher) {
  // 用id来去重
  const id = watcher.id;
  // 没有才往里放
  if (has[id] == null) {
    queue.push(watcher);
    has[id] = true;
    // 防抖处理
    if (!pending) {
      // 异步更新
      nextTick(flushSchedulerQueue, 0);
      pending = true;
    }
  }
}

scheduler

scheduler用来做watcher的调度工作,对watcher进行了去重,更新时的防抖处理。最终通过核心异步方法nextTick实现watcher的更新。

# 异步更新核心方法nextTick

// src/util

export function isFunction(val) {
  return typeof val === "function";
}

export function isObject(val) {
  return typeof val == "object" && val !== null;
}
const callbacks = [];

function flushCallbacks() {
  callbacks.forEach((cb) => cb());
  waiting = false;
}

let waiting = false;

function timer(flushCallbacks) {
  // 定义异步方法-> 优雅降级(微任务优先)
  let timerFn = () => {};
  if (Promise) {
    timerFn = () => {
      Promise.resolve().then(flushCallbacks);
    };
  } else if (MutationObserver) {
    // MutationObserver 监控DOM的变化,异步执行传入的方法
    let textNode = document.createTextNode(1);
    let observe = new MutationObserver(flushCallbacks);
    // 监控文本内容的变化,改变了会异步执行传入的回调
    observe.observe(textNode, {
      characterData: true,
    });
    timerFn = () => {
      textNode.textContent = 3;
    };
  } else if (setImmediate) {
    timerFn = () => {
      setImmediate(flushCallbacks);
    };
  } else {
    timerFn = () => {
      setTimeout(flushCallbacks);
    };
  }
  timerFn();
}

// 微任务是在页面渲染前执行 我取的是内存中的已经计算完的dom,不关心是否渲染完毕
export function nextTick(cb) {
  // 除了渲染watcher,还会存放用户手动调用存入的回调方法,会按照顺序存入
  callbacks.push(cb); 

  if (!waiting) {
    timer(flushCallbacks); // vue2 中考虑了兼容性问题 vue3 里面不在考虑兼容性问题
    waiting = true;
  }
}

# $nextTick 挂载原型

$nextTick

最后将nextTick扩展到Vue的原型上,供用户调用

// src/render.js
import { nextTick } from "./util";

export function renderMixin(Vue) {
  Vue.prototype.$nextTick = nextTick
  // ...省略其他代码
}

# 批量异步更新原理

批量异步更新

多次同步修改数据时,在第一次修改数据时,就会把对应的watcher放入queue队列中(会根据watcher的id做去重处理),之后再同步修改数据,就不会再往queue队列中存放watcher了。当同步代码执行完成后,开始调用$nextTick执行微任务异步执行flushSchedulerQueue,调用watcherrun方法,取到最终的值,更新视图。

# $nextTick中为什么能拿到最新的DOM结构

TIP

$nextTick方法不是微任务异步更新么?微任务不是在渲染之前执行完成么?为什么在$nextTick中能拿到最新的DOM结构呢?

因为使用了$nextTick来统一渲染watcher执行 和 用户$nextTick回调执行 的类型,都是同一个timerFn,所以就会根据放入回调函数的顺序来执行响应的回调。当数据更新时,放入渲染watcher,之后放入用户的回调,渲染watcher执行完更新逻辑后,内存中的DOM节点已经更新完毕,所以之后再执行用户回调的时候就能获取到内存中的最新的DOM节点。

上次更新: 5/24/2023, 11:37:36 AM