Vue 理解之白話 getter/setter

原文 更好的閱讀體驗html

當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter。Object.defineProperty 是 ES5 中一個沒法 shim 的特性,這也就是爲何 Vue 不支持 IE8 以及更低版本瀏覽器vue

以上摘自 深刻響應式原理react

那麼,把這些屬性所有轉爲 getter/setter 具體是怎樣一個過程呢?本文不深刻具體,簡單大體瞭解其過程,旨在總體把握,理解其主要思路git

假設代碼以下:github

const vm = new Vue({
  el: '#app',
  data: {
    msg: 'hello world'
  }
})
複製代碼

data 選項能夠接收一個對象或者方法,這裏以對象爲例(其實最後都會轉爲對象)express

首先,這個對象的全部鍵值對都會被掛載在 vm._data 上(此外 vm._data 對象上還有個 __ob__ key,暫時能夠忽視),這樣咱們便能用 vm._data.msg 訪問到數據api

可是一般咱們是用 vm.msg 這樣訪問數據,如何作到的呢?其實就是作了個代理,將 data 鍵值對中的 vm[key] 的訪問都代理到 vm._data[key] 上數組

proxy(vm, `_data`, key)

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼

一般 vm._data (下劃線變量)用做內部程序,對外暴露的 API 是 vm.$data,其實這二者也是一個東西,也是作了個代理,代碼大概這樣:瀏覽器

const dataDef = {}
dataDef.get = function () { return this._data }

Object.defineProperty(Vue.prototype, '$data', dataDef)

if (process.env.NODE_ENV !== 'production') {
  dataDef.set = function () {
    warn(
      'Avoid replacing instance root $data. ' +
      'Use nested data properties instead.',
      this
    )
  }
}
複製代碼

簡單理解就是訪問 vm.data 的時候,就會代理到 vm.\_data,因此訪問 vm.data.msg 其實就是訪問 vm._data.msg。若是直接在開發環境對 vm.data 賦值會有個警告(是 `vm.data = xxx這樣的賦值,而不是vm.$data.msg = xxx` 這樣的賦值,後者是沒問題的)app

至此,咱們理解了爲何能用 vm.msgvm._data.msg 以及 vm.$data.msg 三種方式獲取/改變數據,最原始的數據是 vm._data.msg,而另外二者即代理了 _data 的數據,vm.$data.msg 即爲 Vue 向外提供的 API,通常狀況下開發咱們直接用 vm.msg 這樣比較多,也方便,若是要獲取整個 data,程序中須要用 this.$data,而不是 this.data

接下來講 getter/setter

將 demo 稍微添點東西:

const vm = new Vue({
  el: '#app',
  data: {
    msg: 'hello world'
  },
  computed: {
    msg2() {
      return this.msg + '123'
    }
  }
})
複製代碼

msg2 是依賴於 msg 的,當 msg 改變的時候,msg2 的值須要自動更新,msg 的改變能夠在 vm._data.msg 的 setter 中監聽到,可是怎麼知道 msg2 是依賴於 msg 的呢?

直觀地咱們能夠想到,遍歷全部 computed 對象的鍵值對,而後進行分析,理論上彷佛可行,可是我尋思着這可能須要解析 AST 啊,或者正則去匹配,看看是否用到了 this.msg,也多是 this.$data.msg 啊,還多是 this._data.msg,並且還要遍歷 data 中的全部 key,這看起來也太麻煩了吧,並且,若是程序中沒有用到 msg2,那不是畫蛇添足了?

事實上,Vue 初始化的時候會對 vm._data 的每一個鍵值對設置 getter/setter,大概代碼以下:

// obj 即爲 vm._data
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
  defineReactive(obj, keys[i])
}

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  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
  },
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    /* eslint-enable no-self-compare */
    if (process.env.NODE_ENV !== 'production' && customSetter) {
      customSetter()
    }
    // #7981: for accessor properties without setter
    if (getter && !setter) return
    if (setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})
複製代碼

Vue 響應式核心就是,getter 的時候會收集依賴,setter 的時候會觸發依賴更新

咱們仍是以上面的 computed msg2 爲例,當咱們第一次去取值 msg2 時(注意,必須是取值行爲,能夠是在 template,也能夠是程序中),勢必須要去取值 this.msg,這就會觸發 msg 的 getter,此時咱們就能夠肯定 msg2 依賴於 msg

msg 能夠被哪些東西依賴呢?目前看來有三

  1. template 模版中
  2. computed
  3. watch

咱們能夠打印 vm._watchers 查看,是一個 Watcher 實例數組,直接看實例的 expression 值,其實就是觸發這個表達式的時候,會觸發 msg 的 getter

而這個表達式就對應上述的三種狀況,由於 msg 改變的時候,這些表達式須要從新求值,因此這些依賴項都要保存起來,因此源碼中定於了這個 Watcher 類

A watcher parses an expression, collects dependencies, and fires callback when the expression value changes. This is used for both the $watch() api and directives.

watcher.deps 數組表示該 watcher 的依賴項,值爲 Dep 實例,能夠理解成和 Watcher 實例的表達式有關的 data 數據。注意,deps 數組多是空,對於 template 而言,能夠是 template 中不依賴於 data,對於 computed 而言,能夠是這個 computed 數據還沒被獲取(好比我定義了 msg2,可是程序中沒有用,這時 deps 爲空,這代表我若是改變了 msg,可是不須要通知到 msg2,由於 msg2 根本沒用到嘛,可是我在控制檯輸入 vm.msg2,從而觸發了 msg 的 getter,繼而進行了依賴收集,這時 deps 就不爲空了,這代表我已經使用了 msg2,下次 msg 更新時須要通知到 msg2 進行改變)

而對於 watch 而言,我試了下任何狀況下 deps 都不爲空,這須要進一步查看源碼確認

deps 數組元素是 Dep 實例,該實例有個 subs 屬性,是 Watcher 實例數組,表示依賴於這個 Dep 的項目

Watcher 和 Dep 比較難理解,能夠暫時這樣理解,Dep 和 data 掛鉤,每個 Dep 實例就對應 data 的一個鍵值對,Watcher 實例則依賴於 Dep,那麼有三個狀況會依賴,也就是以上三種(想一想是否是這樣,當數據更新的時候,是否是隻有這三處須要同時更新,或者同時響應)

總結下:咱們會對 data 中全部鍵值對設置 getter/setter,getter 的時候咱們會收集依賴(依賴項爲上面三項,並非任何狀況下都會收集依賴,好比在鉤子中打印 msg,這時候就沒依賴,因此源碼中這裏還有複雜判斷),setter 的時候咱們會將收集的依賴項觸發,從而更新數據,理解了這些,就能初步理解 Vue 的響應式原理

若是看到最後,不妨關注個人公衆號「碼農隨手記」

相關文章
相關標籤/搜索