你能夠手寫Vue2的響應式原理嗎?

寫在前面

這道題目是面試中至關高頻的一道題目了,但凡你簡歷上有寫:「熟練使用Vue並閱讀過其部分源碼」,那麼這道題目十有八九面試官都會去問你。前端

什麼?你簡歷上不寫閱讀過源碼,那面試官也頗有可能會問你是否閱讀過響應式相關的源碼vue

仍是那句歌詞唱的:面試

掙不脫 逃不過
眉頭解不開的結
命中解不開的劫
複製代碼

總體流程

做爲一個前端的MVVM框架,Vue的基本思路和AngularReact並沒有二致,其核心就在於: 當數據變化時,自動去刷新頁面DOM,這使得咱們能從繁瑣的DOM操做中解放出來,從而專心地去處理業務邏輯。數組

這就是Vue的數據雙向綁定(又稱響應式原理)。數據雙向綁定是Vue最獨特的特性之一。此處咱們用官方的一張流程圖來簡要地說明一下Vue響應式系統的整個流程: markdown

Vue中,每一個組件實例都有相應的watcher實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的setter被調用時,會通知watcher從新計算,從而導致它關聯的組件得以更新。框架

這是一個典型的觀察者模式。函數

關鍵角色

在 Vue 數據雙向綁定的實現邏輯裏,有這樣三個關鍵角色:ui

  • Observer: 它的做用是給對象的屬性添加gettersetter,用於依賴收集和派發更新this

  • Dep: 用於收集當前響應式對象的依賴關係,每一個響應式對象包括子對象都擁有一個Dep實例(裏面subsWatcher實例數組),當數據有變動時,會經過dep.notify()通知各個watcherspa

  • Watcher: 觀察者對象 , 實例分爲渲染 watcher (render watcher),計算屬性 watcher (computed watcher),偵聽器 watcher(user watcher)三種

Watcher 和 Dep 的關係

爲何要單獨拎出來一小節專門來講這個問題呢?由於大部分同窗只是知道:Vue的響應式原理是經過Object.defineProperty實現的。被Object.defineProperty綁定過的對象,會變成「響應式」化。也就是改變這個對象的時候會觸發getset事件。

可是對於裏面具體的對象依賴關係並非很清楚,這樣也就給了面試官一種:你只是背了答案,對於響應式的內部實現細節,你並非很清楚的印象。

關於Watcher 和 Dep 的關係這個問題,其實剛開始我也不是很清楚,在查閱了相關資料後,才逐漸對裏面的具體實現有了清晰的理解。

剛接觸Dep這個詞的同窗都會比較懵: Dep到底是用來作什麼的呢?咱們經過defineReactive方法將data中的數據進行響應式後,雖然能夠監聽到數據的變化了,那咱們怎麼處理通知視圖就更新呢?

Dep就是幫咱們依賴管理的。

如上圖所示:一個屬性可能有多個依賴,每一個響應式數據都有一個Dep來管理它的依賴。

一段話總結原理

上面說了那麼多,下面我總結一下Vue響應式的核心設計思路:

當建立Vue實例時,vue會遍歷data選項的屬性,利用Object.defineProperty爲屬性添加gettersetter對數據的讀取進行劫持(getter用來依賴收集,setter用來派發更新),而且在內部追蹤依賴,在屬性被訪問和修改時通知變化。

每一個組件實例會有相應的watcher實例,會在組件渲染的過程當中記錄依賴的全部數據屬性(進行依賴收集,還有computed watcher,user watcher實例),以後依賴項被改動時,setter方法會通知依賴與此datawatcher實例從新計算(派發更新),從而使它關聯的組件從新渲染。

到這裏,咱們已經瞭解了「套路」,下面讓咱們用僞代碼來實現一下Vue的響應式吧!

核心實現

/** * @name Vue數據雙向綁定(響應式系統)的實現原理 */

// observe方法遍歷幷包裝對象屬性
function observe(target) {
  // 若target是一個對象,則遍歷它
  if (target && typeof target === "Object") {
    Object.keys(target).forEach((key) => {
      // defineReactive方法會給目標屬性裝上「監聽器」
      defineReactive(target, key, target[key]);
    });
  }
}
// 定義defineReactive方法
function defineReactive(target, key, val) {
  const dep = new Dep();
  // 屬性值也多是object類型,這種狀況下須要調用observe進行遞歸遍歷
  observe(val);
  // 爲當前屬性安裝監聽器
  Object.defineProperty(target, key, {
    // 可枚舉
    enumerable: true,
    // 不可配置
    configurable: false,
    get: function () {
      return val;
    },
    // 監聽器函數
    set: function (value) {
      dep.notify();
    },
  });
}

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

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}
複製代碼
相關文章
相關標籤/搜索