快速看懂Vue雙向數(shù)據(jù)綁定原理

單向綁定非常簡單,就是把Model綁定到View囊咏,當我們用JavaScript代碼更新Model時恕洲,View就會自動更新。
有單向綁定匆笤,就有雙向綁定研侣。如果用戶更新了View,Model的數(shù)據(jù)也自動被更新了炮捧,這種情況就是雙向綁定庶诡。
這么個能讓人從dom操作解放出來渾身通泰的東西,能不研究一下它的原理咆课?


Vue源碼的英文解釋很詳細末誓。以下代碼,僅僅用于原理的說明书蚪。
參考滴滴商業(yè)FED
閱讀順序建議粗略過代碼喇澡,對照著思路再看代碼。

兩個核心

在研究之前殊校,得先明白了Vue實現(xiàn)數(shù)據(jù)綁定的兩個核心理念晴玖,即:

  • Object.defineProperty()
    監(jiān)聽數(shù)據(jù)的變動
  • 觀察者(發(fā)布-訂閱者)模式
    數(shù)據(jù)對應的邏輯操作
    它們的關系又是如何?
    一句話描述,一個頁面在多處訂閱使用了同一個數(shù)據(jù)呕屎,用defineProperty監(jiān)聽其改變让簿,并由發(fā)布者通知 訂閱者去更新它所持有的數(shù)據(jù)。

關鍵字get/set

使用 Object.defineProperty()get/set 對傳入new Vue({})所有數(shù)據(jù)對象做一個數(shù)據(jù)監(jiān)聽秀睛,用于在屬性獲取(get)和設置(set)時尔当,添加對應的邏輯。


  // ---------------數(shù)據(jù)監(jiān)聽----------------------
  observe = function(value){
    // 是否監(jiān)聽
    if(!value || typeof value !== 'object'){
        return
    }
    return new Observer(value)
  }
  // ------
  class Observer{
    constructor(value){
        this.value = value
        this.walk(value)
    }
    walk(value){   //監(jiān)聽數(shù)據(jù)的所有屬性
        Object.keys(value).forEach(key => this.convert(key, value[key]))
    }
    convert(key, val){
        defineReactive(this.value, key, val)
    }
  }
  defineReactive = function(obj, key, val){
    var dep = new Dep()
    // 給當前屬性的值添加監(jiān)聽
    var chlidOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=> {
            //判斷是否有watcher需要添加
            if(Dep.target){
                dep.addSub(Dep.target)
            }
            // -------
            return val
        },
        set: (newVal) => {
            if(val === newVal) return
            val = newVal
            // -------
            // 對新值進行監(jiān)聽
            chlidOb = observe(newVal)
            // 通知所有訂閱者更新數(shù)據(jù)
            dep.notify()
        }
    })
  }

將一些的枝枝葉葉去掉蹂安,剩下的Object.defineProperty就可以對一個數(shù)據(jù)監(jiān)聽了椭迎。就不重復貼代碼了。


觀察者(發(fā)布-訂閱者)模式

WHY 為什么要用這個模式田盈?
觀察者模式是開發(fā)基于行為的大型應用程序的有力手段畜号。在一次瀏覽器會話期間,應用程序中可能會斷斷續(xù)續(xù)地發(fā)生幾十次缠黍,幾百次甚至上千次各種事件弄兜。你可以消減為事件注冊監(jiān)聽器的次數(shù),讓可觀察者對象借助一個事件監(jiān)聽器替你處理各種行為并將信息委托給它的所有訂閱者瓷式,從而降低內(nèi)存消耗和提高互動性能替饿。這樣一來,就不用沒完沒了地為同樣的元素增添新的事件監(jiān)聽器贸典。這樣有利于減少系統(tǒng)開銷并提高程序的可維護性视卢。(JavaScript設計模式)
頻繁的數(shù)據(jù)操作與此模式非常的契合
觀察者模式實質(zhì)就是你可以對程序中某個對象的狀態(tài)進行觀察,并在其發(fā)生改變時能得到通知廊驼。
觀察者模式存在兩個角色:

  • 觀察者(發(fā)布者)
  • 被觀察者(訂閱者)

發(fā)布者:

  • [] 一個用來管理訂閱者的數(shù)組
  • addSub() 添加訂閱者
  • notify() 用于發(fā)布消息据过,通知訂閱者有新的訂閱信息
// ------------------------------------
  class Dep{
    constructor(){
        this.subs = []  //管理訂閱者隊列
    }
    addSub(sub){
        // 去重復
        var alreadyExists = this.subs.some( (el) => {
            return el === sub
        })
        if (!alreadyExists) {
            this.subs.push(sub)
        }
    }
    notify(){
        // 通知所有的訂閱者(Watcher),觸發(fā)訂閱者的相應邏輯處理
        this.subs.forEach((sub) => sub.update())
    }
  }

訂閱者:

  • value 自身的值妒挎,在這里是用來保存發(fā)布者發(fā)布過來的值
  • updata() 接收發(fā)布者更新通知
// ----------------------------------
  class Watcher{
    constructor(vm, expOrFn, cb){
        this.vm = vm // 整個實例
        this.cb = cb // 當數(shù)據(jù)更新時想要做的事情
        this.expOrFn = expOrFn // 被訂閱的數(shù)據(jù)
        this.val = this.get() // 獲取訂閱的值作為自身的值
    }
    // 更新數(shù)據(jù)
    update(){
        this.run()
    }
    run(){
        const val = this.get()
        
        if(val !== this.val){
            this.val = val;
            this.cb.call(this.vm)
        }
    }
    get(){
        // 當前訂閱者(Watcher)讀取發(fā)布者的值
        Dep.target = this 
        const exp = this.expOrFn //獲取鍵名绳锅,用來定位到是哪一個發(fā)布者
        var val = this.vm._data[exp] //獲取發(fā)布者的值
        Dep.target = null
        return val;
    }
    
  }

如何整合

在整理完這些核心點之后該,我們擁有了零件酝掩。接下來該如何組裝起來呢鳞芙?
既然用的是觀察者模式
誰是發(fā)布者?怎么添加訂閱期虾?
首先原朝,我們監(jiān)聽的每一個數(shù)據(jù)都應該是一個發(fā)布者,這樣就可以在數(shù)據(jù)發(fā)生改變的時候通知到各個訂閱者镶苞。那么喳坠,就可以在初始化該數(shù)據(jù)監(jiān)聽的時候(defineReactive),在函數(shù)里面var dep = new Dep()茂蚓。

defineReactive = function(obj, key, val){
    var dep = new Dep()
    ...
}

那訂閱者呢壕鹉,前面說過剃幌,在get/set的時候添加對應的邏輯,這就派上用場了御板。
get:
我們可以在調(diào)用get時锥忿,往當前的發(fā)布者dep中添加訂閱者。注意怠肋!是添加。
/* */
在這一處有一個比較繞的一個點淹朋,就是訂閱者的創(chuàng)建笙各。
訂閱者的創(chuàng)建應該伴隨的是在頁面的某個地方需要用到這個數(shù)據(jù),可以說是出現(xiàn)一個 {{}}础芍,這時就有一個watcher杈抢。
這時候回過頭來看watcher類,在new Watcher(this, expOrFn, cb)時仑性,為了初始化訂閱者的值惶楼,調(diào)用了get()this.val = this.get() // 獲取訂閱的值作為自身的值,而且在watcher的參數(shù)里面诊杆,可以知道需要獲取的是哪一個發(fā)布者的值

    const exp = this.expOrFn//獲取鍵名歼捐,定位到是哪一個發(fā)布者
    var val = this.vm._data[exp] //獲取發(fā)布者的值

自然而然的,這一步觸發(fā)了發(fā)布者的get()晨汹,然后我們再看Dep.target = this豹储,我們就可以將整個watcher添加進訂閱者的隊列了。
set:
set需要做的就簡單得多了淘这,當發(fā)布者的數(shù)據(jù)發(fā)生改變時剥扣,會調(diào)用set,在這將新值更新完之后铝穷,就該通知該發(fā)布者的所有訂閱者更新信息钠怯。


保留了添加訂閱者和更新發(fā)布者數(shù)據(jù)兩個功能。

class Vue{
    constructor(options = {}){
        // 簡化了$options的處理
        this.$options = options
        // 簡化了對data的處理
        let data = this._data = this.$options.data
        // 將所有data最外層屬性代理到Vue實例上
        Object.keys(data).forEach(key => this._proxy(key))
        // 監(jiān)聽數(shù)據(jù)
        console.log('listen data :')
        observe(data)
    }
    // 對外暴露調(diào)用訂閱者的接口曙聂,內(nèi)部主要在指令中使用訂閱者
    // ------------------{{}}---------------------
    $watch(expOrFn, cb){
        //
        new Watcher(this, expOrFn, cb)
    }
    // -------------------------------------------
    _proxy(key){
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key],
            set: (val) => {
                //更新數(shù)據(jù)
                this._data[key] = val
            } 
        })
    }
    
  }


  // test-----------
  var t = new Vue({
      data: {
          name: 'NAME'
      }
  })
  t.$watch('name', () => console.log('cb:the one'))
  t.$watch('name', () => console.log('cb:the second'))
  t.name = 'ghjk'

到這Vue雙向數(shù)據(jù)綁定的簡單邏輯基本也就完成了晦炊,還剩下的是同界面的解析交互了。

相信在現(xiàn)在的前端環(huán)境筹陵,雙向數(shù)據(jù)綁定幾乎是選擇框架的一種標準了刽锤。
粗略算起來,研究了小半個星期的Vue雙向數(shù)據(jù)綁定朦佩,略有所得并思,做個記錄,不足或有錯之處语稠,望指出宋彼。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弄砍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子输涕,更是在濱河造成了極大的恐慌音婶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莱坎,死亡現(xiàn)場離奇詭異衣式,居然都是意外死亡,警方通過查閱死者的電腦和手機檐什,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門碴卧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乃正,你說我怎么就攤上這事住册。” “怎么了瓮具?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵荧飞,是天一觀的道長。 經(jīng)常有香客問我名党,道長叹阔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任兑巾,我火速辦了婚禮条获,結果婚禮上,老公的妹妹穿的比我還像新娘蒋歌。我一直安慰自己帅掘,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布堂油。 她就那樣靜靜地躺著修档,像睡著了一般。 火紅的嫁衣襯著肌膚如雪府框。 梳的紋絲不亂的頭發(fā)上吱窝,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音迫靖,去河邊找鬼院峡。 笑死,一個胖子當著我的面吹牛系宜,可吹牛的內(nèi)容都是我干的照激。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼盹牧,長吁一口氣:“原來是場噩夢啊……” “哼俩垃!你這毒婦竟也來了励幼?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤口柳,失蹤者是張志新(化名)和其女友劉穎苹粟,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跃闹,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嵌削,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辣卒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掷贾。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖荣茫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情场靴,我是刑警寧澤啡莉,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站旨剥,受9級特大地震影響咧欣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轨帜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一魄咕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚌父,春花似錦哮兰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膏秫,卻和暖如春右遭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缤削。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工窘哈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亭敢。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓滚婉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吨拗。 傳聞我的和親對象是個殘疾皇子满哪,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

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