深刻源碼學習Vue響應式原理

最近一段時間在閱讀Vue源碼,從它的核心原理入手,開始了源碼的學習,而其核心原理就是其數據的響應式。而且結合設計模式進行學習html

觀察者模式&&發佈訂閱者模式

這裏簡短的介紹這兩種模式的聯繫和差別,react

觀察者模式

觀察者模式定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知,並自動更新。觀察者模式屬於行爲型模式,行爲型模式關注的是對象之間的通信,觀察者模式就是觀察者和被觀察者之間的通信。

觀察者模式有一個別名叫「發佈-訂閱模式」,或者說是「訂閱-發佈模式」,訂閱者和訂閱目標是聯繫在一塊兒的,當訂閱目標發生改變時,逐個通知訂閱者。咱們能夠用報紙期刊的訂閱來形象的說明,當你訂閱了一份報紙,天天都會有一份最新的報紙送到你手上,有多少人訂閱報紙,報社就會發多少份報紙,報社和訂報紙的客戶就是上面文章開頭所說的「一對多」的依賴關係。git

發佈訂閱者模式

其實24種基本的設計模式中並無發佈訂閱模式,上面也說了,他只是觀察者模式的一個別稱。

可是通過時間的沉澱,彷佛他已經強大了起來,已經獨立於觀察者模式,成爲另一種不一樣的設計模式。github

在如今的發佈訂閱模式中,稱爲發佈者的消息發送者不會將消息直接發送給訂閱者,這意味着發佈者和訂閱者不知道彼此的存在。在發佈者和訂閱者之間存在第三個組件,稱爲調度中心或事件通道,它維持着發佈者和訂閱者之間的聯繫,過濾全部發布者傳入的消息並相應地分發它們給訂閱者。設計模式

舉一個例子,你在微博上關注了A,同時其餘不少人也關注了A,那麼當A發佈動態的時候,微博就會爲大家推送這條動態。A就是發佈者,你是訂閱者,微博就是調度中心,你和A是沒有直接的消息往來的,全是經過微博來協調的(你的關注,A的發佈動態)。數組

差別

能夠看出,發佈訂閱模式相比觀察者模式多了個事件通道,事件通道做爲調度中心,管理事件的訂閱和發佈工做,完全隔絕了訂閱者和發佈者的依賴關係。即訂閱者在訂閱事件的時候,只關注事件自己,而不關心誰會發布這個事件;發佈者在發佈事件的時候,只關注事件自己,而不關心誰訂閱了這個事件。閉包

觀察者模式有兩個重要的角色,即目標和觀察者。在目標和觀察者之間是沒有事件通道的。一方面,觀察者要想訂閱目標事件,因爲沒有事件通道,所以必須將本身添加到目標(Subject) 中進行管理;另外一方面,目標在觸發事件的時候,也沒法將通知操做(notify) 委託給事件通道,所以只能親自去通知全部的觀察者。異步

響應式原理

當咱們在data中定義一個值的時候,以下:函數

const vm = new Vue({
    data() {
        return {
            message: ''
        }
    },
    template: '<div>{{message}}</div>'
})
vm.message = 'hello';
複製代碼

此時Vue內部發生了什麼,下面列出須要解決的問題以下:oop

  1. 如何進行依賴收集的
  2. data中的值發生改變時,是如何更新視圖的


上面是表示定義一個 data值的時候,內部這個流程是如何的,結合講解相信你對響應式原理有更深刻的理解。爲了讓結構更加清晰,這裏只考慮一個視圖,而且不會有 computed的狀況。 在講解原理以前,首先對幾個單詞進行定義:

  • Watcher: 訂閱者
  • Observer: 觀察者
  • Dep: 發佈者
  • Data: 實例中的數據項

Observer

首先看看當實例化Vue的時候,對data是如何進行處理的

_init
    => mount => this._watcher = new Watcher(vm, updateComponent, noop)
    => Dep.target = this._watcher
    => observe(data, true)
    => new Observer(data)
複製代碼
  1. 首先new Vue會調用_init函數
  2. mount把須要渲染的模板掛載到元素上
  3. 建立一個Watcher實例
  4. 將上面建立的Watcher實例賦值給Dep.target
  5. data返回的數據進行observe
  6. 調用new Observer遍歷data進行settergetter綁定

下面來看看observe函數的實現:

function observe(value, asRootData) {
    let ob;
    // 檢測當前數據是否被observe過,若是是則沒必要重複綁定
    if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else {
        ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob;
}
複製代碼

首先調用的就是上面這個函數,__ob__用戶判斷是否有Observer實例,若是有就使用原來的,若是沒有就建立一個新的Observer實例。vmCount表示該Vue實例使用的次數,asRootData表示是不是data的跟,例如在一個template中一個相同的組件使用了兩次:

<div>
  <my-component />
  <my-component />
</div>
複製代碼

這個時候vmCount就爲2。接下來看Observer的實現:

class Observer {
    constructor(value) {
        this.value = value;
        this.dep = new Dep();
        this.vmCount = 0;
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            // 若是是數組則須要遍歷數組的每一個成員進行observe
            // 這裏會對數組原有的方法進行從新定義
            this.observeArray(value)
        } else {
            // 若是對象則調用下面的程序
            this.walk(value)
        }
    }
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
複製代碼

下圖是Observer類的結構

這裏主要就是遍歷 data中定義的值,而後在每一個遍歷的屬性下面添加 __ob__,而後在 __ob__定義 Dep,根據數據類型的不一樣調用不一樣的方法,若是是數組則使用 observeArray,該方法會重寫數數組的7種方法,對數組的每一個成員調用 observe函數,若是是普通對象,則遍歷他的屬性調用 defineReactive,進行 getter/setter綁定。 defineReactiveVue最核心的內容,使用方法如: defineReactive(obj, keys[i], obj[keys[i]])。當在 data中定義一個屬性的時候,當咱們更改該值的時候,視圖是如何知道,這個值發生了改變來更新視圖的。

function defineReactive(obj, key, val) {
  // 在閉包中定義一個dep對象
  const dep = new Dep();
  // 對象的子對象遞歸進行observe並返回子節點的Observer對象
  let childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        // 進行依賴收集,dep.depend就是將dep和watcher進行互相綁定
        // Dep.target表示須要綁定的watcher
        dep.depend();
        if (childOb) {
          // 子對象進行依賴收集,其實就是將同一個watcher觀察者實例放進兩個depend中
          // 一個是正在自己閉包中的depend,另外一個是子元素的depend
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          // 若是是數組,須要對數組的每一個成員都進行依賴收集
          dependArray(value)
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      // 經過getter方法獲取當前值,與新值發生比較,一致則不須要執行下面的操做
      const value = getter ? getter.call(obj) : val;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return false;
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 新的值須要從新observe,保證數據響應式
      childOb = observe(newVal)
      // 通知全部觀察者
      dep.notify()
    }
  })
}
複製代碼

經過Object.defineProperty把數據進行了gettersetter綁定。getter用於依賴收集,setter用於經過dep去通知watcher, watcher進行執行變化。 如何進行依賴收集的,能夠經過一個例子進行解釋:

data() {
  return {
    message: [1, 2]
  }
}
複製代碼

結合一個流程圖進行分析上面例子:

observe(data)
=> data.__ob__ = new Observer(data)
=> walk(data)
=> childOb = observe(message)
  => message.__ob__ = new Observer(data)
  => message.__ob__.dep = new Dep;
=> childOb ? childOb.dep.depend();
複製代碼

分析其過程就是:

  1. 先對data函數返回的對象添加__ob__,返回具體的內容以下:
const res = {
  message: [1, 2]
  __ob___: new Observer(data)
}
複製代碼
  1. 遍歷res, 由於res爲對象,因此執行walk
  2. 執行到observe(message)
  3. message添加__ob____ob__上存在一個dep用於依賴收集
  4. childOb = message.__ob__,此時同一個watcher放入子對象中,也就是message.__ob__.dep中 回顧上面的分析,就可以區分出ObserverdefineReactive中兩個dep的區別了,這兩個地方都聲明瞭new DepObserverdep用於收集對象和數組的訂閱者,掛載在對象的屬性上。當對象或者數組增刪元素時調用$set,獲取到__ob__進行依賴收集,而後調用ob.dep.notifyj進行更新。在defineReactive中,這個dep是存在一個閉包中,這是對對象屬性服務的,在獲取屬性值的時候進行依賴收集,設置屬性值的時候發佈更新。

Dep

下面來介紹一下dep,源碼以下:

let uid = 0;
class Dep {
  constructor() {
    this.id = uid++;
    this.subs = []
  }
  // 添加一個訂閱者
  addSub(sub) {
    this.subs.push(sub)
  }
  // 移除一個觀察者對象
  removeSub(sub) {
    remove(this.subs, sub)
  }
  // 依賴收集,當存在Dep.target的時候添加觀察者對象
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  // 通知全部訂閱者
  notify() {
    const subs = this.subs.slice();
    for(let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}
複製代碼

結構以下:

當對象中的屬性觸發 get的時候,先前 defineReactiveconst dep = new Dep()閉包中,就會把當前的 Watcher訂閱者加入到 subs中。 Dep是發佈訂閱者模型中的發佈者, Watcher是訂閱者,一個 Dep實例對應一個對象屬性或一個被觀察的對象,用於收集和數據改變時,發佈更新。好比說有這個一個 data

data() {
  return {
    message: 'a'
  }
}
複製代碼

觸發視圖有兩種方法:

  1. 利用getter/setter,從新設置message的值,設置的過程當中會觸發dep.notify進行發佈更新, 好比this.message = 'b'
  2. 使用$set函數: this.$set(this.message, 'fpx', 'number-one'),這會獲取到message__ob__上的dep進行發佈更新

Watcher

Watcher是一個訂閱者。依賴收集後watcher會被存放在Depsubs中,數據變更的時候經過dep發佈者發佈信息,相關的訂閱者watcher收到信息後經過cb進行視圖更新。 Watcher內容不少,咱們只關注最重要的一些部分:

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm;
    // 存放訂閱者實例
    vm._watchers.push(this)
    this.deps = [];
    this.newDeps = []
    this.depsIds= new Set();
    this.newDepIds new Set();
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get();
  }
  get() {
    pushTarget(this)
    const vm = this.vm;
    value = this.getter.call(vm, vm);
    popTarget();
    this.cleanupDeps();
    return value
  }
  // 添加一個依賴關係到Deps集合中
  addDep(dep) {
    const id = dep.id;
    if (!this.newDepsIds.has(id)) {
      this.newDepsIds.add(id)
      this.newDeps.push(dep);
      // 這裏作一個去重,若是depIds裏包含這個id,那麼以前給depId添加這個id的時候
      // 已經調用過dep.addSub(this),避免了重複添加
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  // 用於更新模板
  update() {
    if (this.sync) {
      // 同步則執行run直接渲染視圖
      this.run();
    } else {
      // 異步推送到觀察者隊列中,下一個tick時調用,最後會調用run方法
      queueWatcher(this)
    }
  }
  // 收集該watcher的全部deps原理
  depend() {
    let i = this.deps.length;
    while(i--) {
      this.deps[i].depend();
    }
  }
}
複製代碼

Watcher結構以下:

首先仍是理清 Watcher構造函數作的事情:

Dep.target = new Watcher(vm, updateComponent, noop = {})
  => 初始化變量
  => 獲取getter函數
  => 調用get函數,get函數會調用getter函數,從而收集依賴
複製代碼

在建立Vue實例的時候,觸發getter就會進行依賴收集,下面是這幾種狀況: Watcher有四個使用的場景,只有在這四種場景中,Watcher纔會收集依賴,更新模板或表達式

  1. 觀察模板中的數據
  2. 建立Vue實例時watch選項裏的數據
  3. computed選型裏的數據所依賴的數據
  4. 使用$watch觀察的數據或者表達式 在前面代碼中聲明瞭Dep.target,這個是幹嗎用的呢。在前面提到依賴收集的時機,是當咱們獲取元素屬性值的時候,可是此時不知道哪一個是正確的watcher,因此定義一個全局變量記錄當前的Watcher,方便添加當前正在執行的WatcherWatcher對象中有兩個屬性: depsnewDeps。他們用來記錄上一次Watcher收集的依賴和新一輪Watcher收集的依賴,每一次數據的更新都須要從新收集依賴, 流程以下:
setter
  => notify => run
  => get
複製代碼

當數據發佈更新後,會調用notify方法,notify會調用run方法,run方法會調用get方法,從新獲取值,從新進行依賴收集。舉一個上面的例子,若是咱們更改了message的值,而且模板依賴了新更改的值,this.message = {key: 'val'},由於上一輪沒有對新值進行依賴,因此這一輪須要從新收集依賴。

總結

Vue初始化的時候,會生成一個watcher,依賴收集就是經過屬性的getter完成的。結合文章開頭給出的圖片,ObserverDep是一對一的關係,DepWatcher是多對多的關係,Dep則是ObserverWatcher之間的紐帶。依賴收集完成偶,當屬性變化會執行被Observer對象的dep.notify()方法,這個方法會遍歷訂閱者Watcher列表向其發送消息,Watcher會執行run方法去更新視圖。

原本還想講點computed的,可是估計您看着也累,我寫着也累,computed將由另一篇文章進行講解。
一篇文章寫下來,很有些難度。下面有三點:

  1. 代碼太多: 由於源碼考慮的狀況不少,當咱們對單個點進行分析的時候,咱們須要摒棄其餘沒有必要的代碼
  2. 流水帳:記錄每行代碼的做用,沒有對更深層次的進行探索
  3. 有機結合:分析了之後,不能和之前學習的知識進行結合

因此給出一些措施來彌補這些問題:

  1. 儘可能少些代碼,把整個流程圖畫出來,圖比代碼更加直觀
  2. 從點上,擴展到線,在擴展到面進行思考
  3. 結合之前學過的知識,好比說這裏的設計模式,結合起來學習 第一次寫這種源碼分析文章,諸多不足,歡迎你們提出寶貴的建議,也請多多關注個人GitHub~~
相關文章
相關標籤/搜索