Vue底層學習3——手擼發佈訂閱模式

全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/14986941.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)

作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上一篇完成MVue框架搭建,實現數據劫持並重寫settergetter,本篇就來手擼發佈訂閱模式~

依賴收集與追蹤

上一篇的結尾我們通過console.log(`${key}屬性更新了:${val}`);預留了視圖更新的代碼位置,也就是當數據發生變更時,我們需要做數據對應的視圖更新,那麼到底更新哪些視圖,就是依賴收集的意義。首先看一個日常的例子:

new Vue({
    template:
       `<div>
            <span>{{name1}}</span>
            <span>{{name2}}</span>
            <span>{{name1}}</span>
        </div>`,
    data: {
    	name1: 'name1',
        name2: 'name2',
        name3: 'name3'
    },
    created() {
    	this.name1 = 'change name1';
        this.name3 = 'change name3';
    }
});

根據頁面綁定的data我們可以整理出以下邏輯:

  • name1被修改,視圖更新2處;
  • name2被修改,視圖更新1處;
  • name3被修改,視圖無需更新;

所以我們需要做的事是掃描視圖收集依賴,得知視圖中哪裏對數據有依賴後,對應數據變更時就可以得到通知,接下來可以對照第一篇《Vue底層學習1——原理解析》中的簡化版原理圖實現,DepWatcher遵從發佈訂閱模式,也是本篇的重點,建議後續代碼結合着原理圖看,思路會更清晰哦~

依賴對象

首先需要實現的是依賴對象Dep,主要用於依賴收集,管理Watcher,它與數據屬性一一對應。其中需要提供2個方法:添加觀察者、通知觀察者。

/*** MVue.js ***/
// 依賴收集,管理Watcher
class Dep {
  constructor() {
    // 存放所有的依賴(Watcher)
    this.deps = [];
  }

  // 在deps中添加一個觀察者對象
  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知所有的觀察者去更新視圖
  notify() {
    this.deps.forEach((dep => dep.update()));
  }
}

觀察者對象

接下來實現觀察者對象Watcher,主要用於視圖更新。其中需要提供更新視圖的方法,構造函數中有一個看似奇怪的操作,後續會詳細說明。

/*** MVue.js ***/
class Watcher {
  constructor() {
    // 將當前Watcher的實例指定到Dep靜態屬性target
    Dep.target = this;
  }

  update() {
    // 預留視圖更新
    console.log('數據更新了,需要我們更新視圖');
  }
}

自建框架整合

根據上面的實現,我們把代碼跟之前的做一下初步整合:

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 數據緩存
    this.$options = options;
    this.$data = options.data;

    // 數據遍歷
    this.observe(this.$data);
  }

  observe(data) {
    // 確定data存在並且爲對象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍歷data對象
    Object.keys(data).forEach(key => {
        // 重寫對象屬性的getter和setter,實現數據的響應化
        this.defineReactive(data, key, data[key]);
    })
  }

  defineReactive(obj, key, val) {
    // 解決數據嵌套,遞歸實現深度遍歷
    this.observe(val);

    Object.defineProperty(obj, key, {
      get: function() {
        return val;
      },
      set: function(newVal) {
        // 判斷屬性值是否發生變化
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 預留視圖更新
        console.log(`${key}屬性更新了:${val}`);
      }
    })
  }
}

// 依賴收集,管理Watcher
class Dep {
  constructor() {
    // 存放所有的依賴
    this.deps = [];
  }

  // 在deps中添加一個觀察者對象
  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知所有的觀察者去更新視圖
  notify() {
    this.deps.forEach((dep => dep.update()));
  }
}

class Watcher {
  constructor() {
    // 將當前Watcher的實例指定到Dep靜態屬性target
    Dep.target = this;
  }

  update() {
    // 預留視圖更新
    console.log('數據更新了,需要我們更新視圖');
  }
}

接下來需要將原本預留在defineReactive中的視圖更新替換成發佈訂閱模式:

  • 第1步:我們需要在defineReactive中定義Dep,這樣在將來的某個時刻就能把收集的依賴(Watcher)放進去;
  • 第2步:我們需要替換掉Line42中預留的部分,修改爲dep.notify(),實現通知Watcher的功能,最終由Watcher完成視圖更新。那麼Watcher怎麼來?交給第三步;
  • 第3步:在MVue構造函數中先模擬一下Watcher的創建過程,即new Watcher(),接下來就發生了神奇的現象,也就是前面提到的Watcher構造函數中那行奇怪的操作,Watcher當前的實例會被指定到Deptarget靜態屬性,這樣做的目的就是爲了將Watcher添加到之前創建的Dep中;
  • 第4步:在屬性的getter中添加依賴收集,即dep.addDep(Dep.target),當然需要先判斷target是否存在;
  • 第5步:要想依賴可以成功收集,那麼我們需要觸發getter,也就是讀取一下屬性,同樣在MVue構造函數中模擬;

修改後代碼如下:

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 數據緩存
    this.$options = options;
    this.$data = options.data;

    // 數據遍歷
    this.observe(this.$data);

    // 模擬Watcher的創建過程——第3步
    new Watcher();
    // 模擬屬性讀取,激活getter,實現依賴收集——第5步
    this.$data.name;
    
    // 模擬Watcher的創建過程——第2步
    new Watcher();
     // 模擬屬性讀取,激活getter,實現依賴收集——第5步
    this.$data.infoObj.location;
  }

  observe(data) {
    // 確定data存在並且爲對象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍歷data對象
    Object.keys(data).forEach(key => {
        // 重寫對象屬性的getter和setter,實現數據的響應化
        this.defineReactive(data, key, data[key]);
    })
  }

  defineReactive(obj, key, val) {
    // 解決數據嵌套,遞歸實現深度遍歷
    this.observe(val);

    // 初始化Dep——第1步
    const dep = new Dep();

    Object.defineProperty(obj, key, {
      get: function() {
      	// 依賴收集,將當前屬性對應的Watcher添加至Dep中——第4步
        Dep.target && dep.addDep(Dep.target);
        return val;
      },
      set: function(newVal) {
        // 判斷屬性值是否發生變化
        if (newVal === val) {
          return;
        }
        val = newVal;

        // 通知觀察者更新視圖——第2步
        dep.notify();
      }
    })
  }
}

// 依賴收集,管理Watcher
class Dep {
  constructor() {
    // 存放所有的依賴
    this.deps = [];
  }

  // 在deps中添加一個觀察者對象
  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知所有的觀察者去更新視圖
  notify() {
    this.deps.forEach((dep => dep.update()));
  }
}

class Watcher {
  constructor() {
    // 將當前Watcher的實例指定到Dep靜態屬性target
    Dep.target = this;
  }

  update() {
    // 預留視圖更新
    console.log('數據更新了,需要我們更新視圖');
  }
}

自建框架測試demo1

運行上一篇《Vue底層學習2——手擼數據響應化》中的demo1案例,執行結果如下,說明我們的發佈訂閱模式成功替換:

總結

本篇實現發佈訂閱模式的整體過程可以歸納如下:新增一個Dep類的實例來做依賴收集。讀取數據時,會觸發getter把當前的Watcher(存放在Dep.target中)收集到Dep實例中。寫入數據時,會觸發setter通知Dep類調用notify方法,以此觸發所有Watcherupdate方法來更新對應的視圖。

最後提個小tips——每個Dep針對單個屬性,有多少個數據屬性就有多少Dep,但是一個Dep中可能有多個Watcher,因爲一個屬性可能在視圖中出現多次。

參考資料

1、Vue源碼:https://github.com/vuejs/vue

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章