1. 程式人生 > 實用技巧 >Vue2.x計算屬性為什麼能依賴於另一個計算屬性

Vue2.x計算屬性為什麼能依賴於另一個計算屬性

概述

說到 computed 和 watch 有什麼不同,也許大多數人都知道:computed 是用現有資料生成一個新資料,並且能夠被快取;而 watch 是根據資料變化,執行一些回撥函式,它有很多配置比如 deep、immediate 等。

大家也都知道,watch 只是原始碼裡面 watcher 的一個例項,computed 屬性也用到了 watcher,但是 computed 屬性為什麼能夠相互依賴變化呢?明顯 watcher 自己是做不到這一點的,因為 watcher 並不能 update 其它 watcher。我為了弄懂其中的原理根據 vue2.x 的原始碼寫了一個簡易的 computed 屬性,供以後工作時參考,相信對其他人也有用。

部分程式碼來源於Vue2.x是怎麼收集依賴的

簡易的 computed

為了簡便,暫不考慮 computed 的 setter 的情況,我實現了一個簡易的 computed,程式碼如下:

function defineReactive(obj, key, val) {
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set(newVal) {
            val = newVal;
            dep.notify();
        }
    });
}

class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub() {
        const index = this.subs.indexOf(sub);
        if (index > -1) {
            this.subs.splice(index, 1);
        }
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }

    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

Dep.target = null;
const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

class Watcher {
    constructor(cb, dirty = false) {
        this.getter = cb;
        this.deps = [];
        this.newDeps = [];
        this.value = this.get();
        this.dirty = dirty;
    }

    get() {
        pushTarget(this);
        const value = this.getter();
        popTarget(this);
        this.deps = [...this.newDeps];
        this.newDeps = [];
        return value;
    }

    addDep(dep) {
        this.newDeps.push(dep);
        dep.addSub(this);
    }

    update() {
        this.dirty = true;
        this.value = this.get();
    }

    evaluate() {
        this.value = this.get();
        this.dirty = false;
    }

    depend() {
        let i = this.deps.length;

        while (i--) {
            this.deps[i].depend();
        }
    }
}

const obj = {};
defineReactive(obj, 'text', 'Hello World!');

const vm = {};
const computed = {
    text1() {
        return `${obj.text}-text1`;
    },
    text2() {
        return `${vm.text1}-text2`;
    }
};

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = vm.computedWatchers[key];

        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }

            if (Dep.target) {
                watcher.depend();
            }

            return watcher.value;
        }
    }
}

function defineCompute(target) {
    const watchers = vm.computedWatchers = Object.create(null);

    for (key in target) {
        const cb = target[key];
        watchers[key] = new Watcher(cb, true);

        //defineComputed
        Object.defineProperty(vm, key, {
            get: createComputedGetter(key),
            set(a) {
                return a;
            }
        });
    }
}

defineCompute(computed);

const watcher = new Watcher(() => {
    document.querySelector('body').innerHTML = vm.text2;
});

把上面的程式碼複製到瀏覽器的控制檯執行,就可以看到瀏覽器裡面出現了Hello World-text1-text2,然後我們繼續在控制檯輸入obj.text = 'Define Reactive',可以看到瀏覽器裡面的Hello World-text1-text2就變成了Define Reactive-text1-text2

顯然,由於我們改變了obj.text的值,然後自動的導致了vm.text1vm.text2的值發生了響應式變化。

而其中的原理是,假如計算屬性 A 依賴計算屬性 B,而計算屬性 B 又依賴響應式資料 C,那麼最一開始先把計算屬性 AB 都轉化為 watcher,然後在把計算屬性 AB 掛載到 vm 上面的時候,插入了一段 getter,而計算屬性 B 的這個 getter 在這個計算屬性 B 被讀取的時候會把計算屬性 A 的 watcher 新增到響應式資料 C 的依賴裡面,所以響應式資料 C 在改變的時候會先後導致計算屬性 B 和 A 執行 update,從而發生改變。

而其中關鍵的那段程式碼就是這段:

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = vm.computedWatchers[key];

        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }

            // 這裡非常關鍵
            if (Dep.target) {
                watcher.depend();
            }

            return watcher.value;
        }
    }
}

為什麼在計算屬性 B 的 getter 函式裡面會新增計算屬性 A 的 watcher 呢?這是因為計算屬性 B 在求值完成後,會自動把Dep.target出棧,從而暴露出計算屬性 A 的 watcher。程式碼如下:

class Watcher {
    get() {
        // 這裡把自己的 watcher 入棧
        pushTarget(this);
        const value = this.getter();
        // 這裡把自己的 watcher 出棧
        popTarget(this);
        this.deps = [...this.newDeps];
        this.newDeps = [];
        return value;
    }
}

這就是 pushTarget 和 popTarget 排程 watchers 的美麗之處~~

其它

需要注意以下兩點:

1.在給計算屬性生成 getter 的時候,不能直接使用 Object.defineProperty,而是使用閉包把 key 值儲存了起來。

2.為什麼不直接使用 defineReactive 把計算屬性變成響應式的。因為當把計算屬性用 setter 掛載到 vm 上面的時候,計算屬性這裡確實變成了一個具體的值,但是如果使用 defineReactive 把計算屬性變成響應式的話,計算屬性會執行自己的依賴,從而和響應式資料的依賴重複了。其實這也是把非資料變成響應式的一種方法。