vue源碼之數(shù)據(jù)響應(yīng)式原理

vue 簡介

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

最核心的是視圖層渲染居灯,然后往外是組件機制,在這個基礎(chǔ)上加入路由機制恭垦,再加入狀態(tài)管理圈匆,以及最外層的構(gòu)建工具漠另。

所謂分層:就是說既可以用最核心的視圖層渲染來開發(fā)一些需求,也可以用vue全家桶來開發(fā)大型應(yīng)用臭脓⌒锍可以更具自己的需求來選擇不同的層級。

數(shù)據(jù)監(jiān)聽(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
            }
        })
    }

這里的函數(shù)defineReactive 用來對Object.defineProperty 進行封裝。從函數(shù)的名字可以看出砚作,其作用是定義一個響應(yīng)式數(shù)據(jù)窘奏。也就是在這個函數(shù)中進行變化追蹤,封裝后只需要傳遞data葫录、keyval 就行了着裹。

封裝好之后,每當從datakey 中讀取數(shù)據(jù)時米同,get 函數(shù)被觸發(fā)骇扇;每當往datakey 中設(shè)置數(shù)據(jù)時,set 函數(shù)被觸發(fā)面粮。

如何收集依賴

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

思考一下稍走,我們之所以要觀察數(shù)據(jù),其目的是當數(shù)據(jù)的屬性發(fā)生變化時柴底,可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方婿脸。

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

該模板中使用了數(shù)據(jù)name,所以當它發(fā)生變化時柄驻,要向使用了它的地方發(fā)送通知狐树。

注意:在Vue.js 2.0 中,模板使用數(shù)據(jù)等同于組件使用數(shù)據(jù)鸿脓,所以當數(shù)據(jù)發(fā)生變化時抑钟,會將通知發(fā)送到組件,然后組件內(nèi)部再通過虛擬DOM重新渲染答憔。

對于上面的問題味赃,先收集依賴,即把用到數(shù)據(jù)name 的地方收集起來虐拓,然后等屬性發(fā)生變化時心俗,把之前收集好的依賴循環(huán)觸發(fā)一遍就好了。

總結(jié)起來蓉驹,其實就一句話城榛,在getter 中收集依賴,在setter 中觸發(fā)依賴态兴。

依賴收集在哪里

思考一下狠持,首先想到的是每個key 都有一個數(shù)組,用來存儲當前key 的依賴瞻润。假設(shè)依賴是一個函數(shù)喘垂,保存在window.target 上甜刻,現(xiàn)在就可以把defineReactive 函數(shù)稍微改造一下:

    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
            }
        })
    }

這里我們新增了數(shù)組dep,用來存儲被收集的依賴正勒。

然后在set 被觸發(fā)時得院,循環(huán)dep 以觸發(fā)收集到的依賴。

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

    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兜喻,那么它到底是什么?我們究竟要收集誰呢赡麦?

收集誰虹统,換句話說,就是當屬性發(fā)生變化后隧甚,通知誰。

我們要通知用到數(shù)據(jù)的地方渡冻,而使用這個數(shù)據(jù)的地方有很多戚扳,而且類型還不一樣,既有可能是模板族吻,也有可能是用戶寫的一個watch帽借,這時需要抽象出一個能集中處理這些情況的類。然后超歌,我們在依賴收集階段只收集這個封裝好的類的實例進來砍艾,通知也只通知它一個。接著巍举,它再負責通知其他地方脆荷。所以,我們要抽象的這個東西需要先起一個好聽的名字懊悯。嗯蜓谋,就叫它 Watcher 吧。

現(xiàn)在就可以回答上面的問題了炭分,收集誰桃焕?Watcher

什么是Watcher

Watcher 是一個中介的角色捧毛,數(shù)據(jù)發(fā)生變化時通知它观堂,然后它再通知其他地方让网。

關(guān)于Watcher,先看一個經(jīng)典的使用方式:

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

這段代碼表示當data.a.b.c 屬性發(fā)生變化時师痕,觸發(fā)第二個參數(shù)中的函數(shù)溃睹。

思考一下,怎么實現(xiàn)這個功能呢七兜?好像只要把這個watcher 實例添加到data.a.b.c 屬性的Dep 中就行了丸凭。然后,當data.a.b.c 的值發(fā)生變化時腕铸,通知Watcher惜犀。接著,Watcher 再執(zhí)行參數(shù)中的這個回調(diào)函數(shù)狠裹。

export default class Watcher {
    constructor (vm, expOrFn, cb) {
        this.vm = vm
        // 執(zhí)行this.getter()虽界,就可以讀取data.a.b.c 的內(nèi)容
        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 設(shè)置成了this莉御,也就是當前watcher 實例,然后再讀一下data.a.b.c 的值俗冻,這肯定會觸發(fā)getter礁叔。

觸發(fā)了getter,就會觸發(fā)收集依賴的邏輯迄薄。而關(guān)于收集依賴琅关,上面已經(jīng)介紹了,會從window.target 中讀取一個依賴并添加到Dep 中讥蔽。

這就導(dǎo)致涣易,只要先在window.target 賦一個this,然后再讀一下值冶伞,去觸發(fā)getter新症,就可以把this 主動添加到keypathDep 中。有沒有很神奇的感覺跋烨荨徒爹?

依賴注入到Dep 中后,每當data.a.b.c 的值發(fā)生變化時金抡,就會讓依賴列表中所有的依賴循環(huán)觸發(fā)update 方法瀑焦,也就是Watcher 中的update 方法。而update 方法會執(zhí)行參數(shù)中的回調(diào)函數(shù)梗肝,將valueoldValue 傳到參數(shù)中榛瓮。

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

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

/**
* 解析簡單路徑
*/
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
        }
   }

可以看到粹懒,這其實并不復(fù)雜重付。先將keypath 用 . 分割成數(shù)組,然后循環(huán)數(shù)組一層一層去讀數(shù)據(jù)凫乖,最后拿到的obj 就是keypath 中想要讀的數(shù)據(jù)确垫。

遞歸偵測所有key

現(xiàn)在,其實已經(jīng)可以實現(xiàn)變化偵測的功能了帽芽,但是前面介紹的代碼只能偵測數(shù)據(jù)中的某一個屬性删掀,我們希望把數(shù)據(jù)中的所有屬性(包括子屬性)都偵測到,所以要封裝一個Observer 類导街。這個類的作用是將一個數(shù)據(jù)內(nèi)的所有屬性(包括子屬性)都轉(zhuǎn)換成getter/setter 的形式披泪,然后去追蹤它們的變化:

    /**
* Observer 類會附加到每一個被偵測的object 上。
* 一旦被附加上搬瑰,Observer 會將object 的所有屬性轉(zhuǎn)換為getter/setter 的形式
* 來收集屬性的依賴款票,并且當屬性發(fā)生變化時會通知這些依賴
*/
export class Observer {
    constructor (value) {
        this.value = value
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }
/**
* walk 會將每一個屬性都轉(zhuǎn)換成getter/setter 的形式來偵測變化
* 這個方法只有在數(shù)據(jù)類型為Object 時被調(diào)用
*/
    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 轉(zhuǎn)換成被偵測的object

然后判斷數(shù)據(jù)的類型翼悴,只有Object 類型的數(shù)據(jù)才會調(diào)用walk 將每一個屬性轉(zhuǎn)換成getter/setter 的形式來偵測變化姆钉。

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

data 中的屬性發(fā)生變化時陶冷,與這個屬性對應(yīng)的依賴就會接收到通知钙姊。

也就是說,只要我們將一個object 傳到Observer 中埂伦,那么這個object 就會變成響應(yīng)式的object煞额。

關(guān)于Object的問題

有些語法即便數(shù)據(jù)發(fā)生了變化,vue.js也監(jiān)測不到沾谜,比如向Object添加和刪除屬性膊毁。

es6 proxy方式監(jiān)聽數(shù)據(jù)響應(yīng)的方式

    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

總結(jié)

變化偵測就是偵測數(shù)據(jù)的變化,當數(shù)據(jù)發(fā)生變化時基跑,要能偵測并發(fā)送出通知婚温。

Object可以通過Object.defineProperty將屬性轉(zhuǎn)換成getter/setter的形式來追蹤變化。讀取數(shù)據(jù)會觸發(fā)getter媳否,修改數(shù)據(jù)會觸發(fā)setter栅螟。

在getter中手機有哪些依賴使用了數(shù)據(jù)荆秦。當setter被觸發(fā)時,通知getter中收集到的依賴數(shù)據(jù)發(fā)生了變化

收集依賴存儲的地方是創(chuàng)建了一個Dep力图,它們用來收集依賴步绸、刪除依賴和向依賴發(fā)送消息等。

依賴就是watcher吃媒,只有watcher觸發(fā)的getter才會收集依賴瓤介,哪個watcher觸發(fā)了getter,就把哪個watcher收集到Dep中赘那。當數(shù)據(jù)發(fā)生變化時刑桑,會循環(huán)依賴列表,把所有的watcher都通知一遍漓概。

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

此外吩蔑,創(chuàng)建一個Observe類,作用是把一個Object中所有數(shù)據(jù)都轉(zhuǎn)換成響應(yīng)式的填抬。

Data烛芬、Observe、Dep和Watcher之間的關(guān)系:Data通過Observe轉(zhuǎn)換成getter/setter的形式來追蹤變化飒责。當外界通過watcher讀取數(shù)據(jù)時赘娄,會觸發(fā)getter從而將watcher添加到依賴中。當數(shù)據(jù)發(fā)生了變化時宏蛉, 會觸發(fā)setter遣臼,從而向Dep中的依賴(watcher)發(fā)送通知。watcher接收到通知后拾并,會向外界發(fā)送通知揍堰,變化通知到外界后可能觸發(fā)視圖更新,也有可能觸發(fā)用戶的某個回調(diào)函數(shù)等嗅义。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屏歹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子之碗,更是在濱河造成了極大的恐慌蝙眶,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褪那,死亡現(xiàn)場離奇詭異械馆,居然都是意外死亡胖眷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門霹崎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來珊搀,“玉大人,你說我怎么就攤上這事尾菇【澄觯” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵派诬,是天一觀的道長劳淆。 經(jīng)常有香客問我,道長默赂,這世上最難降的妖魔是什么沛鸵? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮缆八,結(jié)果婚禮上曲掰,老公的妹妹穿的比我還像新娘。我一直安慰自己奈辰,他們只是感情好栏妖,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奖恰,像睡著了一般吊趾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瑟啃,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天论泛,我揣著相機與錄音,去河邊找鬼蛹屿。 笑死孵奶,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的蜡峰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼朗恳,長吁一口氣:“原來是場噩夢啊……” “哼湿颅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起粥诫,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤油航,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后怀浆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谊囚,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡怕享,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了镰踏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片函筋。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖奠伪,靈堂內(nèi)的尸體忽然破棺而出跌帐,到底是詐尸還是另有隱情,我是刑警寧澤绊率,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布谨敛,位于F島的核電站,受9級特大地震影響滤否,放射性物質(zhì)發(fā)生泄漏脸狸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一藐俺、第九天 我趴在偏房一處隱蔽的房頂上張望炊甲。 院中可真熱鬧,春花似錦紊搪、人聲如沸蜜葱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牵囤。三九已至,卻和暖如春滞伟,著一層夾襖步出監(jiān)牢的瞬間揭鳞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工梆奈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留野崇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓亩钟,卻偏偏與公主長得像乓梨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子清酥,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容