Vue數據綁定原理之依賴收集觸發

在上一篇咱們講到了數據劫持,和數據觀測。那麼怎麼將數據和相關的DOM關聯起來呢?本篇咱們將解開這個過程。html

從實例化Watcher開始

上一篇講解中咱們知道Watcher是實際執行數據變動以後操做的主要對象,咱們先找到它的實例化路徑,發現它是在mount的時候進行的操做。vue

Vue -> this._init -> initLifecycle -> mountComponent
複製代碼

咱們在這個方法中找到了關於Watcher的實例化代碼node

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
複製代碼

結合以前的Watcher構造函數:bash

class Watcher {
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
  }
}
複製代碼

咱們先解釋下參數:首先傳入了組件實例vm,而後是expOrFn傳入的是updateComponentcb是一個空函數noopoptions中定義了一個鉤子before,最後傳入了isRenderWatchertrue,代表這是一個RenderWatcher,就會將該Wathcer掛載到組件上。dom

而這裏關鍵的地方就是updateComponent。咱們在上一篇分析中提到,當數據變動,依賴會通知全部訂閱者Watcher作出相應更新,也就是watcher.update,而watcher.update不論是同步仍是異步,其核心是調用wathcer.run去執行相關操做。異步

run () {
  if (this.active) {
    const value = this.get()
    
    // ...
    this.cb.call(this.vm, value, oldValue)
  }
}
複製代碼

這個函數有兩個關鍵的地方,一個是獲取值,調用了get方法,而另外一個就是執行回調函數cb,在上一篇咱們一樣提到,咱們自定義的watch就是經過傳入expcb來實現觀測具體某個屬性的。ide

好比:函數

new Vue({
  data: {
    msg: ''
  },
  watch: {
    msg: function() {}
  }
})
複製代碼

這裏的watch就是經過new Watcher(vm, 'msg', fn)相似這樣的方式定義的,這和咱們如今看見的徹底不同。oop

這也是困惑的一點,咱們如今看到的RenderWatcher傳入了一個空函數做爲cb,也就是說執行cb是沒有任何做用的,那麼在數據更新時是怎麼通知到視圖層的呢?咱們發如今run方法中,除了執行cb外,還執行了get方法。這就是關鍵!性能

在上一篇中咱們提到get方法其實調用的就是getter,在傳入的第二個參數expOrFn類型爲function時,getter = expOrFn。那還記得傳了什麼進去嗎?updateComponent

咱們理一下思路,並暫時移除掉無關代碼:

// function mountComponent
new Watcher(vm, updateComponent, noop)

// class Watcher
class Watcher {
  constructor(vm, fn, cb) {
    this.vm = vm
    this.getter = fn

    this.value = this.get()
  }
  get() {
    this.getter.call(this.vm, this.vm) 
  }
  update() {
    this.run()
  }
  run() {
    const value = this.get()
    
    // ...
    this.cb.call(this.vm, value, this.value)
  }
}
複製代碼

這裏有幾個要點。第一,在初始化Watcher的時候就調用過一次get;第二,在數據更改觸發更新時又會調用get。再根據實際執行的函數名updateComponent,我想你也猜到了,這個函數就是用來渲染DOM的,而且在每次觀測到數據變動時都會從新渲染DOM。

再來看這張圖,至此,WatcherRender的路徑咱們也清晰了。

data.png

updateComponent

咱們猜想該函數是用來更新DOM的,但咱們仍是得實際看一下它是如何實現的,由於這裏面其實涉及到了更多技術,十分值得學習。

那咱們仍是一步一步的來,看完相關代碼,能夠總結出:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
複製代碼

它最主要就是調用了兩個函數,_render_update

_render

咱們先來看看_render,它是經過renderMixin加在原型上的,因此相關定義會在不一樣的地方。

Vue.prototype._render = function (): VNode {}
複製代碼

咱們先看下這個函數聲明,其返回值是一個VNode類型,若是你有仔細讀過官方文檔,你就會對這個詞有點印象。

在建立一個Vue組件的時候咱們能夠不使用template選項來寫DOM模板,而使用render選項。而render函數返回值的類型就是VNode。很顯然,從函數名上來看,內部的_render是對傳入render的二次包裝。

看一看源碼歸納:

Vue.prototype._render = function (): VNode {
  const { render, _parentVnode } = vm.$options
  // ...
  vnode = render.call(vm._renderProxy, vm.$createElement)
  // ...
  return vnode
}
複製代碼

該函數調用了render並返回了VNode。 這裏發什麼什麼?僅僅是調用render函數這麼簡單嗎? 咱們來看看好比下面這個render函數:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}
複製代碼

他用到了this.blogTitle,很明顯這裏是訪問屬性,也就是會調用到該屬性的get方法,上一篇咱們再講Observer時講過,屬性的get裏面會進行依賴收集。此時,blogTitle有了新的訂閱者subs.push(Watcher),而該Watcher的依賴deps也增長了blogTitle,在blogTitle更新時,就會調用該Watcherupdate方法。

因此上面那張圖中的renderdata這條線也清晰了吧,這也就是官方文檔上說的接觸(touched)!

_update

OK,其實到這裏,整個數據劫持,依賴收集過程都已經很明瞭了,咱們已經能夠實現一個簡單而且優雅的數據單向綁定了。接下來就是Vue怎麼優化DOM渲染,提高性能的操做了。

咱們如今知道_render是建立虛擬DOM的,那麼建立完虛擬DOM以後幹嗎?固然是渲染成真實DOM啊!這也就是_update的做用,那爲何它叫作update而不是create或者transform呢?這也是有知識點在裏面的。

// Vue.prototype._update 
const prevVnode = vm._vnode

if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}
複製代碼

能看見清晰的註釋,initial render/updates,也就是該方法處理了新建和更新兩種操做。新建的時候會將VNode掛載在vm上表示已經建立過了,以後只須要更新就好了,減小消耗。

而這裏又用到了另外一個方法__patch__

// runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop

// runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

// vdom/patch.js
export function createPatchFunction (backend) {
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //...
  }
}
複製代碼

介於這裏內容比較複雜,暫時就不講了,咱們留着下一篇再見。

相關文章
相關標籤/搜索