菜鸡手写vue(五)-批处理更新

126 阅读1分钟

背景

当我们修改属性时,会重新渲染更新页面,每一次修改对应一次页面更新,如果某一次操作中进行了多次修改属性,那就有可能产生多次页面更新。而我们想要的结果是在修改了多次属性后,只需进行一次页面更新,并且在更新属性后,可以访问到最新的dom元素vm.$el。

收集需要更新的watcher

当属性发生改变时会调用watcher的update()方法进行页面更新,因此我们可以在这里开始做批处理。收集watcher时做去重处理,不需要重复的watcher。

update(){
    // 批处理更新
    queueWatcher(this);
}

每次调用update方法都会将watcher加入一个队列中存储,相当于将每次修改属性的操作都存储了起来

let has = {};
let queue = [];
let pending = false;        
function flushSchedularQueue(){
    console.log('更新页面');
    queue.forEach(watcher => {
        watcher.run();
    })
    has = {};
    queue = [];
    pending = false;
}

export function queueWatcher(watcher){
    let id = watcher.id;
    if(!has[id]){
        queue.push(watcher);
        has[id] = true;

        // 多次调用queuewatcher,如果watcher不是同一个,就会调用多次nextTick,所以加入pending控制一下,只调用一次nextTick,其他watcher只需加入列表queue
        if(!pending){
            pending = true;
            // 异步更新页面
            nextTick(flushSchedularQueue);
        }
    }
}

nextTick异步调用回调函数

利用nextTick来更新页面,同时将nextTick挂载在vue原型链上抛出去,这样用户也能使用nextTick,就能利用nextTick来执行页面更新完毕后的操作。nextTick的原理:将回调函数存入一个列表中,通常第一个回调函数就是更新页面的逻辑,而后面的回调函数往往是用户定义的逻辑,同步将回调函数加入队列,最后异步执行队列里的所有回调函数。vue2里面在这里( Promise.resolve().then(flushCallbacks) )对执行异步操作其实有一个优雅降价的写法:promise > mutationObserver > setImmdiate > setTimeout,而vue3则是直接使用promise。

let callbacks = [];
let waiting = false;
function flushCallbacks(){
    // 第一次cb渲染watcher更新操作
    // 第二次后面cb是用户传入的回调
    callbacks.forEach(cb => {
        cb();
    })
    callbacks = [];
    waiting = false;
}
export function nextTick(cb){
    callbacks.push(cb);     // 默认的cb是渲染逻辑  用户的逻辑放到渲染逻辑之后,就可以保证用户获取到渲染后的内容

    // 批处理,只有首次会开定时器,后续只需加入callbacks列表中
    if(!waiting){
        waiting = true;

        Promise.resolve().then(flushCallbacks);
    }
}
Vue.prototype.$nextTick = nextTick;

更新数组

将当前数组和watcher关联起来,递归里层数组,让里层数组也收集watcher。

function defineReactive(obj, key, value){
    let childOb = observe(value);   // 递归每一层,监听每一层对象
    let dep = new Dep()     
    Object.defineProperty(obj, key, {
        get(){
            if(Dep.target){
                dep.depend();   
                if(childOb){    // 如果对数组取值 会将当前的watcher和数组关联
                    childOb.dep.depend();
                    if(Array.isArray(value)){
                        dependArray(value);
                    }
                }
            }
            return value;   
        },
        set(newValue){
            if(value === newValue) return;
            observe(newValue);      
            value = newValue;
            dep.notify();           
        }
    })
}
// 让里层数组收集外层数组的依赖,这样修改里层数组也可以更新视图
function dependArray(value){
    for(let i=0; i< value.length; i++){
        let current = value[i];
        current._ob_ && current._ob_.dep.depend();  // 让里层和外层都是收集同一个watcher
        if(Array.isArray(current)){
            dependArray(current);
        }
    }
}

当数组内容发生改变时,通知更新视图。

methodList.forEach(method => {
    newArray[method] = function(...args){
        let result = Array.prototype[method].apply(this, args);
        let inserted = null;
        const ob = this._ob_;
        switch(method){
            case 'splice':
                inserted = args.slice(2);
                break;
            case 'unshift':
            case 'push':
                inserted = args;
                break
            default:
                break;
        }
        if(inserted){
            ob.observeArray(inserted);
        }
        // 通知更新
        ob.dep.notify();
        return result
    }
})

效果

这样多次修改message数据,最终也只是更新一次页面,同时也可以利用$nextTick访问到更新页面后的内容。

const vm = new Vue({
    el: '#app',
    data: {
        message: 'hello',
        people: {
            name: 'lily',
        },
        bookList: ['哈利波特', '鲤鱼历险记', '老人与海'],
    }
})

setTimeout(() => {
    console.log('一秒后');
    vm._data.message = 'hahaha';
    vm._data.message = '什么玩意';
    vm._data.message = '笑死';
    vm._data.message = '啊这';

    vm.$nextTick(() => {
        console.log(vm.$el);
    })
}, 1000);

理解

批处理更新其实就是将多次渲染只执行一次渲染,有点像防抖但又不是防抖,他是巧妙的利用watcher、同步代码和异步代码来实现的,更新操作放在异步代码里,利用同步代码收集所有watcher,所以只有watcher收集完毕才会去执行异步代码更新页面。数组更新方面有点绕,比较难理解。