vue异步更新dom的原理

Data对象:vue中的data方法中返回的对象;

Dep对象:每一个Data属性都会创建一个Dep,用来搜集所有使用到这个Data的Watcher对象;

Watcher对象:主要用于渲染DOM

 

Vue异步更新DOM的原理

  Vue中的数据更新是异步的,意味着我们在修改完Data之后并不能立刻获取修改后的DOM元素。

1  什么时候能获取到真正的DOM元素?

  在Vue的nextTick回调中。

2   为什么Vue需要通过nextTick方法才能获取最新的DOM?

2.1 vue在调用Watcher更新视图时,并不会直接进行更新,而是把需要更新的Watcher加入到Queue队列里,然后把具体的更新方法flushSchedulerQueue 传给nexTick 进行调用。

 

// src > core > observer > watcher.js + scheduler.js
// 当一个 Data 更新时,会依次执行以下代码
// 1. 触发 Data.set // 2. 调用 dep.notify // 3. Dep 会遍历所有相关的 Watcher 执行 update 方法 class Watcher { // 4. 执行更新操作 update() { queueWatcher(this); } } const queue = []; function queueWatcher(watcher: Watcher) { // 5. 将当前 Watcher 添加到异步队列 queue.push(watcher); // 6. 执行异步队列,并传入回调 nextTick(flushSchedulerQueue); } // 更新视图的具体方法 function flushSchedulerQueue() { let watcher, id; // 排序,先渲染父节点,再渲染子节点 // 这样可以避免不必要的子节点渲染,如:父节点中 v-if 为 false 的子节点,就不用渲染了 queue.sort((a, b) => a.id - b.id); // 遍历所有 Watcher 进行批量更新。 for (index = 0; index < queue.length; index++) { watcher = queue[index]; // 更新 DOM watcher.run(); } }

 

 

 

 

 

 2.2 nextTick -- 将传入的flushSchedulerQueue 添加到callbacks 数组中,然后执行了timerFunc 方法。

 

const callbacks = [];
let timerFunc;

function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  // 1.将传入的 flushSchedulerQueue 方法添加到回调数组
  callbacks.push(() => {
    cb.call(ctx);
  });
  // 2.执行异步任务
  // 此方法会根据浏览器兼容性,选用不同的异步策略
  timerFunc();
}

 

2.3  timerFunc方法 -- 是根据浏览器兼容性创建的一个异步方法,执行完该方法就会调用flushSchedulerQueue 方法进行具体的DOM更新。

let timerFunc;
// 判断是否兼容 Promise
if (typeof Promise !== "undefined") {
  timerFunc = () => {
    Promise.resolve().then(flushCallbacks);
  };
  // 判断是否兼容 MutationObserver
  // https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
} else if (typeof MutationObserver !== "undefined") {
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  // 判断是否兼容 setImmediate
  // 该方法存在一些 IE 浏览器中
} else if (typeof setImmediate !== "undefined") {
  // 这是一个宏任务,但相比 setTimeout 要更好
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 如果以上方法都不知道,使用 setTimeout 0
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

// 异步执行完后,执行所有的回调方法,也就是执行 flushSchedulerQueue
function flushCallbacks() {
  for (let i = 0; i < copies.length; i++) {
    callbacks[i]();
  }
}

 

 

 2.4 完善逻辑判断

2.4.1 判断has 标识,避免在一个Queue 中添加相同的Watcher;

2.4.2 判断waiting 标识,让所有的Watcher 都在一个tick 内进行更新;

2.4.3 判断flushing 标识,处理Watcher 渲染时,可能产生的新Watcher。

  如触发了v-if 条件,新增的Watcher 渲染。

 

 

 tip:nextTick 只是单纯通过Promise、setTimeout等方法模拟的异步任务。

3 为什么this.$nextTick 能够获取更新后的DOM? 

  调用this.$nextTick 其实就是调用图中的nextTick 方法,在异步队列中执行回调函数。根据先进先出原则,修改Data 触发的更新异步队列会先得到执行,执行完成后就生成了新的DOM,接下来执行this.$nextTick 的回调函数时,能获取到更新后的DOM元素了。

// 我们使用 this.$nextTick 其实就是调用 nextTick 方法
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this);
};

 

总结:vue异步更新的原理

  1. 修改 Vue 中的 Data 时,就会触发所有和这个 Data 相关的 Watcher 进行更新。
  2. 首先,会将所有的 Watcher 加入队列 Queue。
  3. 然后,调用 nextTick 方法,执行异步任务。
  4. 在异步任务的回调中,对 Queue 中的 Watcher 进行排序,然后执行对应的 DOM 更新。

 

posted on 2020-08-26 18:56  pleaseAnswer  阅读(2625)  评论(0编辑  收藏  举报