vue 源碼自問自答-響應式原理

vue 源碼自問自答-響應式原理

最近看了 Vue 源碼和源碼分析類的文章,感受明白了不少,可是仔細想一想卻說不出個因此然。vue

因此打算把本身掌握的知識,試着組織成本身的語言表達出來dom

不打算平鋪直敘的寫清楚 vue 源碼的來龍去脈和所有細節,而是以自問自答的形式,回答我本身以前的疑惑,異步

若是有錯誤的地方,歡迎指正哈~函數

Vue 的雙向數據綁定原理

Vue 實現響應式的核心 API 是 ES5 的 Object.defineProperty(obj,key,descriptor),Vue 的「響應式」和「依賴收集」都依靠這個 API源碼分析

它接受 3 個參數,分別是 obj / key / 描述符,返回的是一個包裝後的對象性能

它的做用就是,用這個 API 包裝事後的對象能夠擁有 getter 和 setter 函數。this

getter 會在對象的這個 key 被獲取時觸發,setter 會在這個對象的 key 被修改時觸發。code

一個 Vue 項目的開始, 一般是從 Vue 構造函數的實例化開始的。對象

new Vue()的時候會執行一個_init()方法,會初始化屬性,好比 props/event/生命週期鉤子,也包括 data 對象的初始化。遞歸

Vue 在初始化時,將 data 對象上的全部 key,都包裝成擁有 getter 和 setter 的屬性。

  • 渲染頁面時,會執行 render function(不管是用 template 仍是 render 最終都會生成 render 函數)
  • 執行 render function 會獲取 data 對象上的屬性,因此會觸發對應屬性的 getter 函數
  • getter 觸發時,主要就作 2 個事情。 1.把值返回 2.依賴收集,也就是講 watcher 存放到 Dep 實例的一個隊列裏。
  • 當修改對象的值觸發 setter,setter 一樣是作 2 個事情。1.把 newVal 設置好 2.用一個循環通知以前存放在 dep 實例中 watcher 對象們,watcher 對象調用各自的 update 方法來更新視圖

實現雙向數據綁定的demo1 - 忽略「收集依賴」的版本

function cb() {
    console.log("更新視圖");
}

function defineReactve(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            console.log("觸發了getter");
            return val;
        },
        set: newVal => {
            console.log("觸發了setter");
            if (newVal === val) return;
            val = newVal;
            cb()
        }
    });
}

function observe(data) {
    function walk(data) {
        Object.keys(data).forEach(key => {
            if (typeof data[key] === "object") {
                walk(data[key]);
            } else {
                defineReactve(data, key, data[key]);
            }
        });
    }
    walk(data);
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observe(this._data);
    }
}

var vm = new Vue({
    data: {
        msg: "test",
        person: {
            name: "ziwei",
            age: 18
        }
    }
});

vm._data.person.name = 'hello'

// 觸發setter和cb函數,從而視圖更新

實現雙向數據綁定的demo2 - 「收集依賴」的版本

function defineReactve( obj, key, val ) {
            const dep = new Dep()
            Object.defineProperty( obj, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                    console.log( "觸發了getter" );
                    dep.addSub(Dep.target)
                    return val;
                },
                set: newVal => {
                    console.log( "觸發了setter" );
                    if ( newVal === val ) return;
                    val = newVal;
                    dep.notify()  // 通知隊列的wather去update視圖
                }
            } );
        }

        function observe( data ) {
            function walk( data ) {
                Object.keys( data ).forEach( key => {
                    if ( typeof data[ key ] === "object" ) {
                        walk( data[ key ] );
                    } else {
                        defineReactve( data, key, data[ key ] );
                    }
                } );
            }
            walk( data );
        }

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

            addSub(){
                this.subs.push(Dep.target)
            }

            notify(){
                this.subs.forEach(sub => {
                    sub.update()
                })
            }
        }

        Dep.target = null

        class Watcher{
            constructor(){
                Dep.target = this
            }

            update(){
                console.log('update更新視圖啦~')
            }
        }

        class Vue {
            constructor( options ) {
                this._data = options.data;
                observe( this._data );

                new Watcher()           // 模擬頁面渲染,觸發getter,依賴收集的效果
                this._data.person.name
            }
        }

        var vm = new Vue( {
            data: {
                msg: "test",
                person: {
                    name: "ziwei",
                    age: 18
                }
            }
        } );

        vm._data.person.name = 'hello'

一些省略掉的環節

這樣就是 Vue 響應式的一個基本原理,不過我描述的過程當中,也省略了不少環節,好比

  • Vue 是如何實現給 data 對象上的屬性都擁有 getter 和 setter 的
  • 爲何要進行「依賴收集」,
  • 如何避免重複「收集依賴」
  • watcher 調用 update,也並非直接更新視圖。實現上中間還有 patch 的過程以及使用隊列來異步更新的策略。
Vue 是如何實現給 data 對象上的屬性都擁有 getter 和 setter 的
經過循環data對象,給對象的每個key,用Object.defineProperty包裝

遍歷時,若是發現data[key]也是對象的話,須要用遞歸
爲何要進行「依賴收集」?

舉2個場景的栗子🌰

  • 1.若是咱們不進行依賴收集,那頁面裏有2處使用了data上的數據,當這個數據發生變化時,會觸發setter,而後setter當中進行視圖更新。

但問題在於,你並不知道誰依賴我?我應該更新哪幾個地方

  • 2.若是沒有依賴收集,咱們修改了data中的msg,按說咱們要觸發setter更新視圖,可是若是視圖裏並無用到data.msg的話,實際上不該該更新的。
如何避免重複「收集依賴」

在往Dep中放wather對象是,實際上wather的update方法時,會把其放入queue隊列中,會經過watch.id判斷是否重複了,重複的wather就不會被推入隊列

watcher 調用 update,也並非直接更新視圖。實現上中間還有 patch 的過程以及使用隊列來異步更新的策略。

異步更新的策略,相似於setTimeout(fn,0) ,目的就是爲了不頻繁的更新dom,讓頁面的渲染的性能更好。

相關文章
相關標籤/搜索