Vue源碼分析之Observer

碎碎念

四月份真是慵懶無比的一個月份,看着手頭上沒啥事幹,只好翻翻代碼啥的,看了一會Vue的源碼,忽而有點感悟,因而便記錄一下。javascript

Vue中的觀察者模式

觀察者模式通常包含發佈者(Publisher)和訂閱者(Subscriber)兩種角色;顧名思義發佈者負責發佈消息,訂閱者經過訂閱消息響應動做了。
回到Vue中,在Vue源碼core/oberver目錄下分析代碼能夠知道有三個類分別是Oberver,Watcher和Dep;那這三個類中誰是Publisher,誰是Subscriber尼?java

Observer

觀察者,這個觀察者究竟觀察什麼的尼?
仍是用最簡單粗暴的方式,目錄搜索一下哪裏用到這個類,步步追尋,大體是這樣一個調用過程。react

initState()-->observe(data)-->new Observer()

基本上Vue在咱們的data對象上都會定義一個__ob__屬性指向新建立的Observer對象,就像這樣子:數組

{
    a: {
        b: {
            d: 1
            __ob__: [Observer Object]
        }
        c: { e: 1, f: 2, g: 3 } //也是有__ob__屬性的
        __ob__: [Observer Object]
    }
    __ob__: [Observer Object]
}

這裏能夠知道其實對象或者數組裏面Vue都會幫你添加一個__ob__屬性,可是這個__ob__屬性或者這個Observer對象到底是幹嗎用的尼?
先舉個栗子:瀏覽器

<div>
    <ul v-for="el in a.c">
        <li></li>
    </ul>
</div>

在模板裏面咱們遍歷數組內容,很明顯數組有多少元素就會輸出多少個li;那麼咱們數組元素增長和刪除的時候怎麼通知到組件去從新渲染尼?
恩,答案就是經過這個__ob__屬性。
好,直接上代碼:異步

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep() //1. 爲屬性建立一個發佈者
  ...
  let childOb = observe(val) //2. 獲取屬性值的__ob__屬性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
      if (Dep.target) {
        dep.depend() //3. 添加訂閱者
        if (childOb) {
          childOb.dep.depend() //4. 也爲屬性值添加一樣的訂閱者
        }
        if (Array.isArray(value)) {
          dependArray(value) // 同上
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

第4步至關重要,若是沒有第4步,咱們添加或者刪除元素,剛纔那個組件是不會從新渲染的;咱們通常狀況下都會想到去攔截屬性的get和set方法,在get的方法咱們能夠收集訂閱者,set的方法咱們簡單的判斷舊的值和新的值是否相等咱們就能夠通知訂閱者去更新;可是對於引用的值(相似Object或者Array)這樣就不行了,咱們得讓他們內容發生變化(主要是增長刪除內容,對象增長一個屬性時候)的時候也要通知訂閱者去更新,因此__ob__上的dep屬性主要用於監控對象屬性增長和刪減而第1步所建立的dep用於監控屬性值的更新。oop

但在這裏的例子也致使另一個行爲,咱們剛纔在例子中很明顯並無實際用到數組的內容,然而在for循環的過程當中,也就等同於咱們遍歷對象全部內容,Vue就會認爲咱們會「關心」這些內容的變化,因此當對象的內容(假設這個對象裏的元素也是對象,在某個子對象上增長或者刪除一個屬性)發生變化的時候也會觸發從新渲染;性能

還有的是Vue對數組的處理跟對象仍是有挺大的不一樣,length是數組的一個很重要的屬性,不管數組增長元素或者刪除元素(經過splice,push等方法操做)length的值一定會更新,那麼豈不是一勞永逸,不須要攔截splice,push等方法就能夠知道數組的狀態更新,可是當我試着在數組length屬性上用defineProperty攔截的時候,冒出了這樣的錯誤:學習

Uncaught TypeError: Cannot redefine property: length

不能重定義length屬性??再用Object.getOwnPropertyDescriptor(arr, 'length')查看一下:this

{
    configurable: false
    enumerable: false
    value: 0
    writable: true
}

configurable爲false,看來Object.defineProperty真的不行了,而MDN上也說重定義數組的length屬性在不一樣瀏覽器上表現也是不一致的,因此仍是老老實實攔截splice,push等方法,要麼就等ES6的Proxy才能夠作到了。
那麼數組的下標可使用defineProperty攔截嗎? 答案:是能夠的。
那麼Vue也是是對待普通對象同樣對數組全部下標進行了攔截嗎? 答案:是否認的。
因此像這樣:

this.arr[0] = 1;

徹底不行的。
那麼爲啥不直接遍歷數組而後攔截數組的下標尼,我大概想了一下答案:
性能的考慮,數組可能很大,一次性都對下標進行攔截,會有性能影響;數組可能運行時變化很大,增刪頻繁。
[2019.01.25]實際上是由於用Object.defineProperty方法攔截下標的話會讓數組進入字典模式,效率會極其低下,參考文章最後一段
還有沒有其餘緣由尼,這個還有待學習,可是看到源碼其中是這樣收集數組的依賴的:

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

遞歸收集數組的依賴了,全部子數組的變化也會觸發當前觀察者,這是個值得注意的地方。

因此咱們能夠再看添加一個元素的時候:

function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  const ob = (target : any).__ob__
  ...
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

最終會讓Observer的dep屬性去通知更新。

Observer對象的做用可讓一個普通的對象變成"Reactive",而Dep則是充當最終的發佈者角色。

Dep

當Dep的notify方法調起時,便遍歷subs(訂閱者數組就是Array<Watcher>)調用訂閱者的update方法。

Watcher

Watcher的update方法調起,便把Watcher壓入schedule隊列中,等待nextTick異步執行,固然咱們可使用同步模式,直接執行Watcher的run方法方便咱們調試。
Vue中主要有兩種類型的Watcher,一種是Render Watcher,另一種是User Watcher;
User Watcher是經過vm.$watch 或者 options中的watch屬性定義的。
Render Watcher又是啥尼,看了一下initRender()方法,追蹤一下調用過程,來到Vue.prototype._mount方法,能夠看到:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
    }, noop)

這個就應該是Render Watcher了;
咱們定義在options中的watch對象是在initState方法中初始化,而initState又比initRender先調用,因此組件中User Watcher確定比Render Watcher優先級高(User Watcher的id比Render Watcher小);
可是咱們在mounted生命週期中使用vm.$watch定義的Watcher就不必定了(我的推測),由於Render Watcher已經建立。

Dep 和 Watcher

通常訂閱者模式都是一對多的關係(一個發佈者對應多個訂閱者),可是在這裏Dep和Watcher是多對多的關係,因此就有;

  1. 一個Watcher能夠偵測多個屬性的變化(在Render的時候,RenderWatcher就收集了咱們在模板裏面所使用的各類屬性的依賴,因此當咱們修改模板裏面任意一個變量時都會觸發RenderWatcher從新Render)
  2. Dep能夠被多個Watcher收集(例如咱們能夠定義多個vm.$watch同一個屬性,當屬性變化時就能夠觸發多個Watcher)
  3. 另外Props定義的屬性默認是不會偵測的(可是若是Props有默認值,也是會調用Observe),由於Props的屬性都是由父組件傳遞給子組件,當Props屬性修改時,父組件會先本身從新Render,也會致使子組件Render,而後開始Diff流程。

關於渲染時依賴收集

在Render Watcher中Wachter.run方法會調起vm._render()方法,這樣狀況下咱們在模板中訪問的屬性例如a.b這樣,會在對象的getter中把Render Watcher添加到訂閱者列表中。

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }

因此之後咱們改動相關的屬性時,對象的setter自動會通知到Render Watcher讓Dom結構更新。

結束

好了,基本結束,若有錯漏,望指正。

相關文章
相關標籤/搜索