Vue 源碼學習(一)

vue 無疑是一個很是棒的前端MVVM庫,懷着好奇的心情開始看VUE源碼,固然遇到了不少的疑問,也查了不少的資料看了一些文章。可是這些資料不少都忽略了很重要的部分或者是一些重要的細節,亦或是一些很重要的部分沒有指出,特別是在computed的實現上。因此纔打算寫這篇文章,記錄一下本身的學習過程,固然也但願能給其餘想了解VUE源碼的童鞋一點參考。若是筆者在某些地方理解有誤,也歡迎批評指正出來,一塊兒學習。html

爲了加深理解,我按着源碼的思路造了一個簡易的輪子,基本核心的實現是與VUE源代碼一致。測試 demo。倉庫的地址:eltonchan/rollup-ts前端

VUE的源碼採用rollup flow至於爲何不採用typescript,主要考慮工程上成本和收益的考量, 這一點尤大在知乎也有說過。(3.0+版本肯定改用typesript)vue

Vue 2.0 爲何選用 Flow 進行靜態代碼檢查而不是直接使用TypeScriptnode

不懂rollup 與typescript 也不要緊,本項目已經配置好了, 只須要先執行npm i (或者cnpm i)安裝相應依賴,而後 npm start 啓動就能夠。 npm run build 構建,默認是輸出umd格式,若是須要cmd或者amd 能夠在rollup.config.js配置文件修改。git

output: {
        file: 'dist/bundle.js',
        format: 'umd',
        name: 'myBundle',
        sourcemap: true
    }
複製代碼

questions ? 帶着問題去了解一個事物每每能帶來更好的收益,那咱們就從下面幾個問題開始github

  • 如何對http://this.xxx的訪問代理到http://this._data.xxx 上 ?
  • 如何實現數據劫持,監聽數據的讀寫操做 ?
  • 如何實現依賴緩存 ?
  • template 改變的時候 如何清理依賴項集合? eg: v-if 、組件銷燬
  • 如何實現數據修改 dom更新 ?

vue實現雙向綁定原理,主要是利用Object.defineProperty getter/setter(事實上,大多數響應式編程的庫都是利用這個實現的,好比很是棒的mobx.js)和發佈訂閱模式(定義了對象間的一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將得到通知),而在vue中,watcher 就是訂閱者,而一對多的依賴關係 就是指data上的屬性與watcher,而data上的屬性如何與watcher 關聯起來, dep 就是橋樑, 因此搞懂 dep, watcher, observe三者的關係,天然就搞懂了vue實現雙向綁定的原理了。typescript

總體的流程圖


1、 Proxy 回到第一個問題, 答案實際上是:對於每個 data 上的key,都在 vm 上作一個代理,實際操做的是 this._data、 實現的代碼以下:express

export function proxy (target: IVue, sourceKey: string, key: string) {
    Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get() {
            return this[sourceKey][key];
        },

        set(val: any) {
            this[sourceKey][key] = val;
        }
    });
}
複製代碼

能夠看出獲取和修改this.xx 都是在獲取或者修改this.data.xx;npm


2、Observer 用於把data上的屬性封裝成可觀察的屬性 用Object.defineProperty來攔截對象的讀寫gettet/setter操做, 在獲取的時候收集依賴, 在修改的時候通知相關的依賴。編程

walk(data): void {
        if (!data || typeof data !== 'object') return;
        Object.keys(data).forEach(key => {
            this.defineReactive({
                data,
                key, 
                value: data[key]
            });
        });
    }

    defineReactive({ data, key, value }: IReactive): void {
        const dep = new Dep();
        this.walk(value);
        const self = this;
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get() {
                if (Dep.target) {
                    Dep.target.addDep(dep);
                }
                return value;
            },
    
            set(newVal: any): void {
                if (value === newVal) return;
                self.walk(value);
                value = newVal;
                dep.notify();
            }
    
        });
    }
複製代碼

能夠看出, 在get的時候收集依賴,而Dep.target 其實就是watcher, 等下講到watcher的時候再回過來, 這裏要關注dep 其實dep在這裏是一個閉包環境,在執行get 或者set的時候 還能夠訪問到建立的dep. 好比 this.name當在獲取this.name的值的時候 會建立一個Dep的實例, 把watcher 添加到這個dep中。

爲何對象上新增屬性不會監聽,而修改整個對象爲何能檢測到子屬性的變化 ?

因爲 JavaScript 的限制,Vue 不能檢測對象屬性的添加或刪除(固然mobx也不例外的)。因此可觀察的對象屬性的添加或者刪除沒法觸發set 方法,而直接修改對象則能夠,而在set 方法中則會判斷新值是不是對象數組類型,若是是 則子屬性封裝成可觀察的屬性,這也是set中self.walk(value);的做用。


3、Watcher 剛纔提到了watcher,從上圖中也能夠看到了watcher的做用 事實上,每個computed屬性和watch 都會new 一個 Watcher。接下來會講到。先來看watcher的實現。

constructor(
        vm: IVue,
        expression: Function | string,
        cb: Function,
    ) {
        this.vm = vm;
        vm._watchers.push(this);

        this.cb = cb || noop;

        this.id = ++uid;

        // 處理watch 的狀況
        if (typeof expression === 'function') {
            this.getter = expression;
        } else {
            this.getter = () => vm[expression];
        }

        this.expression = expression.toString();

        this.depIds = new Set();
        this.newDepIds = new Set();
        this.deps = [];
        this.newDeps = [];

        this.value = this.get();
    }
複製代碼

這裏的expression,對於初始化用來渲染視圖的watcher來講,就是render方法,對於computed來講就是表達式,對於watch纔是key,因此這邊須要判斷是字符串仍是函數,而getter方法是用來取value的。這邊有個depIds,deps,可是又有個newDepIds,newDeps,爲何這樣設計,接下去再講,先看this.value = this.get();能夠看出在這裏給watcher的value賦值,再來看get方法。

get() :void {
        Dep.target = this;
        const value = this.getter.call(this.vm); // 執行一次get 收集依賴
        Dep.target = null;
        this.cleanupDeps(); // 清除依賴
        return value;
    }
複製代碼

能夠看到getter是用來取值的,當執行這一行代碼的時候,以render的那個watcher爲例,會執行VNode render 當遇到{{ msg }}的表達式的時候會取值,這個時候會觸發msg的get方法,而此時的Dep.target 就是這個watcher, 因此咱們會把這個render的watcher和msg這個屬性關聯起,也就是msg的dep已經有render的這個watcher了。這個就是Watcher,Dep,Observer的關係。咱們再來看Dep:

export default class Dep implements IDep {
    static target: any = null;
    subs:any = [];
    id;

    constructor () {
        this.id = uid++;
        this.subs = [];
    }

    addSub(sub: IWatcher): void {
        if (this.subs.find(o => o.id === sub.id)) return;
        this.subs.push(sub);
    }

    removeSub (sub: IWatcher) {
        const idx = this.subs.findIndex(o => o.id === sub.id);
        if (idx >= 0) this.subs.splice(idx, 1);
    }

    notify():void {
        this.subs.forEach((sub: Isub) => {
            sub.update();
        })
    }

    depend () {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }
}
複製代碼

Dep的實現很簡單,這邊看notify的方法,咱們知道在修改data上的屬性的時候回觸發set,而後觸發notify方法,而後咱們知道sub就是watcher,因此watcher.update方法就是修改屬性所執行的方法,回到watcher看這個update的實現。

update() {
        // 推送到觀察者隊列中,下一個tick時調用。*/
        queueWatcher(this);
    }

    run(cb) {
        const value = this.get();
        if (value !== this.value) {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.vm, value, oldValue);
        }
    }
複製代碼

update方法並無直接render vNode。而是把watcher推到一個隊列中,事實上vue是的更新dom是異步的,爲何要異步更新隊列,這邊摘抄了一下官網的描述:Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做上很是重要。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,若是執行環境不支持,會採用 setTimeout(fn, 0) 代替。其實這是一種很是好的性能優化方案,咱們設想一下若是在mounted中循環賦值,若是不採用異步更新策略,每個賦值都更新,徹底是一種浪費。

4、nextTick 關於nextTick其實不少文章寫的都不錯,這邊就不詳細介紹了。涉及到的概念能夠點擊下面連接查看:

JavaScript 運行機制詳解:再談Event Loop

關於 macrotask 和 microtask


5、Computed 計算屬性是基於它們的依賴進行緩存的。只在相關依賴發生改變時它們纔會從新求值 回到開始的那個問題,如何實現依賴緩存? name的更新如何讓info也更新,若是name不變,info如何取值?

剛纔在講watcher的時候,提到過每一個computed會實例化一個Watcher,從下面代碼中也能夠看出來,每個computed屬性都有一個訂閱者watcher。

initComputed(computed) {
        if (!computed || typeof computed !== 'object') return;
        const keys = Object.keys(computed);
        const watchers = this._computedWatchers;
        let i = keys.length;
        while(i--) {
            const key = keys[i];
            const func = computed[key];
            watchers[key] = new Watcher(
                this,
                func || noop,
                noop,
            );

            defineComputed(this, key);
        }
    }
複製代碼

看這個例子:

computed: {
        info() {
            console.info('computed update');
            return this.name + 'hello';
        }
    },
複製代碼

watcher 的getter方法就是computed屬性的表達式,而在執行this.value = this.get();這個value就會是表達式的運行結果,因此其實Vue是把info的值存儲在它的watcher的value裏面的,而後又知道在取name的值的時候,會觸發name的get方法,此時的Dep.target 就是這個info的watcher,而dep是一個閉包,仍是以前收集name的那個dep, 因此name的dep就會有兩個watcher,[renderWatcher, computedWatcher], 當name更新的時候,這兩個訂閱者watcher都會收到通知,這也就是name的更新讓info也更新。

那info的值是watcher的value, 因此這邊要作一個代理,把computed屬性的取值代理到對應watcher的value,實現起來也很簡單。

export default function defineComputed(vm: IVue, key: string) {
    Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        get() {
            const watcher = vm._computedWatchers && vm._computedWatchers[key];
            return watcher.value;
        },
    });
}
複製代碼

6、依賴更新

<p v-if="switch">{{ name }}</p>
複製代碼

假設switch由true切換成false時候,是須要把name上面的renderWatcher刪除掉的,因此須要用depIds和deps的屬性來記錄dep。

addDep(dep: Dep) {
        const id = dep.id;
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);
            }
        }
    }

    cleanupDeps() {
        let i = this.deps.length;
        while (i--) {
            const dep = this.deps[i];
            if (!this.depIds.has(dep.id)) {
                dep.removeSub(this);
            }
        }

        const tmp = this.depIds;
        this.depIds = this.newDepIds;
        this.newDepIds = tmp;
        this.newDepIds.clear();

        const deps = this.deps;
        this.deps = this.newDeps;
        this.newDeps = deps;
        this.newDeps.length = 0;
    }
複製代碼

這裏把newDepIds賦值給了depIds, 而後newDepIds再清空,deps也是這樣的操做,這是一種效率很高的操做,避免使用了深拷貝。添加依賴的時候都是用newDepIds,newDeps來記錄,刪除的時候會去deps裏面遍歷查找,等刪除了再把newDepIds賦值給depIds,這樣能保證在更新依賴的時候,沒有使用的依賴會從這個watcher中移除。

7、watch 爲何watch 一個對象的時候 oldValue == value ?

watch的屬性也是一個實例化的Watcher,只是這個時候的expression是key,value 是vm[key],而cb就是回調函數,因此這個時候對應屬性的dep中天然就有這個watcher。

initWatch(watch) {
        if (!watch || typeof watch !== 'object') return;
        const keys = Object.keys(watch);
        let i = keys.length;

        while(i--) {
            const key = keys[i];
            const cb = watch[key];
            new Watcher(this, key, cb);
        }
    }
複製代碼

當屬性更新的時候,會執行到這個run方法, 當watch一個對象的時候,watcher的value實際上是一個引用,修改這個屬性的時候,this.value也同步修改了,因此也就是爲何oldValue == value了, 至於做者爲何這麼設計,我想確定是有他緣由的。

run(cb) {
        const value = this.get();
        if (value !== this.value) {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.vm, value, oldValue);
        }
    }
複製代碼

8、Compile vue 2+ 已經使用VNode了,這部分尚未細緻研究過,因此我這邊本身寫了個簡易的Compile,這部分已經和源碼沒有關係了。主要用到了DocumentFragment和閉包而已,有興趣的童鞋能夠到這個倉庫查看。

components vnode 待補充...

相關文章
相關標籤/搜索