【vue】用圖告訴你響應式原理

laihuamin發表於2019-06-18

前言

如果自己去實現資料驅動的模式,如何解決一下幾個問題:

  • 通過什麼手段去知道我的資料變了?
  • 通過什麼東西去同步更新檢視?

資料劫持——obvserver

我們需要知道資料的獲取和改變,資料劫持是最基礎的手段。在Obeserver中,我們可以看到程式碼如下:

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      // ...
    }
  })
複製程式碼

通過Object.defineProperty這個方法,我們可以在資料發生改變或者獲取的時候,插入一些自定義操作。同理,vue也是在這個方法中做依賴收集和派發更新的。

繫結和更新檢視——watcher

從初始化開始,我們渲染檢視的時候,便會生成一個watcher,他是監視檢視中引數變化以及更新檢視的。程式碼如下:

// 在mount的生命鉤子中
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true /* isRenderWatcher */)
複製程式碼

當然,我們可以保留疑問:

  • watcher是怎麼去更新檢視的
  • 資料又是怎麼和watcher聯動起來的

具體的繫結和更新的流程,我們到後續的依賴收集中講解。

我們先來講講響應式系統中涉及到的設計模式。

釋出訂閱模式

在釋出訂閱模式中,釋出者和訂閱者之間多了一個釋出通道;一方面從釋出者接收事件,另一方面向訂閱者釋出事件;訂閱者需要從事件通道訂閱事件

以此避免釋出者和訂閱者之間產生依賴關係

【vue】用圖告訴你響應式原理

vue的響應式流程

vue的響應式系統借鑑了資料劫持和釋出訂閱模式。

【vue】用圖告訴你響應式原理

Vue用Dep作為一箇中間者,解藕了Observer和Watcher之間的關係,使得兩者的職能更加明確。

那具體是如何來完成依賴收集和訂閱更新的呢?

依賴收集過程

  • 依賴收集的流程

舉個例子

<div id="app">
    {{ message }}
    {{ message1 }}
    <input type="text" v-model="message">
    <div @click="changeMessage">改變message</div>        
</div>
複製程式碼
var app = new Vue({
    el: '#app',
    data: {
        message: '1',
        message1: '2',
    },
    methods: {
        changeMessage() {
            this.message = '2'
        }
    },
    watch: {
        message: function(val) {
            this.message1 = val
        }
    }
})
複製程式碼

依賴收集流程圖:

【vue】用圖告訴你響應式原理

如何看懂這個依賴收集流程?關鍵在watcher程式碼中:

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      // 省略
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
複製程式碼

呼叫的這個this.getter有兩種,一種是key值的getter方法,還有一種是expOrFn,比如mounted中傳入的updateComponent。

  • 如何防止重複收集

我們不妨想想什麼才算是重複收集了?

筆者想到一種情況:就是dep陣列中,出現了多個一樣的watcher。

比如renderWatch就容易被重複收集,因為我們在html模版中,會重複使用data中的某個變數。那他是如何去重的呢?

1、只有watch在執行get時,觸發的取數操作,才會被收集

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      dep.notify()
    }
  })
複製程式碼

當只有Dep.target這個存在的時候才進行依賴收集。Dep.target這個值只有在watcher執行get方法的時候才會存在。

2、在dep.depend的時候會判斷watch的id

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}
複製程式碼
addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
 }
複製程式碼

我們會發現,在depend過程中,會有一個newDepIds去記錄已經存入的dep的id,當一個watcher已經被該dep存過時,便不再會進行依賴收集操作。

派發更新過程

收集流程講完了,不妨在聽聽更新流程。

  • 訂閱更新的流程 老例子
<div id="app">
    {{ message }}
    {{ message1 }}
    <input type="text" v-model="message">
    <div @click="changeMessage">改變message</div>        
</div>
複製程式碼
var app = new Vue({
    el: '#app',
    data: {
        message: '1',
        message1: '2',
    },
    methods: {
        changeMessage() {
            this.message = '3'
        }
    },
    watch: {
        message: function(val) {
            this.message1 = val
        }
    }
})
複製程式碼

依賴收集的最終結果:

【vue】用圖告訴你響應式原理

當觸發click事件的時候,便會觸發訂閱更新流程。

訂閱更新流程圖:

【vue】用圖告訴你響應式原理

當renderWatch執行更新的時候,回去呼叫beforeUpdate生命鉤子,然後執行patch方法,進行檢視的變更。

  • 如何防止重複更新

如何去防止重複更新呢?renderWatch會被很多dep進行收集,如果檢視多次渲染,會造成效能問題。

其實問題的關在在於——queueWatcher

在queueWatcher中有兩個操作:去重和非同步更新。

function queueWatcher (watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    queue.push(watcher)
    // ...
    if (!waiting) {
      waiting = true
      // ...
      nextTick(flushSchedulerQueue)
    }
  }
}
複製程式碼

其實queueWatcher很簡單,將所有watch收集到一個陣列當中,然後去重。

這樣至少可以避免renderWatch頻繁更新。

比如上述例子中的,message和message1都有一個renderWatch,但是隻會執行一次。

非同步更新也可以保證當一個事件結束之後,才會觸發檢視層的更新,也能防止renderWatch重複更新

結尾

文章講述了響應式流程的原因,程式碼細節並未深入,如果喜歡瞭解原始碼的,可以翻看筆者其他的文章:

Observer原始碼解析

Watcher原始碼解析(未完成)

相關文章