vue computed 和 vm.$data 原理

仔細閱讀註解內容。會針對源碼原理深度講解 原文轉載地址

使用vuexstore中的數據,基本上離不開vue中一個常用的屬性computed。官方一個最簡單的例子如下

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join()
    }
  }
})

不知大家有沒有思考過,vue的computed是如何更新的,爲什麼當vm.message發生變化時,vm.reversedMessage也會自動發生變化?

我們來看看vuedata屬性和computed相關的源代碼。

// src/core/instance/state.js
// 初始化組件的state
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  // 當組件存在data屬性
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 當組件存在 computed屬性
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState方法當組件實例化時會自動觸發,該方法主要完成了初始化data,methods,props,computed,watch這些我們常用的屬性,我們來看看我們需要關注的initDatainitComputed(爲了節省時間,去除了不太相關的代碼)

先看看 initData 這條線

// src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // .....省略無關代碼
  
  // 將vue的data傳入observe方法
  observe(data, true /* asRootData */)
}

// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  // ...省略無關代碼
  ob = new Observer(value)
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

在初始化的時候observe方法本質上是實例化了一個Observer對象,這個對象的類是這樣的

// src/core/observer/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    // 關鍵代碼 new Dep對象
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // ...省略無關代碼
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 給data的所有屬性調用defineReactive
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

在對象的構造函數中,最後調用了walk方法,該方法即遍歷data中的所有屬性,並調用defineReactive方法,defineReactive方法是vue實現 MDV(Model-Driven-View)的基礎,本質上就是代理了數據的set,get方法,當數據修改或獲取的時候,能夠感知(當然vue還要考慮數組,Object中嵌套Object等各種情況,本文不在分析)。

我們具體看看defineReactive的源代碼

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 重點,在給具體屬性調用該方法時,都會爲該屬性生成唯一的dep對象
  const dep = new Dep()

  // 獲取該屬性的描述對象
  // 該方法會返回對象中某個屬性的具體描述
  // api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果該描述不能被更改,直接返回,因爲不能更改,那麼就無法代理set和get方法,無法做到響應式
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  // 重新定義data當中的屬性,對get和set進行代理。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 收集依賴, reversedMessage爲什麼會跟着message變化的原因
      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
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知依賴進行更新
      dep.notify()
    }
  })
}

我們可以看到,在所代理的屬性的get方法中,當dep.Target存在的時候會調用dep.depend()方法,這個方法非常的簡單,不過在說這個方法之前,我們要認識一個新的類Dep

Depvue 實現的一個處理依賴關係的對象,
主要起到一個紐帶的作用,就是連接 reactive datawatcher,代碼非常的簡單

// src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // 更新 watcher 的值,與 watcher.evaluate() 類似,
      // 但 update 是給依賴變化時使用的,包含對 watch 的處理
      subs[i].update()
    }
  }
}

// 當首次計算 computed 屬性的值時,Dep 將會在計算期間對依賴進行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  // 在一次依賴收集期間,如果有其他依賴收集任務開始(比如:當前 computed 計算屬性嵌套其他 computed 計算屬性),
  // 那麼將會把當前 target 暫存到 targetStack,先進行其他 target 的依賴收集,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  // 當嵌套的依賴收集任務完成後,將 target 恢復爲上一層的 Watcher,並繼續做依賴收集
  Dep.target = targetStack.pop()
}

代碼非常的簡單,回到調用dep.depend()方法的時候,當Dep.Target存在,就會調用,而depend方法則是將該dep加入watchernewDeps中,同時,將所訪問當前屬性的dep對象中的subs插入當前Dep.targetwatcher.看起來有點繞,不過沒關係,我們一會跟着例子講解一下就清楚了。

講完了代理的get,方法,我們講一下代理的set方法,set方法的最後調用了dep.notify(),當設置data中具體屬性值的時候,就會調用該屬性下面的dep.notify()方法,通過class Dep瞭解到,notify方法即將加入該depwatcher全部更新,也就是說,當你修改data中某個屬性值時,會同時調用dep.notify()來更新依賴該值的所有watcher

介紹完了initData這條線,我們繼續來介紹initComputed這條線,這條線主要解決了什麼時候去設置Dep.target的問題(如果沒有設置該值,就不會調用dep.depend(), 即無法獲取依賴)。

// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // 初始化watchers列表
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // 關注點1,給所有屬性生成自己的watcher, 可以在this._computedWatchers下看到
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      // 關注點2
      defineComputed(vm, key, userDef)
    }
  }
}

在初始化computed時,有2個地方需要去關注

對每一個屬性都生成了一個屬於自己的Watcher實例,並將 { lazy: true }作爲options傳入
對每一個屬性調用了defineComputed方法(本質和data一樣,代理了自己的setget方法,我們重點關注代理的get方法)

我們看看Watcher的構造函數

// src/core/observer/watcher.js
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // 如果初始化lazy=true時(暗示是computed屬性),那麼dirty也是true,需要等待更新
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn // 在computed實例化時,將具體的屬性值放入this.getter中
    // 省略不相關的代碼
    this.value = this.lazy
      ? undefined
      : this.get()
  }

除了日常的初始化外,還有2行重要的代碼

this.dirty = this.lazy
this.getter = expOrFn

computed生成的watcher,會將watcherlazy設置爲true,以減少計算量。因此,實例化時,this.dirty也是true,標明數據需要更新操作。我們先記住現在computed中初始化對各個屬性生成的watcherdirtylazy都設置爲了true。同時,將computed傳入的屬性值(一般爲funtion),放入watchergetter中保存起來。

我們在來看看第二個關注點defineComputed所代理屬性的get方法是什麼

// src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 如果找到了該屬性的watcher
    if (watcher) {
      // 和上文對應,初始化時,該dirty爲true,也就是說,當第一次訪問computed中的屬性的時候,會調用 watcher.evaluate()方法;
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

當第一次訪問computed中的值時,會因爲初始化watcher.dirty = watcher.lazy的原因,從而調用evalute()方法,evalute()方法很簡單,就是調用了watcher實例中的get方法以及設置dirty = false,我們將這兩個方法放在一起

// src/core/instance/state.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}
  
get () {  
// 重點1,將當前watcher放入Dep.target對象
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 重點2,當調用用戶傳入的方法時,會觸發什麼?
    value = this.getter.call(vm, vm)
  } catch (e) {
  } finally {
    popTarget()
    // 去除不相關代碼
  }
  return value
}

get方法中中,第一行就調用了pushTarget方法,其作用就是將Dep.target設置爲所傳入的watcher,即所訪問的computed中屬性的watcher,
然後調用了value = this.getter.call(vm, vm)方法,想一想,調用這個方法會發生什麼?

this.getterWatcher構建函數中提到,本質就是用戶傳入的方法,也就是說,this.getter.call(vm, vm)就會調用用戶自己聲明的方法,那麼如果方法裏面用到了 this.data中的值或者其他被用defineReactive包裝過的對象,那麼,訪問this.data.或者其他被defineReactive包裝過的屬性,是不是就會訪問被代理的該屬性的get方法。我們在回頭看看
get方法是什麼樣子的。

注意:我講了其他被用defineReactive,這個和後面的vuex有關係,我們後面在提

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 這個時候,有值了
      if (Dep.target) {
        // computed的watcher依賴了this.data的dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }

代碼註釋已經寫明瞭,就不在解釋了,這個時候我們走完了一個依賴收集流程,知道了computed是如何知道依賴了誰。最後根據this.data所代理的set方法中調用的notify,就可以改變this.data的值,去更新所有依賴this.data值的computed屬性value了。

那麼,我們根據下面的代碼,來簡易拆解獲取依賴並更新的過程

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join()
    }
  }
})
vm.reversedMessage // =>  olleH
vm.message = 'World' // 
vm.reversedMessage // =>  dlroW

初始化 datacomputed,分別代理其set以及get方法, 對data中的所有屬性生成唯一的dep實例。
computed中的reversedMessage生成唯一watcher,並保存在vm._computedWatchers
訪問 reversedMessage,設置Dep.target指向reversedMessagewatcher,調用該屬性具體方法reversedMessage

方法中訪問this.message,即會調用this.message代理的get方法,將this.messagedep加入reversedMessagewatcher,同時該dep中的subs添加這個watcher
設置vm.message = 'World',調用message代理的set方法觸發depnotify方法’
因爲是computed屬性,只是將watcher中的dirty設置爲true
最後一步vm.reversedMessage,訪問其get方法時,得知reversedMessagewatcher.dirtytrue,調用watcher.evaluate()方法獲取新的值。

這樣,也可以解釋了爲什麼有些時候當computed沒有被訪問(或者沒有被模板依賴),當修改了this.data值後,通過vue-tools發現其computed中的值沒有變化的原因,因爲沒有觸發到其get方法。

現在在回頭看 vuex的使用會有一種豁然開朗的感覺 回看vuex 的設計思路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章