vue源碼之數據響應式原理

vue 簡介

漸進式框架:就是把框架分層。

最核心的是視圖層渲染,然后往外是組件機制,在這個基礎上加入路由機制,再加入狀態管理,以及最外層的構建工具。

所謂分層:就是說既可以用最核心的視圖層渲染來開發一些需求,也可以用vue全家桶來開發大型應用。可以更具自己的需求來選擇不同的層級。

數據監聽(Object)

有兩種方法可以偵測到變化:使用Object.definePropertyES6Proxy

    function defineReactive(data, key ,val) {
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                return val
            },
            set: function(newVal) {
                if(val === newVal) {
                    return;
                }
                val = newVal
            }
        })
    }

這里的函數defineReactive 用來對Object.defineProperty 進行封裝。從函數的名字可以看出,其作用是定義一個響應式數據。也就是在這個函數中進行變化追蹤,封裝后只需要傳遞datakeyval 就行了。

封裝好之后,每當從datakey 中讀取數據時,get 函數被觸發;每當往datakey 中設置數據時,set 函數被觸發。

如何收集依賴

如果只是把Object.defineProperty 進行封裝,那其實并沒什么實際用處,真正有用的是收集依賴。

思考一下,我們之所以要觀察數據,其目的是當數據的屬性發生變化時,可以通知那些曾經使用了該數據的地方。

    <template>
        <h1>{{ name }}</h1>
    </template>

該模板中使用了數據name,所以當它發生變化時,要向使用了它的地方發送通知。

注意:在Vue.js 2.0 中,模板使用數據等同于組件使用數據,所以當數據發生變化時,會將通知發送到組件,然后組件內部再通過虛擬DOM重新渲染。

對于上面的問題,先收集依賴,即把用到數據name 的地方收集起來,然后等屬性發生變化時,把之前收集好的依賴循環觸發一遍就好了。

總結起來,其實就一句話,在getter 中收集依賴,在setter 中觸發依賴。

依賴收集在哪里

思考一下,首先想到的是每個key 都有一個數組,用來存儲當前key 的依賴。假設依賴是一個函數,保存在window.target 上,現在就可以把defineReactive 函數稍微改造一下:

    function defineReactive(data, key, val) {
        let dep = [];
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                dep.push(window.target) // 新增
                return val
            },
            set(newVal) {
                if(val === newVal) {
                    return;
                }
                // 新增
                for (let i = 0; i < dep.length; i++) {
                    dep[i](newVal, val)
                }
                val = newVal
            }
        })
    }

這里我們新增了數組dep,用來存儲被收集的依賴。

然后在set 被觸發時,循環dep 以觸發收集到的依賴。

但是這樣寫有點耦合,我們把依賴收集的代碼封裝成一個Dep 類,它專門幫助我們管理依賴。使用這個類,我們可以收集依賴、刪除依賴或者向依賴發送通知等。其代碼如下:

    export default class Dep {
        constructor() {
            this.subs = []
        }
        addSub (sub) {
            this.subs.push(sub)
        }
        removeSub (sub) {
            remove(this.subs, sub)
        }
        depend () {
            if (window.target) {
                this.addSub(window.target)
            }
        }
        notify() {
            const subs = this.subs.slice();
            for(let i = 0, l = subs.length; i < l; i++) {
                subs[i].update()
            }
        }
    }
    
    function remove (arr, item) {
        if (arr.length) {
            const index = arr.indexOf(item)
            if (index > -1) {
                return arr.splice(index, 1)
            }
        }
    }

之后再改造下defineReactive:

    function defineReactive (data, key, val) {
        let dep = new Dep() // 修改
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                dep.depend() // 修改
                return val
            },
            set: function (newVal) {
                if(val === newVal){
                    return
                }
                val = newVal
                dep.notify() // 新增
            }
        })
    }

依賴是誰

在上面的代碼中,我們收集的依賴是window.target,那么它到底是什么?我們究竟要收集誰呢?

收集誰,換句話說,就是當屬性發生變化后,通知誰。

我們要通知用到數據的地方,而使用這個數據的地方有很多,而且類型還不一樣,既有可能是模板,也有可能是用戶寫的一個watch,這時需要抽象出一個能集中處理這些情況的類。然后,我們在依賴收集階段只收集這個封裝好的類的實例進來,通知也只通知它一個。接著,它再負責通知其他地方。所以,我們要抽象的這個東西需要先起一個好聽的名字。嗯,就叫它 Watcher 吧。

現在就可以回答上面的問題了,收集誰?Watcher

什么是Watcher

Watcher 是一個中介的角色,數據發生變化時通知它,然后它再通知其他地方。

關于Watcher,先看一個經典的使用方式:

    // keypath
    vm.$watch('a.b.c', function (newVal, oldVal) {
    // 做點什么
    })

這段代碼表示當data.a.b.c 屬性發生變化時,觸發第二個參數中的函數。

思考一下,怎么實現這個功能呢?好像只要把這個watcher 實例添加到data.a.b.c 屬性的Dep 中就行了。然后,當data.a.b.c 的值發生變化時,通知Watcher。接著,Watcher 再執行參數中的這個回調函數。

export default class Watcher {
    constructor (vm, expOrFn, cb) {
        this.vm = vm
        // 執行this.getter(),就可以讀取data.a.b.c 的內容
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    get() {
        window.target = this
        let value = this.getter.call(this.vm, this.vm)
        window.target = undefined
        return value
    }
    update () {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    } 
}

這段代碼可以把自己主動添加到data.a.b.cDep 中去,是不是很神奇?

因為我在 get 方法中先把 window.target 設置成了this,也就是當前watcher 實例,然后再讀一下data.a.b.c 的值,這肯定會觸發getter

觸發了getter,就會觸發收集依賴的邏輯。而關于收集依賴,上面已經介紹了,會從window.target 中讀取一個依賴并添加到Dep 中。

這就導致,只要先在window.target 賦一個this,然后再讀一下值,去觸發getter,就可以把this 主動添加到keypathDep 中。有沒有很神奇的感覺啊?

依賴注入到Dep 中后,每當data.a.b.c 的值發生變化時,就會讓依賴列表中所有的依賴循環觸發update 方法,也就是Watcher 中的update 方法。而update 方法會執行參數中的回調函數,將valueoldValue 傳到參數中。

所以,其實不管是用戶執行的vm.$watch('a.b.c', (value, oldValue) => {}),還是模板中用到的data,都是通過Watcher 來通知自己是否需要發生變化。

這里有些小伙伴可能會好奇上面代碼中的parsePath 是怎么讀取一個字符串的keypath 的,下面用一段代碼來介紹其實現原理:

/**
* 解析簡單路徑
*/
const bailRE = /[^w.$]/
export function parsePath (path) {
    if (bailRE.test(path)) {
        return
    }
    const segments = path.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return
                obj = obj[segments[i]]
            }
            return obj
        }
   }

可以看到,這其實并不復雜。先將keypath 用 . 分割成數組,然后循環數組一層一層去讀數據,最后拿到的obj 就是keypath 中想要讀的數據。

遞歸偵測所有key

現在,其實已經可以實現變化偵測的功能了,但是前面介紹的代碼只能偵測數據中的某一個屬性,我們希望把數據中的所有屬性(包括子屬性)都偵測到,所以要封裝一個Observer 類。這個類的作用是將一個數據內的所有屬性(包括子屬性)都轉換成getter/setter 的形式,然后去追蹤它們的變化:

    /**
* Observer 類會附加到每一個被偵測的object 上。
* 一旦被附加上,Observer 會將object 的所有屬性轉換為getter/setter 的形式
* 來收集屬性的依賴,并且當屬性發生變化時會通知這些依賴
*/
export class Observer {
    constructor (value) {
        this.value = value
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }
/**
* walk 會將每一個屬性都轉換成getter/setter 的形式來偵測變化
* 這個方法只有在數據類型為Object 時被調用
*/
    walk (obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
function defineReactive (data, key, val) {
    // 新增,遞歸子屬性
    if (typeof val === 'object') {
        new Observer(val)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend()
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            val = newVal
            dep.notify()
        }
    })
}

在上面的代碼中,我們定義了Observer 類,它用來將一個正常的object 轉換成被偵測的object

然后判斷數據的類型,只有Object 類型的數據才會調用walk 將每一個屬性轉換成getter/setter 的形式來偵測變化。

最后,在defineReactive 中新增new Observer(val)來遞歸子屬性,這樣我們就可以把data 中的所有屬性(包括子屬性)都轉換成getter/setter 的形式來偵測變化。

data 中的屬性發生變化時,與這個屬性對應的依賴就會接收到通知。

也就是說,只要我們將一個object 傳到Observer 中,那么這個object 就會變成響應式的object

關于Object的問題

有些語法即便數據發生了變化,vue.js也監測不到,比如向Object添加和刪除屬性。

es6 proxy方式監聽數據響應的方式

    let obj = {
        a: 1,
        b: 2,
        c: 3
    }
    
    let reactive = new Proxy(obj, {
        get: function(target, key, receiver) {
            console.log(`getting ${key}`);
            return Reflect.get(target, key, receiver)
        }
        set: function(target, key, receiver) {
            console.log(`setting ${key}`);
            return Reflect.set(target, key, receiver)
        }
    })
    
    
    reactive.a      // getting a  // 1
    reactive.a = 4  // setting a
    reactive.a      // getting a  // 4

總結

變化偵測就是偵測數據的變化,當數據發生變化時,要能偵測并發送出通知。

Object可以通過Object.defineProperty將屬性轉換成getter/setter的形式來追蹤變化。讀取數據會觸發getter,修改數據會觸發setter。

在getter中手機有哪些依賴使用了數據。當setter被觸發時,通知getter中收集到的依賴數據發生了變化

收集依賴存儲的地方是創建了一個Dep,它們用來收集依賴、刪除依賴和向依賴發送消息等。

依賴就是watcher,只有watcher觸發的getter才會收集依賴,哪個watcher觸發了getter,就把哪個watcher收集到Dep中。當數據發生變化時,會循環依賴列表,把所有的watcher都通知一遍。

watcher的原理是先把自己設置到全局唯一的指定位置(例如window.target),然后讀取數據。因為讀取了數據,所以會觸發這個數據的getter。接著在getter中就會從全局唯一的window.target讀取當前正在讀取數據的watcher,并收集這個watcher到Dep中。

此外,創建一個Observe類,作用是把一個Object中所有數據都轉換成響應式的。

Data、Observe、Dep和Watcher之間的關系:Data通過Observe轉換成getter/setter的形式來追蹤變化。當外界通過watcher讀取數據時,會觸發getter從而將watcher添加到依賴中。當數據發生了變化時, 會觸發setter,從而向Dep中的依賴(watcher)發送通知。watcher接收到通知后,會向外界發送通知,變化通知到外界后可能觸發視圖更新,也有可能觸發用戶的某個回調函數等。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容