【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,而非多次

相关推荐
_codeOH7 小时前
Vue 3 vs React 19:框架还在卷,核心原理就这些
前端·vue.js
英勇无比的消炎药8 小时前
新手必看玩转TinyRobot一定要避开这些坑
前端·vue.js
英勇无比的消炎药8 小时前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
英勇无比的消炎药10 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药11 小时前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo11 小时前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰11 小时前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·11 小时前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
rising start12 小时前
九、vue3 组件通信:全场景详解
前端·vue.js·typescript
编程技术手记12 小时前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js