【手写 Vue2.x 源码】第二十四篇 - 异步更新流程

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一前言

上篇介绍了 Vue依赖收集的视图更新部分主要涉及以下几点

视图初始化时

  • render方法中会进行取值操作进入 Object.defineProperty 的 get 方法
  • get 方法中为数据添加 dep并记录当前的渲染 watcher
  • 记录方式watcher查重并记住 depdep 再记住 watcher

数据更新时

  • 当数据发生改,会进入 Object.defineProperty 的 set 方法
  • 在 set 方法中使 dep 中收集的全部 watcher 执行视图渲染操作 watcher.get()
  • 在视图渲染前this.getter方法执行前,通过 dep.target 记录当前的渲染 watcher
  • 重复视图初始化流程

本篇介绍 Vue 的异步更新流程


二异步更新的实现

1为什么要做异步更新

上文末尾提到了一个问题

当前版本在视图渲染阶段进行依赖收集数据改变通知所有被收集的 watcher 更新视图

let vm = new Vue({
  el: '#app',
  data() {
    return { name: "Brave" , age: 123}
  }
}); 
vm.name = "Brave Wang";
vm.name = "Brave";
vm.name = "Brave Wang";
vm.name = "Brave";
vm.name = "Brave Wang";
vm.name = "Brave";

在这种情况下频繁更新同一数据就会多次触发视图渲染dep.notify->watcher.update

虽然name的值变化了6次但只在最后一次进行视图更新即可

由于当前逻辑是同步调用watcher.update进行更新的即数据变化一次就会触发一次视图更新

要想做到只在最后执行一次视图更新就需要将视图更新改造为异步更新的机制

2异步更新的实现思路

当数据发生变化时将数据变更的逻辑先缓存起来不直接处理如果有相同数据更新就进行合并在最后做更新一次

在 Vue 中vue.nextTick 方法能够实现异步更新

3数据变更缓存的位置

数据变更就会进入 setter但不能在 setter 进行缓存因为数组的变化是不会进入 setter 的

但不管是何种数据变化最终视图渲染都会汇集到 watcher.update 方法所以在这里缓存是最佳的

4缓存 watcher 更新逻辑

可以先将 watcher 集中缓存到一个队列中缓存过程中可以进行合并会后一次执行即可

因为此时为异步代码当逻辑都执行完成后才会执行会把队列中的 watcher 都 run

在 vue 中有一个任务调度方法src/observe/schedule.js

创建watcher 缓存队列 queueWatcher作用做 watcher 的去重和缓存

let queue = [];           // 用于缓存渲染 watcher
let has = {};             // 存放 watcher 唯一 id用于 watcher 的查重
let pending = false;      // 控制 setTimeout 只走一次

/**
 * 将 watcher 进行查重并缓存最后统一执行更新
 * @param {*} watcher 需更新的 watcher
 */
export function queueWatcher(watcher) {
  let id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    queue.push(watcher);  // 缓存住watcher,后续统一处理
    if (!pending) {       // 等效于防抖
      setTimeout(() => {
        queue.forEach(watcher => watcher.run()) // 依次触发视图更新
        queue = [];       // reset
        has = {};         // reset
        pending = false;  // reset
      }, 0);
      pending = true;     // 首次进入被置为 true使微任务执行完成后宏任务才执行
    }
  }
}

Watcher类 update 方法使用 queueWatcher 方法添加 run 方法做视图更新

从而实现异步更新

// src/observe/watcher.js

import Dep from "./dep";
import { queueWatcher } from "./scheduler";

let id = 0;
class Watcher {
  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;
    this.id = id++;
    this.depsId = new Set();
    this.deps = [];
    this.getter = fn;
    this.get();
  }
  addDep(dep){
    let did = dep.id;
    if(!this.depsId.has(did)){
      this.depsId.add(did);
      this.deps.push(dep);
      dep.addSub(this); 
    }
  }
  get(){
    Dep.target = this; 
    this.getter();
    Dep.target = null; 
  }
  update(){
    console.log("watcher-update", "查重并缓存需要更新的 watcher")
    queueWatcher(this);
  }
  run(){
    console.log("watcher-run", "真正执行视图更新")
    this.get();
  }
}

export default Watcher;

TODO 问题Vue 的更新策略是等待同步代码都执行完再更新异步

5代码重构

  • nextTick 异步方案改用 promise 方案实现
// src/utils.js

/**
 * 将方法异步化
 * @param {*} fn 需要异步化的方法
 * @returns 
 */
export function nextTick(fn) {
  return Promise.resolve().then(fn);
}
  • 将刷新队列逻辑抽取为独立的方法 flushschedulerQueue

    setTimeiout 中的逻辑用于刷新队列执行所有 watcher.run 并将队列清空

/**
 * 刷新队列执行所有 watcher.run 并将队列清空
 */
function flushschedulerQueue() {
  queue.forEach(watcher => watcher.run()) // 依次触发视图更新
  queue = [];       // reset
  has = {};         // reset
  pending = false;  // reset
}

  • 改造后的代码
**
 * 将 watcher 进行查重并缓存最后统一执行更新
 * @param {*} watcher 需更新的 watcher
 */
export function queueWatcher(watcher) {
  let id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    queue.push(watcher);
    if (!pending) {
      nextTick(flushschedulerQueue); // 改造后使用 nextTick
      pending = true;
    }
  }
}

6测试异步更新

let vm = new Vue({
  el: '#app',
  data() {
    return { name:  "Brave"}
  }
}); 
vm.name = "Brave Wang";
console.log("数据更新后立即获取 dom", vm.$el.innerHTML);

控制台输出结果

image.png

此时控制台输出取到的 dom 元素为是旧值因为 vm.name 已变更为异步更新
那么如何获取到更新后的 dom?

7获取更新后的 dom

Vue中使用 vm.$nextTick 方法所以在Vue初始化的 initMixin 中为其添加原型方法 $nextTick:

// src/init.js

import { nextTick } from "./utils";

export function initMixin(Vue) {·
  Vue.prototype._init = function (options) {...}
  Vue.prototype.$mount = function (el) {...}
  // 为 Vue 扩展原型方法 $nextTick
  Vue.prototype.$nextTick = nextTick;
}

测试

let vm = new Vue({
  el: '#app',
  data() {
    return { name:  "Brave"}
  }
}); 

vm.name = "Brave Wang";
console.log("数据更新后立即获取 dom", vm.$el.innerHTML);

vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})

image.png


三异步更新实现的优化

在上边的实现中共创造了两个 promise

  • 第一次更新数据时创造了一个 promise
  • 第二次在 nextTick 中又创造了一个 promise
    第一个promise先执行第二个promise再执行
    所以第二个拿到的其实是第一个成功后的结果

这里可以优化成为创建一个 promise与 watcher 异步执行跟新的原理相似

  • 更新数据时将更新逻辑存起来
  • 当用户nextTick取值时继续将取值逻辑存起来
    将两个逻辑存到一个数组中在一个微任务中全部执行并清空即可

这样整个过程就只创建了一个 promise

// src/utils.js

let callbacks = []; // 缓存异步更新的 nextTick
let waiting = false;
function flushsCallbacks() {
  callbacks.forEach(fn => fn()) // 依次执行 nextTick
  callbacks = [];   // reset
  waiting = false;  // reset
}

/**
 * 将方法异步化
 * @param {*} fn 需要异步化的方法
 * @returns 
 */
export function nextTick(fn) {
  // return Promise.resolve().then(fn);
  callbacks.push(fn); // 先缓存异步更新的nextTick,后续统一处理
  if(!waiting){
    Promise.resolve().then(flushsCallbacks);
    waiting = true; // 首次进入被置为 true,控制逻辑只走一次
  }
}

callbacks中第一个 fn 一定来自是内部的第二个 fn 才是用户写的

将两个 fn 先进行缓存实现将用户的 nextTick 和内部更新的 nextTick 合并在一起

let vm = new Vue({
  el: '#app',
  data() {
    return { name:  "Brave"}
  }
}); 

vm.name = "Brave Wang";
console.log("数据更新后立即获取 dom", vm.$el.innerHTML);

vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})
vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})
vm.$nextTick(()=>{
  console.log("$nextTick获取更新后的 dom", vm.$el.innerHTML);
})
所以在这种情况下
    更新数据的nextTick + 3次用户手写的nextTick
    共四次只创建了一个 promise 
    最后只用了一个微任务就都清空了这是一个批处理的思想
    多个nextTick执行一次 then而非多次

测试结果

image.png


四结尾

本篇主要介绍了 Vue 的异步更新流程主要涉及以下几点

  • 为什么要做异步更新
  • 异步更新的实现思路
  • 数据变更缓存的位置
  • 缓存 watcher 更新逻辑
  • vm.$nextTick 获取更新后的 dom
  • 测试异步更新

下一篇数组的依赖收集

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue