通俗易懂了解Vue雙向綁定原理及實現

 看到一篇文章,以爲寫得挺好的,拿過來給你們分享一下,恰好解答了一些困擾個人一些疑惑!!!html


1. 前言

每當被問到Vue數據雙向綁定原理的時候,你們可能都會脫口而出:Vue內部經過Object.defineProperty方法屬性攔截的方式,把data對象裏每一個數據的讀寫轉化成getter/setter,當數據變化時通知視圖更新。雖然一句話把大概原理歸納了,可是其內部的實現方式仍是值得深究的,本文就以通俗易懂的方式剖析Vue內部雙向綁定原理的實現過程。vue

2. 思路分析

所謂MVVM數據雙向綁定,即主要是:數據變化更新視圖,視圖變化更新數據。以下圖:
node

也就是說:數組

  • 輸入框內容變化時,data 中的數據同步變化。即 view => model 的變化。
  • data 中的數據變化時,文本節點的內容同步變化。即 model => view 的變化。

要實現這兩個過程,關鍵點在於數據變化如何更新視圖,由於視圖變化更新數據咱們能夠經過事件監聽的方式來實現。因此咱們着重討論數據變化如何更新視圖。緩存

數據變化更新視圖的關鍵點則在於咱們如何知道數據發生了變化,只要知道數據在何時變了,那麼問題就變得迎刃而解,咱們只需在數據變化的時候去通知視圖更新便可。函數

3. 使數據對象變得「可觀測」

數據的每次讀和寫可以被咱們看的見,即咱們可以知道數據何時被讀取了或數據何時被改寫了,咱們將其稱爲數據變的‘可觀測’。post

要將數據變的‘可觀測’,咱們就要藉助前言中提到的Object.defineProperty方法了,關於該方法,MDN上是這麼介紹的:性能

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。學習

在本文中,咱們就使用這個方法使數據變得「可觀測」。測試

首先,咱們定義一個數據對象car

1 let car = {
2         'brand':'BMW',
3         'price':3000
4     }

 

咱們定義了這個car的品牌brandBMW,價格price是3000。如今咱們能夠經過car.brandcar.price直接讀寫這個car對應的屬性值。可是,當這個car的屬性被讀取或修改時,咱們並不知情。那麼應該如何作纔可以讓car主動告訴咱們,它的屬性被修改了呢?

接下來,咱們使用Object.defineProperty()改寫上面的例子:

  let car = {}
    let val = 3000
    Object.defineProperty(car, 'price', {
        get(){
            console.log('price屬性被讀取了')
            return val
        },
        set(newVal){
            console.log('price屬性被修改了')
            val = newVal
        }
    })

 

經過Object.defineProperty()方法給car定義了一個price屬性,並把這個屬性的讀和寫分別使用get()set()進行攔截,每當該屬性進行讀或寫操做的時候就會出發get()set()。以下圖:

能夠看到,car已經能夠主動告訴咱們它的屬性的讀寫狀況了,這也意味着,這個car的數據對象已是「可觀測」的了。

爲了把car的全部屬性都變得可觀測,咱們能夠編寫以下兩個函數:

 1 /**
 2      * 把一個對象的每一項都轉化成可觀測對象
 3      * @param { Object } obj 對象
 4      */
 5     function observable (obj) {
 6         if (!obj || typeof obj !== 'object') {
 7             return;
 8         }
 9         let keys = Object.keys(obj);
10         keys.forEach((key) =>{
11             defineReactive(obj,key,obj[key])
12         })
13         return obj;
14     }
15     /**
16      * 使一個對象轉化成可觀測對象
17      * @param { Object } obj 對象
18      * @param { String } key 對象的key
19      * @param { Any } val 對象的某個key的值
20      */
21     function defineReactive (obj,key,val) {
22         Object.defineProperty(obj, key, {
23             get(){
24                 console.log(`${key}屬性被讀取了`);
25                 return val;
26             },
27             set(newVal){
28                 console.log(`${key}屬性被修改了`);
29                 val = newVal;
30             }
31         })
32     }

 

如今,咱們就能夠這樣定義car:

1 let car = observable({
2         'brand':'BMW',
3         'price':3000
4     })

 

car的兩個屬性都變得可觀測了。

4. 依賴收集

完成了數據的'可觀測',即咱們知道了數據在何時被讀或寫了,那麼,咱們就能夠在數據被讀或寫的時候通知那些依賴該數據的視圖更新了,爲了方便,咱們須要先將全部依賴收集起來,一旦數據發生變化,就統一通知更新。其實,這就是典型的「發佈訂閱者」模式,數據變化爲「發佈者」,依賴對象爲「訂閱者」。

如今,咱們須要建立一個依賴收集容器,也就是消息訂閱器Dep,用來容納全部的「訂閱者」。訂閱器Dep主要負責收集訂閱者,而後當數據變化的時候後執行對應訂閱者的更新函數。

建立消息訂閱器Dep:

 1 class Dep {
 2         constructor(){
 3             this.subs = []
 4         },
 5         //增長訂閱者
 6         addSub(sub){
 7             this.subs.push(sub);
 8         },
 9         //判斷是否增長訂閱者
10         depend () {
11             if (Dep.target) {
12                 this.addSub(Dep.target)
13             }
14         },
15 
16         //通知訂閱者更新
17         notify(){
18             this.subs.forEach((sub) =>{
19                 sub.update()
20             })
21         }
22     }
23 Dep.target = null;

 

有了訂閱器,再將defineReactive函數進行改造一下,向其植入訂閱器:

 1 function defineReactive (obj,key,val) {
 2         let dep = new Dep();
 3         Object.defineProperty(obj, key, {
 4             get(){
 5                 dep.depend();
 6                 console.log(`${key}屬性被讀取了`);
 7                 return val;
 8             },
 9             set(newVal){
10                 val = newVal;
11                 console.log(`${key}屬性被修改了`);
12                 dep.notify()                    //數據變化通知全部訂閱者
13             }
14         })
15     }

 

從代碼上看,咱們設計了一個訂閱器Dep類,該類裏面定義了一些屬性和方法,這裏須要特別注意的是它有一個靜態屬性 target,這是一個全局惟一 的Watcher,這是一個很是巧妙的設計,由於在同一時間只能有一個全局的 Watcher 被計算,另外它的自身屬性 subs 也是 Watcher 的數組。

咱們將訂閱器Dep添加訂閱者的操做設計在getter裏面,這是爲了讓Watcher初始化時進行觸發,所以須要判斷是否要添加訂閱者。在setter函數裏面,若是數據變化,就會去通知全部訂閱者,訂閱者們就會去執行對應的更新的函數。

到此,訂閱器Dep設計完畢,接下來,咱們設計訂閱者Watcher.

5. 訂閱者Watcher

訂閱者Watcher在初始化的時候須要將本身添加進訂閱器Dep中,那該如何添加呢?咱們已經知道監聽器Observer是在get函數執行了添加訂閱者Wather的操做的,因此咱們只要在訂閱者Watcher初始化的時候出發對應的get函數去執行添加訂閱者操做便可,那要如何觸發get的函數,再簡單不過了,只要獲取對應的屬性值就能夠觸發了,核心緣由就是由於咱們使用了Object.defineProperty( )進行數據監聽。這裏還有一個細節點須要處理,咱們只要在訂閱者Watcher初始化的時候才須要添加訂閱者,因此須要作一個判斷操做,所以能夠在訂閱器上作一下手腳:在Dep.target上緩存下訂閱者,添加成功後再將其去掉就能夠了。訂閱者Watcher的實現以下:

 1   class Watcher {
 2         constructor(vm,exp,cb){
 3             this.vm = vm;
 4             this.exp = exp;
 5             this.cb = cb;
 6             this.value = this.get();  // 將本身添加到訂閱器的操做
 7         },
 8 
 9         update(){
10             let value = this.vm.data[this.exp];
11             let oldVal = this.value;
12             if (value !== oldVal) {
13                 this.value = value;
14                 this.cb.call(this.vm, value, oldVal);
15             },
16         get(){
17             Dep.target = this;  // 緩存本身
18             let value = this.vm.data[this.exp]  // 強制執行監聽器裏的get函數
19             Dep.target = null;  // 釋放本身
20             return value;
21         }
22     }

 

過程分析:

訂閱者Watcher 是一個 類,在它的構造函數中,定義了一些屬性:

  • vm:一個Vue的實例對象;
  • exp:node節點的v-modelv-on:click等指令的屬性值。如v-model="name"exp就是name;
  • cb:Watcher綁定的更新函數;

當咱們去實例化一個渲染 watcher 的時候,首先進入 watcher 的構造函數邏輯,就會執行它的 this.get() 方法,進入 get 函數,首先會執行:

Dep.target = this;  // 緩存本身

 

實際上就是把 Dep.target 賦值爲當前的渲染 watcher ,接着又執行了:

let value = this.vm.data[this.exp]  // 強制執行監聽器裏的get函數

 

在這個過程當中會對 vm 上的數據訪問,其實就是爲了觸發數據對象的getter

每一個對象值的 getter都持有一個 dep,在觸發 getter 的時候會調用 dep.depend() 方法,也就會執行this.addSub(Dep.target),即把當前的 watcher 訂閱到這個數據持有的 depsubs 中,這個目的是爲後續數據變化時候能通知到哪些 subs 作準備。

這樣實際上已經完成了一個依賴收集的過程。那麼到這裏就結束了嗎?其實並無,完成依賴收集後,還須要把 Dep.target 恢復成上一個狀態,即:

Dep.target = null;  // 釋放本身

 

由於當前vm的數據依賴收集已經完成,那麼對應的渲染Dep.target 也須要改變。

update()函數是用來當數據發生變化時調用Watcher自身的更新函數進行更新的操做。先經過let value = this.vm.data[this.exp];獲取到最新的數據,而後將其與以前get()得到的舊數據進行比較,若是不同,則調用更新函數cb進行更新。

至此,簡單的訂閱者Watcher設計完畢。

6. 測試

完成以上工做後,咱們就能夠來真正的測試了。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改變data內容" onclick="changeInput()">
    
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //將數據變的可觀測
        el.innerHTML = this.data[exp];           // 初始化模板數據的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }

    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    
    //改變輸入框內容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改變data內容
    function changeInput(){
        myVue.data.name = "難涼熱血"
    
    }
</script>
</body>
</html>
observer.js

    /**
     * 把一個對象的每一項都轉化成可觀測對象
     * @param { Object } obj 對象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一個對象轉化成可觀測對象
     * @param { Object } obj 對象
     * @param { String } key 對象的key
     * @param { Any } val 對象的某個key的值
     */
    function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}屬性被修改了`);
                dep.notify()                    //數據變化通知全部訂閱者
            }
        })
    }
    class Dep {
        
        constructor(){
            this.subs = []
        }
        //增長訂閱者
        addSub(sub){
            this.subs.push(sub);
        }
        //判斷是否增長訂閱者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }

        //通知訂閱者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
        
    }
    Dep.target = null;
watcher.js

    class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 將本身添加到訂閱器的操做
        }
        get(){
            Dep.target = this;  // 緩存本身
            let value = this.vm.data[this.exp]  // 強制執行監聽器裏的get函數
            Dep.target = null;  // 釋放本身
            return value;
        }
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
    }
}

 

效果:

vue數據雙向綁定原理及實現

7. 總結

總結一下:

實現數據的雙向綁定,首先要對數據進行劫持監聽,因此咱們須要設置一個監聽器Observer,用來監聽全部屬性。若是屬性發上變化了,就須要告訴訂閱者Watcher看是否須要更新。由於訂閱者是有不少個,因此咱們須要有一個消息訂閱器Dep來專門收集這些訂閱者,而後在監聽器Observer和訂閱者Watcher之間進行統一管理的。

(完)




免責聲明

  • 本博客全部文章僅用於學習、研究和交流目的,歡迎非商業性質轉載。
  • 博主在此發文(包括但不限於漢字、拼音、拉丁字母)均爲隨意敲擊鍵盤所出,用於檢驗本人電腦鍵盤錄入、屏幕顯示的機械、光電性能,並不表明本人局部或所有贊成、支持或者反對觀點。如須要詳查請直接與鍵盤生產廠商法人表明聯繫。挖井挑水無水錶,不會網購無快遞。
  • 博主的文章沒有高度、深度和廣度,只是湊字數。因爲博主的水平不高(實際上是個菜B),不足和錯誤之處在所不免,但願你們可以批評指出。
  • 博主是利用讀書、參考、引用、抄襲、複製和粘貼等多種方式打形成本身的文章,請原諒博主成爲一個無恥的文檔搬運工!

 

參考資料:http://www.javashuo.com/article/p-vbvongpy-ea.html

相關文章
相關標籤/搜索