【vue2原理】-第五篇,异步更新流程

一、介绍

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

二、异步更新的实现

1,为什么要做异步更新

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

js 复制代码
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 的去重和缓存

js 复制代码
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 方法做视图更新,

从而实现异步更新:

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;

Vue 的更新策略是:等待同步代码都执行完,再更新异步

5,代码重构

  • nextTick 异步方案改用 promise 方案实现
javascript 复制代码
// src/utils.js
/** * 将方法异步化 * @param {*} fn 需要异步化的方法 * @returns  */
export function nextTick(fn) {  
    return Promise.resolve().then(fn);
}
  • 将刷新队列逻辑抽取为独立的方法 flushschedulerQueue
  • setTimeiout 中的逻辑用于刷新队列:执行所有 watcher.run 并将队列清空;
js 复制代码
/** 
* 刷新队列:执行所有 watcher.run 并将队列清空; 
*/
function flushschedulerQueue() {  
    queue.forEach(watcher => watcher.run()) // 依次触发视图更新  
    queue = [];       // reset  
    has = {};         // reset  
    pending = false;  // reset
}
  • 改造后的代码:
js 复制代码
/** 
* 将 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,获取更新后的 dom

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

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;
}

三,异步更新实现的优化

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

  • 第一次,更新数据时创造了一个 promise
  • 第二次,在 nextTick 中又创造了一个 promise

第一个 promise 先执行;第二个 promise 再执行;

所以第二个拿到的其实是第一个成功后的结果

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

  • 更新数据时,将更新逻辑存起来;
  • 当用户 nextTick 取值时,继续将取值逻辑存起来;

将两个逻辑存到一个数组中,在一个微任务中全部执行并清空即可

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

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 合并在一起;

js 复制代码
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,而非多次

相关推荐
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
不是鱼2 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
开心工作室_kaic3 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育3 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博3 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
isSamle3 小时前
使用Vue+Django开发的旅游路书应用
前端·vue.js·django
ss2734 小时前
基于Springboot + vue实现的汽车资讯网站
vue.js·spring boot·后端
武昌库里写JAVA4 小时前
浅谈怎样系统的准备前端面试
数据结构·vue.js·spring boot·算法·课程设计
TttHhhYy5 小时前
uniapp+vue开发app,蓝牙连接,蓝牙接收文件保存到手机特定文件夹,从手机特定目录(可自定义),读取文件内容,这篇首先说如何读取,手机目录如何寻找
开发语言·前端·javascript·vue.js·uni-app
CodeChampion7 小时前
61.基于SpringBoot + Vue实现的前后端分离-在线动漫信息平台(项目+论文)
java·vue.js·spring boot·后端·node.js·maven·idea