Vue2異步批量更新與computed、watcher原理實現

Vue2響應式原理與實現
Vue2組件掛載與對象數組依賴收集vue

1、實現Vue2生命週期

Vue2中生命週期能夠在建立Vue實例傳入的配置對象中進行配置,也能夠經過全局的Vue.mixin()方法來混入生命週期鉤子,如:node

Vue.mixin({
    a: {
        b: 1
    },
    c: 3,
    beforeCreate () { // 混入beforeCreate鉤子
        console.log("beforeCreate1");
    },
    created () { // 混入created鉤子
        console.log("created1");
    }
});

Vue.mixin({
    a: {
        b: 2
    },
    d: 4,
    beforeCreate () { // 混入beforeCreate鉤子
        console.log("beforeCreate2");
    },
    created () { // 混入created鉤子
        console.log("created2");
    }
});

因此在實現生命週期前,咱們須要實現Vue.mixin()這個全局的方法,將混入的全部生命週期鉤子進行合併以後再到合適的時機去執行生命週期的各個鉤子。咱們能夠將全局的api放到一個單獨的模塊中,如:react

// src/index.js
import {initGlobalApi} from "./globalApi/index";
function Vue(options) {
    this._init(options);
}
initGlobalApi(Vue); // 混入全局的API
// src/globalApi/index.js
import {mergeOptions} from "../utils/index"; // mergeOptions可能會被屢次使用,單獨放到工具類中
export function initGlobalApi(Vue) {
    Vue.options = {}; // 初始化一個options對象並掛載到Vue上
    Vue.mixin = function(options) {
        this.options = mergeOptions(this.options, options); // 將傳入的options對象進行合併,這裏的this就是指Vue
    }
}

接下來就開始實現mergeOptions這個工具方法,該方法能夠合併生命週期的鉤子也能夠合併普通對象,合併的思路很簡單,首先遍歷父對象中的全部屬性對父子對象中的各個屬性合併一次,而後再遍歷子對象找出父對象中不存在的屬性再合併一次,通過兩次合併便可完成父子對象中全部屬性的合併。segmentfault

export function mergeOptions(parent, child) {
    const options = {}; // 用於保存合併結果
    for (let key in parent) { // 遍歷父對象上的全部屬性合併一次
        mergeField(key);
    }
    
    for (let key in child) { // 遍歷子對象上的全部屬性
        if (!Object.hasOwnProperty(parent, key)) { // 找出父對象中不存在的屬性,即未合併過的屬性,合併一次
            mergeField(key);
        }
    }
    return options; // 通過兩次合併便可完成父子對象各個屬性的合併
}

接下來就是要實現mergeField()方法,對於普通對象的合併而言很是簡單,爲了方便,咱們能夠將mergeField()方法放到mergeOptions內部,如:api

export function mergeOptions(parent, child) {
    function mergeField(key) {
        if (isObject(parent[key]) && isObject(child[key])) { // 若是父子對象中的同一個key對應的值都是對象,那麼直接解構父子對象,若是屬性相同,用子對象覆蓋便可
            options[key] = {
                ...parent[key],
                ...child[key]
            }
            
        } else { // 對於不全是對象的狀況,子有就用子的值,子沒有就用父的值
            options[key] = child[key] || parent[key];
        }
    }
}

而對於生命週期的合併,咱們須要將相同的生命週期放到一個數組中,等合適的時機依次執行,咱們能夠經過策略模式實現,如:數組

const stras = {};
const hooks = [
    "beforeCreate",
    "created",
    "beforeMount",
    "mounted"
];
function mergeHook(parentVal, childVal) {
    if (childVal) { // 子存在
        if(parentVal) { // 子存在,父也存在,直接合並便可
            return parentVal.concat(childVal);
        } else { // 子存在,父不存在,一開始父中確定不存在
            return [childVal];
        }
    } else { // 子不存在,直接使用父的便可
        return parentVal;
    }
}
hooks.forEach((hook) => {
    stras[hook] = mergeHook; // 每一種鉤子對應一種策略
});

合併生命週期的時候parent一開始是{},因此確定是父中不存在子中存在,此時返回一個數組並將子對象中的生命週期放到數組中便可,以後的合併父子都有可能存在,父子都存在,那麼直接將子對象中的生命週期鉤子追加進去便可,若是父存在子不存在,直接使用父的便可。緩存

// 往mergeField新增生命週期的策略合併
function mergeField(key) {
    if (stras[key]) { // 若是存在對應的策略,即生命週期鉤子合併
        options[key] = stras[key](parent[key], child[key]); // 傳入鉤子進行合併便可
    } else if (isObject(parent[key]) && isObject(child[key])) {
    
    } else {
        
    }
}

完成Vue.mixin()全局api中的options合併以後,咱們還須要與用戶建立Vue實例時候傳入的options再進行合併,生成最終的options並保存到vm.$options中,如:app

// src/init.js
import {mountComponent, callHook} from "./lifecyle";
export function initMixin(Vue) {
    Vue.prototype._init = function(options) {
        const vm = this;
        // vm.$options = options;
        vm.$options = mergeOptions(vm.constructor.options, options); // vm.constructor就是指Vue,即將全局的Vue.options與用戶傳入的options進行合併
        callHook(vm, "beforeCreate"); // 數據初始化前執行beforeCreate
        initState(vm);
        callHook(vm, "created"); // 數據初始化後執行created
    }
}
// src/lifecyle.js
export function mountComponent(vm, el) {
    callHook(vm, "beforeMount"); // 渲染前執行beforeMount
    new Watcher(vm, updateComponent, () => {}, {}, true);
    callHook(vm, "mounted"); // 渲染後執行mounted
}

咱們已經在合適時機調用了callHook()方法去執行生命週期鉤子,接下來就是實現callHook()方法,即拿到對應鉤子的數組遍歷執行,如:異步

// src/lifecyle.js
export function callHook(vm, hook) {
    const handlers = vm.$options[hook]; // 取出對應的鉤子數組
    handlers && handlers.forEach((handler) => { // 遍歷鉤子
        handler.call(vm); // 依次執行便可
    });
}

2、異步批量更新

目前咱們是每次數據發生變化後,就會觸發set()方法,進而觸發對應的dep對象調用notify()給渲染watcher派發通知,從而讓頁面更新。若是咱們執行vm.name = "react"; vm.name="node",那麼能夠看到頁面會渲染兩次,由於數據被修改了兩次,因此每次都會通知渲染watcher進行頁面更新操做,這樣會影響性能,而對於上面的操做,咱們能夠將其合併成一次更新便可。
其實現方式爲,將須要執行更新操做的watcher先緩存到隊列中,而後開啓一個定時器等同步修改數據的操做完成後,開始執行這個定時器,異步刷新watcher隊列,執行更新操做。
新建一個scheduler.js用於完成異步更新操做,如:函數

// src/observer/scheduler.js
let queue = []; // 存放watcher
let has = {}; // 判斷當前watcher是否在隊列中
let pending = false; // 用於標識是否處於pending狀態
export function queueWatcher(watcher) {
    const id = watcher.id; // 取出watcher的id
    if (!has[id]) { // 若是隊列中尚未緩存該watcher
        has[id] = true; // 標記該watcher已經緩存過
        queue.push(watcher); // 將watcher放到隊列中
        if (!pending) { // 若是當前隊列沒有處於pending狀態
            setTimeout(flushSchedulerQueue, 0); // 開啓一個定時器,異步刷新隊列
            pending = true; // 進入pending狀態,防止添加多個watcher的時候開啓多個定時器
        }
    }    
}
// 刷新隊列,遍歷存儲的watcher並調用其run()方法執行
function flushSchedulerQueue() {
    for (let i = 0; i < queue.length; i++) {
        const watcher = queue[i];
        watcher.run();
    }
    queue = []; // 清空隊列
    has = {};
}

修改watcher.js,須要修改update()方法,update()將再也不當即執行更新操做,而是將watcher放入隊列中緩存起來,由於update()方法已經被另作他用,因此同時須要新增一個run()方法讓wather能夠執行更新操做

// src/observer/watcher.js
import {queueWatcher} from "./scheduler";
export default class Watcher {
    update() {
        // this.get(); // update方法再也不當即執行更新操做
        queueWatcher(this); // 先將watcher放到隊列中緩存起來
    }
    run() { // 代替原來的update方法執行更新操做
        this.get();
    }
}

3、實現nextTick

目前已經實現異步批量更新,可是若是咱們執行vm.name = "react";console.log(document.getElementById("app").innerHTML),咱們從輸出結果能夠看到,拿到innerHTML仍然是舊的,即模板中使用的name值仍然是更新前的。之因此這樣是由於咱們將渲染watcher放到了一個隊列中,等數據修改完畢以後再去異步執行渲染wather去更新頁面,而上面代碼是在數據修改後同步去操做DOM此時渲染watcher尚未執行,因此拿到的是更新前的數據。
要想在數據修改以後當即拿到最新的數據,那麼必須在等渲染Watcher執行完畢以後再去操做DOM,Vue提供了一個$nextTick(fn)方法能夠實如今fn函數內操做DOM拿到最新的數據。
其實現思路就是,渲染watcher進入隊列中後不當即開啓一個定時器去清空watcher隊列,而是將清空watcher隊列的方法傳遞給nextTick函數nextTick也維護一個回調函數隊列將清空watcher隊列的方法添加到nextTick的回調函數隊列中,而後在nextTick中開啓定時器,去清空nextTick的回調函數隊列。因此此時咱們只須要再次調用nextTick()方法追加一個函數,就能夠保證在該函數內操做DOM能拿到最新的數據,由於清空watcher的隊列在nextTick的頭部,最早執行

// src/observer/watcher.js 
export function queueWatcher(watcher) {
    const id = watcher.id; // 取出watcher的id
    if (!has[id]) { // 若是隊列中尚未緩存該watcher
        has[id] = true; // 標記該watcher已經緩存過
        queue.push(watcher); // 將watcher放到隊列中
        // if (!pending) { // 若是當前隊列沒有處於pending狀態
            // setTimeout(flushSchedulerQueue, 0); // 開啓一個定時器,異步刷新隊列
            // pending = true; // 進入pending狀態,防止添加多個watcher的時候開啓多個定時器
        // }
        nextTick(flushSchedulerQueue); // 不是當即建立一個定時器,而是調用nextTick,將清空隊列的函數放到nextTick的回調函數隊列中,由nextTick去建立定時器    
    }    
}

let callbacks = []; // 存放nextTick回調函數隊列
export function nextTick(fn) {
    callbacks.push(fn); // 將傳入的回調函數fn放到隊列中
    if (!pending) { // 若是處於非pending狀態
        setTimeout(flushCallbacksQueue, 0);
        pending = true; // 進入pending狀態,防止每次調用nextTick都建立定時器
    }
}
function flushCallbacksQueue() {
    callbacks.forEach((fn) => {
        fn();
    });
    callbacks = []; // 清空回調函數隊列
    pending = false; // 進入非pending狀態
}

4、實現計算屬性watcher

計算屬性本質也是建立了一個Watcher對象,只不過計算屬性watcher有些特性,好比計算屬性能夠緩存只有依賴的數據發生變化纔會從新計算。爲了可以緩存,咱們須要記錄下watcher的值,須要給watcher添加一個value屬性,當依賴的數據沒有變化的時候,直接從計算watcher的value中取值便可。建立計算watcher的時候須要傳遞lazy: true,標識須要懶加載即計算屬性的watcher。

// src/state.js
import Watcher from "./observer/watcher";
function initComputed(vm) {
    const computed = vm.$options.computed; // 取出用戶配置的computed屬性
    const watchers = vm._computedWatchers = Object.create(null); // 建立一個對象用於存儲計算watcher
    for (let key in computed) { // 遍歷計算屬性的key
        const userDef = computed[key]; // 取出對應key的值,多是一個函數也多是一個對象
        // 若是是函數那麼就使用該函數做爲getter,若是是對象則使用對象的get屬性對應的函數做爲getter
        const getter = typeof userDef === "function" ? userDef : userDef.get;
        watchers[key] = new Watcher(vm, getter, () => {}, {lazy: true}); // 建立一個Watcher對象做爲計算watcher,並傳入lazy: true標識爲計算watcher
        if (! (key in vm)) { // 若是這個key不在vm實例上
            defineComputed(vm, key, userDef); // 將當前計算屬性代理到Vue實例對象上
        }
    }
}

計算屬性的初始化很簡單,就是取出用戶配置的計算屬性執行函數,而後建立計算watcher對象,並傳入lazy爲true標識爲計算watcher。爲了方便操做,還須要將計算屬性代理到Vue實例上,如:

// src/state.js
function defineComputed(vm, key, userDef) {
    let getter = null;
    if (typeof userDef === "function") {
        getter = createComputedGetter(key); // 傳入key建立一個計算屬性的getter
    } else {
        getter = userDef.get;
    }

    Object.defineProperty(vm, key, { // 將當前計算屬性代理到Vue實例對象上
        configurable: true,
        enumerable: true,
        get: getter,
        set: function() {} // 未實現setter
    });
}

計算屬性最關鍵的就是計算屬性的getter,因爲計算屬性存在緩存,當咱們去取計算屬性的值的時候,須要先看一下當前計算watcher是否處於dirty狀態處於dirty狀態才須要從新去計算求值

// src/state.js
function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers[key]; // 根據key值取出對應的計算watcher
        if (watcher) {
            if (watcher.dirty) { // 若是計算屬性當前是髒的,即數據有被修改,那麼從新求值
                watcher.evaluate();
            }
            // watcher計算完畢以後就會將計算watcher從棧頂移除,因此Dep.target會變成渲染watcher
            if (Dep.target) { // 這裏拿到的是渲染Watcher,可是先建立的是計算Watcher,初始化就會建立對應的計算Watcher
                watcher.depend(); // 調用計算watcher的depend方法,收集渲染watcher(將渲染watcher加入到訂閱者列表中)
            }
            return watcher.value; // 若是數據沒有變化,則直接返回以前的值,再也不進行計算
        }
    }
}

這裏最關鍵的就是計算屬性求值完畢以後,須要調用其depend()方法收集渲染watcher的依賴,即將渲染watcher加入到計算watcher所依賴key對應ddep對象的觀察者列表中。好比,模板中僅僅使用到了一個計算屬性:

<div id="app">{{fullName}}</div>
new Vue({
    data: {name: "vue"},
    computed:() {
        return "li" + this.name
    }
});

當頁面開始渲染的時候,即渲染watcher執行的時候,會首先將渲染watcher加入到棧頂,而後取計算屬性fullName的值,此時會將計算watcher加入到棧頂,而後求計算屬性的值,計算屬性依賴了name屬性,接着去取name的值,name對應的dep對象就會將計算watcher放到其觀察者列表中,計算屬性求值完畢後,計算watcher從棧頂移除,此時棧頂變成了渲染watcher,可是因爲模板中只使用到了計算屬性,因此name對應的dep對象並無將渲染watcher放到其觀察者列表中,因此當name值發生變化的時候,沒法通知渲染watcher更新頁面。因此咱們須要在計算屬性求值完畢後遍歷計算watcher依賴的key並拿到對應的dep對象將渲染watcher放到其觀察者列表中

// src/observer/watcehr.js
export default class Watcher {
    constructor(vm, exprOrFn, cb, options, isRenderWatcher) {
            if (options) {
                this.lazy = !!options.lazy;// 標識是否爲計算watcher
            } else {
                this.lazy = false;
            }
            this.dirty = this.lazy; // 若是是計算watcher,則默認dirty爲true
            this.value = this.lazy ? undefined : this.get(); // 計算watcher須要求值,添加一個value屬性
    }
    get() {
        pushTarget(this);
        // this.getter.call(this.vm, this.vm);
        const value = this.getter.call(this.vm, this.vm); // 返回計算結果
        popTarget();
        return value;
    }
    update() {
        // queueWatcher(this); //計算wather不須要當即執行,須要進行區分
        if (this.lazy) { // 若是是計算watcher
            this.dirty = true; // 將計算屬watcher的dirtry標識爲了髒了便可
        } else {
            queueWatcher(this);
        }
    }

    evaluate() {
        this.value = this.get(); // 執行計算watcher拿到計算屬性的值
        this.dirty = false; // 計算屬性求值完畢後將dirty標記爲false,表示目前數據是乾淨的
    }

    depend() { // 由計算watcher執行
        let i = this.deps.length;
        while(i--) { // 遍歷計算watcher依賴了哪些key
            this.deps[i].depend(); // 拿到對應的dep對象收集依賴將渲染watcher添加到其觀察者列表中
        }
    }
}

5、實現用戶watcher

用戶watcher也是一個Watcher對象,只不過建立用戶watcher的時候傳入的是data中的key名而不是函數表達式,因此須要將傳入的key轉換爲一個函數表達式用戶watcher不是在模板中使用,因此用戶watcher關鍵在於執行傳入的回調

// src/state.js
function initWatch(vm) {
    const watch = vm.$options.watch; // 拿到用戶配置的watch
    for (let key in watch) { // 遍歷watch監聽了data中的哪些屬性
        const handler = watch[key]; // 拿到數據變化後的處理回調函數
        new Watcher(vm, key, handler, {user: true}); // 爲用戶watch建立Watcher對象,並標識user: true
    }
}

用戶watcher須要將監聽的key轉換成函數表達式

export default class Watcher {
    constructor(vm, exprOrFn, cb, options, isRenderWatcher) {
        if (typeof exprOrFn === "function") {
        } else {
            this.getter = parsePath(exprOrFn);// 將監聽的key轉換爲函數表達式
        }
        if (options) {
            this.lazy = !!options.lazy; // 標識是否爲計算watcher
            this.user = !!options.user; // 標識是否爲用戶watcher
        } else {
            this.user = this.lazy = false; 
        }
    }
  
    run() {
        const value = this.get(); // 執行get()拿到最新的值
        const oldValue = this.value; // 保存舊的值
        this.value = value; // 保存新值
        if (this.user) { // 若是是用戶的watcher
            try {
                this.cb.call(this.vm, value, oldValue); // 執行用戶watcher的回調函數,並傳入新值和舊值
            } catch(err) {
                console.error(err);
            }
        } else {
            this.cb && this.cb.call(this.vm, oldValue, value); // 渲染watcher執行回調
        }
    }
}
function parsePath(path) {
    const segments = path.split("."); // 若是監聽的key比較深,以點號對監聽的key進行分割爲數組
    return function(vm) { // 返回一個函數
        for (let i = 0; i < segments.length; i++) {
            if (!vm) {
                return;
            }
            vm = vm[segments[i]]; // 這裏會進行取值操做
        }
        return vm;
    }
}

還須要注意的是,dep對象notify方法通知觀察者列表中的watcher執行的時候必須保證渲染watcher最後執行,若是渲染Watcher先執行,那麼當渲染watcher使用計算屬性的時候,求值的時候發現計算watcher的dirty值仍然爲false,致使計算屬性拿到值仍爲以前的值,即緩存的值,必須讓計算watcher先執行將dirty變爲true以後再執行渲染watcher,才能拿到計算屬性最新的值,因此須要對觀察者列表進行排序

因爲計算watcher和用戶watcher在狀態初始化的時候就會建立,而渲染watcher是在渲染的時候纔開始建立,因此咱們能夠按照建立順序進行排序,後面建立的id越大,即按id從小到大進行排序便可。

export default class Dep {
    notify() {
        this.subs.sort((a, b) => a.id - b.id); // 對觀察者列表中的watcher進行排序保證渲染watcher最後執行
        this.subs.forEach((watcher) => {
            watcher.update();
        });
    }
}
相關文章
相關標籤/搜索