1. 程式人生 > >一起學習vue原始碼 - Object的變化偵測

一起學習vue原始碼 - Object的變化偵測

 

作者:小土豆biubiubiu

部落格園:www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

簡書:https://www.jianshu.com/u/cb1c3884e6d5

微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)

碼字不易,點贊鼓勵喲~

一.前言

  一起學習vue原始碼的第一篇,本來想起名為雙向資料繫結原理,但是想來還是引用書中[深入淺出vue.js]比較專業的描述作為題目。

  (主要是因為雙向資料繫結中Object和Array的實現原理是不一樣的,所以還是拆分的細一些比較好)

  總歸來說,雙向資料繫結就是通過變化偵測這種方式去實現的,這篇文章主要總結的是Object的變化偵測。

 

  我們在面試的時候,如果面試者的技術棧包含vue框架,那麼面試官會有很大的機率甩出“你瞭解vue中雙向資料繫結的原理嗎”這個問題。

  我也聽過一些回答,大家一般都能說出一個詞叫“釋出-訂閱”。

  那在深入去問的時候,或者說你能不能給我實現一個簡單的雙向資料繫結,基本就回答不上來了。

  

  說到這裡我已經丟擲三個名詞了:雙向資料繫結、變化偵測、釋出-訂閱。

  前面說過雙向資料繫結就是通過變化偵測這種方式去實現的。

  那這裡的釋出-訂閱我理解是軟體的設計思想,它比變化偵測更深一層,已經到了程式碼的設計模式這一層了。

  所以我們可以說雙向資料繫結就是通過變化偵測這種方式去實現的,也可以說雙向資料繫結是通過釋出-訂閱這種模式去實現的。

  我個人覺得兩者說法都沒有問題,只是描述方式不一樣。

 

  那不管是叫變化偵測還是釋出-訂閱,有一些實際生活中的例子可以便於我們理解它們。

  (後面的很多描述都會混用這兩個名詞,不用糾結叫法,瞭解說的是同一個東西即可)

  比如我們經常玩的微博:

    有一個使用者kk很喜歡某個博主MM,然後就在微博上關注了博主MM。

    之後每一次博主MM在微博上發表一些吃吃喝喝的動態,微部落格戶端都會主動將動態推送給使用者kk。

    在過了一段時間,博主MM爆出一個不好的新聞,使用者kk便將博主MM的微博取關了。

 

  在這個實際場景中,我們可以稱博主MM是一個釋出者。

  使用者kk是一個訂閱者。

  微部落格戶端就是一個管理者的角色,它時刻偵測這博主MM的動態,在博主MM更新動態是主動將動態推送給訂閱者。

  

  前面說了這麼多想來大家應該能理解發布訂閱/變化偵測大致的設計思想和需要關注的幾個點了:

    1.如何偵測資料的變化(或者說如何偵測到釋出者的釋出的內容)

    2.如何收集儲存訂閱者。

    3.訂閱者如何實現。

  

  接著我們就我們總結的點逐個擊破。

 

二.如何偵測資料的變化

  看過javascript高階程式設計的應該都知道Object類提供了一個方法defineProperty,在該方法中定義get和set就可以實現資料的偵測。

  備註:對Object.defineProperty不瞭解的可以移步這裡。

  

  下面就Object的defineProperty方法做一個示例演示。

var obj = {};

var name;
Object.defineProperty(obj, 'name', {
      enumerable : true,
      configurable : true,
      get: function(){
          console.log("get方法被呼叫");
          return name;
      },
      set: function(newName){
          console.log("set方法被呼叫");
          name = newName;
      }
})


// 修改name屬性時會觸發set方法
obj.name = 'newTodou';

// 訪問name屬性時會觸發get方法
var objName = obj.name;

  

  我們將這段程式碼引入一個html中,執行後控制檯的列印結果如下:

  

  

  可以看到,當我們修改obj.name屬性值時,呼叫了name屬性的set方法,列印了"set方法被呼叫";

  當我們訪問obj.name屬性值時,呼叫name屬性的get方法,列印了"get方法被呼叫"。

  那麼這就是我們說的“如何偵測資料的變化”這個問題的答案,是不是很簡單呢。

提示:

  訪問資料屬性值時會觸發定義在屬性上的get方法;修改資料屬性值時觸發定義在屬性上的set方法。

  這句話很關鍵,希望可以牢記,後面很多內容都跟這個相關。

 

  實際上到這裡我們已經可以實現一個簡單的雙向資料繫結:input輸入框內容改變,實現輸入框下方span文字內容改變。

 

  我們先梳理一下這整個的實現思路:監聽輸入框的內容,將輸入框的內容同步到span的innerText屬性。

  監聽輸入框內容的變化可以通過keyup事件,在事件內部獲取到input框中的內容,即獲取到變化的資料,我們把這個資料儲存到一個obj物件的name屬性中。

  將輸入框的內容同步到span的innerText屬性這個操作相當於將變化的資料同步更新到檢視中,更新的邏輯很簡單:

    spanEle.innerText = obj.name;

  我們需要考慮的是在哪裡觸發這個更新操作。

  在監聽輸入框內容變化的邏輯中我們說過會將變化的資料儲存到obj.name中。

  那這個操作實際上就是為物件的屬性賦值,會觸發定義在屬性上的set方法。

  那麼將輸入框的內容同步到span的innerText屬性這個操作,很自然的就落到了name屬性的set方法中。

  

  到這裡,相信大家已經很輕鬆能寫出程式碼了。

<input type="text" id="name"/>
<br/>
<span id="text"></span>
<script type="text/javascript">
    var nameEle = document.getElementById("name");
    var textEle = document.getElementById('text');

    var obj = {};
    Object.defineProperty(obj, 'name', {
        enumerable: true,
        configurable: true,
        get: function(){
            return textEle.value;
        },
        set: function(newName){
            textEle.innerText = newName;

        }
    })
    nameEle.onkeyup = function () {
        obj.name = event.target.value;
    }
</script>

   

  接著還沒完,我們知道一個物件裡面一般都會有多個屬性,vue data中一般也會存在多個或者多層的屬性和資料,比如:

  data: {

    id: 12091,

    context: {

      index:1,

      result: 0

    }

  }

  所以我們得讓物件中的所有屬性都變得可偵測:遞迴遍歷物件的所有屬性,為每個屬性都定義get和set方法。

  vue原始碼封裝了一個Observer類來實現這個功能。

/*
*   obj資料實際上就是vue中的data資料
*/
function Observer(obj){
    this.obj = obj;
    this.walk(obj);
   
}
Observer.prototype.walk = function(obj) {
    // 獲取obj物件中所有的屬性
    var keysArr = Object.keys(obj);
    keysArr.forEach(element =>{
        defineReactive(obj, element, obj[element]);
    })
}
// 參照原始碼,將該方法為獨立一個方法
function defineReactive(obj, key, val) {
    // 如果obj是包含多層資料屬性的物件,就需要遞迴每一個子屬性
    if(typeof val === 'object'){
        new Observer(val);
    }

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

 

  到這裡,資料觀測這一步就完成了。

 

三.如何收集儲存訂閱者

  收集儲存訂閱者說的簡單點就是一個數據儲存的問題,所以也不用太糾結,就將訂閱者保持到陣列中。

  前面我們說過微博的那個例子:

    使用者kk關注博主MM,對應的就是往陣列中新增一個訂閱者/元素。

    使用者kk取關博主MM,可以理解為從陣列中移除一個訂閱者/元素。

    博主MM釋出動態,微部落格戶端主動動態給使用者kk,這可以理解為通知資料更新操作。

  

  那上面描述的一整個內容就是收集儲存訂閱者需要關注的東西,書中[深入淺出vue.js]把它叫做如何收集依賴。

  那麼現在就我們說的內容,實現一個類Dep,後面把它稱為訂閱器,用於管理訂閱者/管理依賴。

function Dep(){
    this.subs = [];
}

Dep.prototype.addSub = function(sub){
    this.subs.push(sub);
}
// 新增依賴
Dep.prototype.depend = function() {
    // 這裡可以先不用關注depObject是什麼
    // 就先暫時理解它是一個訂閱者/依賴物件
    this.addSub(depObject);
}

 // 移除依賴
 Dep.prototype.removeSub = function(sub) {
    // 原始碼中是通過抽出來一個remove方法來實現移除的
    if(this.subs.length > 0){
        var index = this.subs.indexOf(sub);
        if(index > -1){
            // 注意splice的用法
            this.subs.splice(index, 1);
        }
    }
}

// 通知資料更新
Dep.prototype.notify = function() {
    for(var i = 0; i < this.subs.length; i++ ){
        // 這裡相當於依次呼叫subs中每個元素的update方法
        // update方法內部實現可以先不用關注,瞭解其目的就是為了更新資料
        this.subs[i].update()
    }
}

 

  依賴收集和管理實現了之後,我們需要考慮兩個問題:什麼時候新增依賴?什麼時候通知更新資料?

 

  在微博的例子中,使用者kk關注博主MM,對應的就是往陣列中新增一個訂閱者/元素。

  那對應到程式碼中,可以視作訪問了物件的屬性,那我們就可以在訪問物件屬性的時候新增依賴。

 

  博主MM釋出動態,微部落格戶端主動動態給使用者kk,這可以理解為通知資料更新操作。

  在對應到程式碼中,可以視作修改了物件的屬性,那我們就可以在修改物件屬性的時候通知資料更新。

  

  這段話可能不是很好理解,所以我們可以去聯想平時我們在vue中的操作:使用雙花括號{{text}}在模板的div標籤內插入資料。

  這個操作實際上就相當於是模板中的div便籤讀取並且依賴了vue中的data.text資料,那我們就可以將這個div作為一個依賴物件收集起來。

  之後當text資料發生變化後,我們就需要通知這個div標籤更新它內部的資料。

 

  說了這麼多,我們剛剛的提的什麼時候新增依賴,什麼時候通知更新資料這個問題就已經有答案了:

    在get中新增依賴,在set中通知資料更新。

   

  關於新增依賴和通知資料更新這兩個操作均是Dep這個類的功能,介面分別為:Dep.depend和Dep.notify。

  那現在我們就將Observer這個類進行完善:get中新增依賴,在set中通知資料更新。

/*
*   obj資料實際上就是vue中的data資料
*/
function Observer(obj){
    this.obj = obj;
    if(Array.isArray(this.obj)){
        //如果是陣列,則會呼叫陣列的偵測方法
    }else{
        this.walk(obj);
    }
}
Observer.prototype.walk = function(obj) {
    // 獲取obj物件中所有的屬性
    var keysArr = Object.keys(obj);
    keysArr.forEach(element =>{
        defineReactive(obj, element, obj[element]);
    })
}
// 參照原始碼,將該方法為獨立一個方法
function defineReactive(obj, key, val) {
    // 如果obj是包含多層資料屬性的物件,就需要遞迴每一個子屬性
    if(typeof val === 'object'){
        new Observer(val);
    }
    var dep = new Dep();    
    Object.defineProperty(obj, key,{
        enumerable: true,
        configurable: true,
        get: function(){
            // 在get中新增依賴
            dep.depend();
            return val;
        },
        set: function(newVal) {
            val = newVal;
            // 在set中通知資料更新
            dep.notify();

        }
    })        
}

 

四.如何實現訂閱者

  還是前面微博的例子,其中使用者KK被視為一個訂閱者,vue原始碼中將定義為Watcher。

  那訂閱者需要做什麼事情呢?

 

  先回顧一下我們實現的訂閱器Dep。

  第一個功能就是新增訂閱者。

depend() {
        // 這裡可以先不用關注depObject是什麼
        // 就先暫時理解它是一個訂閱者/依賴物件
        this.addSub(depObject);
}

  可以看到這段程式碼中當時的註釋是“可以先不用關注depObject是什麼,暫時理解它是一個訂閱者/依賴物件”。

  那現在我們就知道depObject實際上就是一個Watcher例項。

 

  那如何觸發depend方法新增訂閱者呢?

  在前面編寫偵測資料變化程式碼時,觸發depend方法新增依賴的邏輯在屬性的get方法中。

  

  那vue原始碼的設計是在Watcher初始化的時候觸發資料屬性的get方法,即可以將訂閱者新增到訂閱器中。

  

  下面將程式碼貼出來。

/*
*   vm: vue例項物件
*   exp: 屬性名
*/
function Watcher(vm, exp){
    this.vm = vm;
    this.exp = exp;

    // 初始化的時候觸發資料屬性的get方法,即可以將訂閱者新增到訂閱器中
    this.value = this.get();
}

// 觸發資料屬性的get方法: 訪問資料屬性即可實現
Watcher.prototype.get = function() {
    // 訪問資料屬性邏輯
    var value =  this.vm.data[this.exp];
    return value;
}

  

  這裡對get方法的邏輯簡單的解讀一下:

    資料屬性的訪問肯定是需要傳遞資料和對應的屬性名才能實現。

    然後我們想一下vue中的data屬性是可以使用vue的例項物件加"."操作符進行訪問的。

    所以vue在這裡設計的時候沒有直接將資料傳入,而是傳遞一個vue例項,使用vue例項.data['屬性名']對屬性進行訪問,從而去觸發屬性的get方法。

  注意:vue還將訪問到的資料屬性值儲存到了Watcher中value變數中。

 

  到這裡,由訂閱器Dep的depend方法順藤摸瓜出來的Watcher的第一個功能就完成了,即:

    Watcher初始化的時候觸發資料屬性的get方法,將訂閱者新增到訂閱器中。

 

  我們在接著摸瓜,看一下訂閱器Dep的第二個功能:通知資料更新。

// 通知資料更新
notify() {
        for(let i = 0; i < this.subs.length; i++ ){
            // 這裡相當於依次呼叫subs中每個元素的update方法
            // update方法內部實現可以先不用關注,瞭解其目的就是為了更新資料
            this.subs[i].update()
        }
}

  這段程式碼最重要的一行:this.subs[i].update(),這行程式碼實際上觸發的是訂閱者Watcher例項的update方法。

  (因為subs中的每一個元素就是一個訂閱者例項)

 

  所以我們的Watcher的第二個功能就是需要實現一個真正包含更新資料邏輯的update函式。

 

  那什麼叫真正更新資料的邏輯呢?

  還是vue的雙花括號示例:使用雙花括號{{text}}在模板的div標籤內插入資料。

  當text資料發生變化後,真正更新資料的邏輯就是: div.innerText = newText;

  那Watcher中的update方法我們應該大致瞭解了。

 

  在說回vue的設計,它將真正更新資料的邏輯封裝成一個函式,Watcher例項初始化的時候傳遞給Watcher的建構函式,然後在update方法中進行呼叫。

   

 

 

 

function Watcher(vm, exp, cb){
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    // 初始化的時候觸發資料屬性的get方法,即可以將訂閱者新增到訂閱器中
    this.value = this.get();
}

// 觸發資料屬性的get方法: 訪問資料屬性即可實現
Watcher.prototype.get = function() {
    // 訪問資料屬性邏輯
    var value =  this.vm.data[this.exp];
    return value;
}
Watcher.prototype.update = function() {
    // 當update被觸發時,此時獲取到的資料屬性值是已經被修改過後的新值
    var newValue = this.vm.data[this.exp];

    // 觸發傳遞給Watcher的更新資料的函式
    this.cb.call(this.vm, newValue);
    
}

 

  那簡單的update程式碼就實現了,不過vue在這裡有做小小的優化。

  我們在get方法中訪問了資料的屬性,並將資料為修改前的初值儲存到了this.value中。

  所以update方法的優化就是在執行update後續程式碼之前,先對this.value和newValue做一個比較,即對舊值和新值作比較。

  只有在新值和舊值不相等的情況下,才會觸發cb函式。

Watcher.prototype.update = function() {
    // 當update被觸發時,此時獲取到的資料屬性值是已經被修改過後的新值
    var newValue = this.vm.data[this.exp];
    var oldValue = this.value;

    if(oldValue !== newValue){
        // 觸發傳遞給Watcher的更新資料的函式
        this.cb.call(this.vm, newValue);
    }
}

 

五.程式碼補充

  Watcher中觸發資料屬性get方法的執行已經補充完畢,我們在看看訂閱器Dep的depend方法。

depend() {
        // 這裡可以先不用關注depObject是什麼
        // 就先暫時理解它是一個訂閱者/依賴物件
        this.addSub(depObject);
}

 

  關於這個depObject我們說過它是一個訂閱者,即Watcher的一個例項,那怎麼獲取Watcher這個例項呢?

  我們回頭再看看這個depend觸發的流程:

    

   

  即建立Watcher例項,呼叫Watcher例項的get方法,從而觸發資料屬性上定義的get方法,最終觸發 dep.depend方法。

 

  所以按照這個流程,在觸發資料屬性上定義的get方法之前,就必須將Watcher例項準備好。

  我們知道在初始化Watcher時,Watcher內部的this的指向就是Watcher例項。

  所以vue設計的時候,在Watcher的get方法中把Watcher例項儲存到了Dep的target屬性上。

  這樣Watcher例項化完成後,全域性訪問Dep.target就能獲取到Watcher例項。

  所以現在將Watcher類的get方法進行補充

// 觸發資料屬性的get方法: 訪問資料屬性即可實現
Watcher.prototype.get = function() {
    // 把Watcher例項儲存到了Dep的target屬性上
    Dep.target = this;
    // 訪問資料屬性邏輯
    var value =  this.vm.data[this.exp];
    // 將例項清空釋放
    Dep.target = null;
    return value;
}

 

  備註:對於get方法中清空釋放Dep.target的程式碼,是有一定原因的。請先繼續往下看,把Dep.depend的補全程式碼看完。

 

  接著我們需要將Dep中的depend方法進行補全。

// 新增依賴
Dep.prototype.depend = function() {
    // addSub新增的是一個訂閱者/依賴物件
    // Watcher例項就是訂閱者,在Watcher例項初始化的時候,已經將自己儲存到了Dep.target中
    if(Dep.target){
        this.addSub(Dep.target);
    } 
}

 

  現在我在說一下清空釋放Dep.target的程式碼。

  假如我們沒有Dep.target = null這行程式碼,depend方法中也沒有if(Dep.target)的判斷。

  那第一個訂閱者新增完成後是正常的,當資料發生變化後,程式碼執行邏輯:

    觸發資料屬性上定義的set方法,

    執行dep.notify

    執行Watcher例項的update方法

    ....

  後面的就不說了,我們看一下這個過程中執行Watcher例項的update方法這一步。

Watcher.prototype.update = function() {
    // 當update被觸發時,此時獲取到的資料屬性值是已經被修改過後的新值
    var newValue = this.vm.data[this.exp];
    var oldValue = this.value;

    if(oldValue !== newValue){
        // 觸發傳遞給Watcher的更新資料的函式
        this.cb.call(this.vm, newValue);
    }
}

 

  可以看到,update方法中因為在執行真正更新資料的函式cb之前需要獲取到新值。

  所以再次訪問了資料屬性,那可想而知,訪問資料屬性就會呼叫屬性的get方法。

  又因為dep.depend的執行沒有任何條件判斷,導致當前Watcher被植入訂閱器兩次。

  這顯然是不正常的。因此,Dep.target = null 和 if(Dep.target)的判斷是非常必須的步驟。

 

六.完整程式碼

  現在我們將Observer、Dep、Watcher的完整程式碼貼出來。

  Observer實現

/*
*   obj資料實際上就是vue中的data資料
*/
function Observer(obj){
    this.obj = obj;
    if(Array.isArray(this.obj)){
        //如果是陣列,則會呼叫陣列的偵測方法
    }else{
        this.walk(obj);
    }
}
Observer.prototype.walk = function(obj) {
    // 獲取obj物件中所有的屬性
    var keysArr = Object.keys(obj);
    keysArr.forEach(element =>{
        defineReactive(obj, element, obj[element]);
    })
}
// 參照原始碼,將該方法為獨立一個方法
function defineReactive(obj, key, val) {
    // 如果obj是包含多層資料屬性的物件,就需要遞迴每一個子屬性
    if(typeof val === 'object'){
        new Observer(val);
    }
    var dep = new Dep();    
    Object.defineProperty(obj, key,{
        enumerable: true,
        configurable: true,
        get: function(){
            // 在get中新增依賴
            dep.depend();
            return val;
        },
        set: function(newVal) {
            val = newVal;
            // 在set中通知資料更新
            dep.notify();

        }
    })        
}

 

  Dep實現

function Dep(){
    this.subs = [];
}

Dep.prototype.addSub = function(sub){
    this.subs.push(sub);
}
// 新增依賴
Dep.prototype.depend = function() {
    // addSub新增的是一個訂閱者/依賴物件
    // Watcher例項就是訂閱者,在Watcher例項初始化的時候,已經將自己儲存到了Dep.target中
    if(Dep.target){
        this.addSub(Dep.target);
    } 
}

 // 移除依賴
 Dep.prototype.removeSub = function(sub) {
    // 原始碼中是通過抽出來一個remove方法來實現移除的
    if(this.subs.length > 0){
        var index = this.subs.indexOf(sub);
        if(index > -1){
            // 注意splice的用法
            this.subs.splice(index, 1);
        }
    }
}

// 通知資料更新
Dep.prototype.notify = function() {
    for(var i = 0; i < this.subs.length; i++ ){
        // 這裡相當於依次呼叫subs中每個元素的update方法
        // update方法內部實現可以先不用關注,瞭解其目的就是為了更新資料
        this.subs[i].update()
    }
}

 

  Watcher實現

function Watcher(vm, exp, cb){
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    // 初始化的時候觸發資料屬性的get方法,即可以將訂閱者新增到訂閱器中
    this.value = this.get();
}

// 觸發資料屬性的get方法: 訪問資料屬性即可實現
Watcher.prototype.get = function() {
    // 把Watcher例項儲存到了Dep的target屬性上
    Dep.target = this;
    // 訪問資料屬性邏輯
    var value =  this.vm.data[this.exp];
    // 將例項清空釋放
    Dep.target = null;
    return value;
}
Watcher.prototype.update = function() {
    // 當update被觸發時,此時獲取到的資料屬性值是已經被修改過後的新值
    var newValue = this.vm.data[this.exp];
    var oldValue = this.value;

    if(oldValue !== newValue){
        // 觸發傳遞給Watcher的更新資料的函式
        this.cb.call(this.vm, newValue);
    }
}

七.實踐

  關鍵核心的程式碼已經實現完成了,接下來就是使用了。

  因為這個過程中沒有模板編譯的實現,因此有些程式碼需要寫死。

  回想vue中雙向資料繫結的用法。

  我們先寫一段簡單的程式碼。

<html>
    <head>
        <meta charset="utf-8" />
        <title>一起學習Vue原始碼-Object的變化偵測</title>
    </head>
    <body>
        <h1>一起學習Vue原始碼-Object的變化偵測</h1>
        <div id="box">
            {{text}}
        </div>
    </body>
    <script type="text/javascript" src="./Dep.js"></script> 
    <script type="text/javascript" src="./Observer.js"></script>    
    <script type="text/javascript" src="./Watcher.js"></script>

    <script type='text/javascript'>
        /*
        *   data: 資料
        *   el: 元素
        *   exp:物件的屬性
        *   (傳遞這個exp固定引數也是因為沒有模板編譯相關的程式碼,所以就暫時寫死一個屬性)
        */
        function Vue(data, el, exp){
            this.data = data;
            this.el = el;
            // 因為沒有模板相關的程式碼,所以{{text}}的值使用這種方式進行解析
            this.innerHTML = this.data[exp];
        }

        var data = {
            text: 'hello Vue'
        };
        var el = document.getElementById('box');
      
        var vm = new Vue(data, el);      
    </script>
</html>    

 

  這段程式碼執行後,瀏覽器中已經可以顯示{{text}}的值了。

  備註:正常顯示並不是因為我們對模板和花括號進行編譯,而是使用el.innerHTML = data.text;這種寫死的方式實現的。

  

  接著,第一步就是將資料變得可觀測,即呼叫Observer傳入data資料,我們將程式碼寫到Vue建構函式中。

 /*
 *   data: 資料
 *   el: 元素
 *   exp:物件的屬性
 */
function Vue(data, el, exp){
     this.data = data;
     this.el = el;
     this.exp = exp;

     // 因為沒有模板相關的程式碼,所以{{text}}的值使用這種方式進行解析
     this.el.innerHTML = this.data[exp];

     //初始化vue例項需要將data資料變得可觀測
     new Observer(data);
}

 

  接著,手動為data的text屬性建立一個訂閱者,程式碼依然寫在vue建構函式中。

  備註:手動建立訂閱者也是因為沒有模板編譯程式碼,否則建立訂閱者正常的邏輯是遍歷模板動態建立訂閱者。

/*
 *   data: 資料
 *   el: 元素
 *   exp:物件的屬性
 */
function Vue(data, el, exp){
     this.data = data;
     this.el = el;
     this.exp = exp;

     // 因為沒有模板相關的程式碼,所以{{text}}的值使用這種方式進行解析
     this.el.innerHTML = this.data[exp];

     //初始化vue例項需要將data資料變得可觀測
     new Observer(data);

     this.cb = function(newVal){
          this.el.innerHTML = newVal;
     }
     // 建立一個訂閱者
     new Watcher(this, exp, this.cb);
}

  建立訂閱者的時候有一個cb引數,cb就是我們前面一直說的那個真正包含更新資料邏輯的函式。

  

  這些操作完成後,最後一步就是修改data.text的資料,如果修改完成後,div的內容發生變化,就證明我們這份程式碼已經成功運行了。

  那修改data.text資料的邏輯我借用一個button來實現:監聽button的click事件,觸發時將data.text的值改為"hello new vue"。

<html>
    <head>
        <meta charset="utf-8" />
        <title>一起學習Vue原始碼-Object的變化偵測</title>
    </head>
    <body>
        <h1>一起學習Vue原始碼-Object的變化偵測</h1>
        <div id="box">
            {{text}}
        </div>
        <br/>
        <button onclick="btnClick()">點選我改變div的內容</button>
    </body>
    <script type="text/javascript" src="./Dep.js"></script> 
    <script type="text/javascript" src="./Observer.js"></script>    
    <script type="text/javascript" src="./Watcher.js"></script>

    <script>
        /*
        *   data: 資料
        *   el: 元素id
        *   exp:物件的屬性
        *   (傳遞這個exp固定引數也是因為沒有模板編譯相關的程式碼,所以就暫時寫死一個屬性)
        *   cb: 真正包含資料更新邏輯的函式
        */
        function Vue(data, el, exp){
            this.data = data;
            this.el = el;
            this.exp = exp;
            // 因為沒有模板相關的程式碼,所以{{text}}的值使用這種方式進行解析
            this.el.innerHTML = this.data[exp];
            this.cb = function(newVal){
                this.el.innerHTML = newVal;
            }
            //初始化vue例項需要將data資料變得可觀測
            new Observer(data);
            //建立一個訂閱者
            new Watcher(this, exp, this.cb);
        }
        var data = {
            text: 'hello Vue'
        };
        var el = document.getElementById('box');
        
        var exp = 'text';

        var vm = new Vue(data, el, exp);

        function btnClick(){
            vm.data.text = "hello new vue";
        }
    </script>
</html>

  我們看一下效果。

  

  

   可以看到,我們的程式碼已經成功執行。

   到此,這篇 "一起學習vue原始碼 - Object的變化偵測" 總結完成。

 

 

結束語:

 我的vue原始碼的學習途徑主要會參考我自己剛入手的《深入淺出vue.js》這本書,同時會參考網上一些內容。

 我會盡量將從原始碼中解讀出的內容,以一種更通俗易懂的方式總結出來。

 如果我的內容能給你帶來幫助,可以持續關注我,或者在評論區指出不足之處。

 同時因為是原始碼學習,所以這個過程中我也充當一個原始碼搬運工的角色,不創造程式碼只搬運並解讀原始碼。

  

作者:小土豆biubiubiu

部落格園:www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

簡書:https://www.jianshu.com/u/cb1c3884e6d5

微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)

碼字不易,點贊鼓勵喲~

&n