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 把計算屬性變成響應式的話,計算屬性會執行自己的依賴,從而和響應式數據的依賴重復了。其實這也是把非數據變成響應式的一種方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM