【源碼解讀】通過分析 Vue computed 的實現,居然發現隱藏的小彩蛋

Vue 的 computed 經常會用到,其中包含以下兩個重點:

1、 computed 的計算結果會進行緩存;

2、只有在響應式依賴發生改變時纔會重新計算結果。

接下從源碼的出發,看看能不能驗證這兩個重點。爲了能更好理解 computed 的實現,文章字數會比較多,請耐心閱讀。

源碼分析

// vue/src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化 props
  if (opts.props) initProps(vm, opts.props)
  // 初始化 methods
  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)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

從初始化狀態的順序可以看出,在翻轉字符串的例子中會先初始化 data,再進行初始化 computed

data 初始化

先看看初始化 data 做了什麼,initData 源碼如下:

// vue/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  // 兼容 對象或函數返回對象的寫法
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  // 判斷 data 是否爲普通對象
  if (!isPlainObject(data)) {
    // data 不是普通對象,重新賦值爲空對象,並在輸出警告
    data = {}
    ...
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // data 的屬性不能與 methods、 props 的屬性重複
    if (process.env.NODE_ENV !== 'production') {
      // 重複 key,輸出警告
      ...
    } else if (!isReserved(key)) {
      // 將每個 key 掛載到實例上,在組件內就可以用 this.key 取值
      proxy(vm, `_data`, key)
    }
  }
  // 監聽 data
  // observe data
  observe(data, true /* asRootData */)
}

初始化 data,主要做了 3 點,1、屬性名重複的判斷;2、將屬性掛載到 vm 上;3、監聽 data。

接下來看看 observe 的實現,源碼如下:

// vue/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 非對象 或者是 VNode,直接 return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 存在 '__ob__' 屬性,表示已經監聽
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 開始創建監聽
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

接下來則到了 Observer 類,源碼如下:

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

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 將 '__ob__' 掛載到 value 上,避免重複監聽
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 將對象每個屬性添加 getter、 setter
      defineReactive(obj, keys[i])
    }
  }
  
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
     // 對數組的每一項進行監聽
      observe(items[i])
    }
  }
}

接下來會調用 defineReactive,源碼如下:

// vue/src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // dep 用於依賴收集
  const dep = new Dep()

  ...

  // data 的值有可能包含數組、對象,在這裏 data 的值進行監聽
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Dep.target 是一個靜態屬性
      // 給 data 的屬性添加 getter 時,target 爲 undefined,不會進行依賴收集
      // 當 computed 用了 data中的屬性時時將會進行依賴收集,先跳過這部分,等到了 computed 再回來看
      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
      ...
      // #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()
    }
  })
}

defineReactive 中用到了 Dep 用來進行依賴收集,接下來看看 Dep 的源碼:

// vue/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)
  }

  // 將 Dep 實例傳遞給目標 Watcher 上,目標 Watcher 再通過 addSub 進行訂閱
  depend () {
    // 只有目標 Watcher 存在纔可以進行訂閱
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知訂閱者
  notify () {
    // 根據 Watcher id 進行排序,通知更新
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 調用訂閱 Watcher 的 update 方法進行更新
      subs[i].update()
    }
  }
}

Dep.target = null
const targetStack = []

// 添加目標 Watcher,並將 Dep.target 指向最新的 Wathcer
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 移除目標 Watcher,並將 Dep.target 指向 targetStack 的最後一個
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Dep 其實就是一個訂閱發佈模式,說明一下最主要的兩個地方

1、pushTargetpopTarget

這兩個方法中用到了 targetStack 堆棧,這樣做就可以進行嵌套,比如在給某個 Watcher 收集依賴的時候,發現了新的 Watcher 需要收集依賴,這樣就可以 target 指向新的 Watcher,先把新的 Watcher 收集完再 popTarget,再進行上一個 Watcher 的收集。

2、depend

depend 執行的是 Watcher 的 addDep 方法,看看 addDep 怎麼寫的

// vue/src/core/observer/watcher.js
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)
    }
  }
}

addDep 做了一些判斷避免重複訂閱,再調用 addSub 添加訂閱。

再回過頭來看看 initData

// vue/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
}

data 是一個函數時,會調用 getData 獲取 data 函數的返回值,看看 getData 的實現。

// vue/src/core/instance/state.js
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

可以看到在執行 data 函數前後,執行了 pushTargetpopTarget 的操作,因爲 data 的屬性並不依賴其他響應式變量、在設置 gettersetter 時,因爲 dep.targetundefined 所以並不會收集依賴。

data 的初始化到這裏就差不多了,接下來看看 computed 的初始化。

computed 初始化

同樣的,先從 initComputed 方法開始

// vue/src/core/instance/state.js
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // 創建空對象,綁定到 vm._computedWatchers 上
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // computed 如果是函數就當成 getter,如果是對象則取 get 方法
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // getter 不存在時,輸出警告
    ...

    if (!isSSR) {
      // 爲 computed 的每個屬性創建 Watcher
      // Watcher 是引用變量,vm._computedWatchers 也會被修改
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 此時 key 還沒掛載到 vm
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // key 在 data 或者 props 存在,輸出警告
    }
  }
}

initComputed 會給 computed 的每個屬性創建 Watcher(服務端渲染不會創建 Watcher), 然後調用 defineComputed。先看看 new Watcher 的構造函數做了什麼

// vue/sct/core/observe/watcher.js
constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  this.vm = vm
  if (isRenderWatcher) {
    // 渲染 Watcher
    vm._watcher = this
  }
  vm._watchers.push(this)
  // options
  if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
    this.before = options.before
  } else {
    this.deep = this.user = this.lazy = this.sync = false
  }
  this.cb = cb
  this.id = ++uid // uid for batching
  this.dirty = this.lazy // for lazy watchers
  // 還有其他屬性的賦值
  ...

  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    // 解析表達式,得到 getter 函數
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
      this.getter = noop
      // getter 爲空時,輸出警告
      ...
    }
  }

  // lazy 爲 true 時,將 value 賦值爲 undefined,否則調用 get 函數計算 value
  this.value = this.lazy
    ? undefined
    : this.get()
}

看看 defineComputed 傳了哪些參數給這個構造函數。

const computedWatcherOptions = { lazy: true }
...
  watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
  )
...

可以從上面看到 computed 屬性創建 Watcher 時,lazytrue,也就是在 computed 中聲明瞭屬性也不使用,那麼將不會計算該屬性的結果,value 爲 undefined。

順便看下 Watcher 的 get 方法

// vue/sct/core/observe/watcher.js
get () {
  // 將該 Watcher push 到 Dep 的 targetStack 中,開啓依賴收集的模式
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 執行 computed 中的 get 函數
    // 如果函數內使用了 data 中的屬性,那麼就會觸發 defineProperty 中 get
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    // 完成依賴收集
    popTarget()
    this.cleanupDeps()
  }
  return value
}

這裏就可以看到在調用 get 函數時,會將當前的 Watcher 指定爲 Dep.target,然後開始執行 computed 屬性的 get 函數,如果 computed 屬性的 get 函數內使用了 data 中的屬性,那麼就會觸發 defineProperty 中的 getter。這就驗證了開頭說的第二點:只有在響應式依賴發生改變時纔會重新計算結果。

// vue/src/core/observer/index.js
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // 這個時候 target 爲 computed 屬性的 Watcher,然後將 data 屬性的 dep 收集到 computed 屬性的 Watcher 中
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },
  set: {
    ...
    // data 的屬性發生變化,通知訂閱者進行更新
    dep.notify()
  }
})

從這裏可以看出 Vue 設計的非常巧妙,通過執行 computed 屬性的 get 函數,就可以完成所有依賴的收集,當這些依賴發生變化時,又會通知 computed 屬性的 Watcher 進行更新。

接着看回 defineComputed

// vue/src/core/instance/state.js
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 客戶端渲染時,shouldCache 爲 true,也就是對計算結果進行緩存。
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    // 開發環境 computed 屬性的 set 函數爲空函數時,替換爲輸出警告的函數
    ...
  }
  // 將 computed 的屬性掛載到 vm 上,這樣就可以用 this.key 調用 computed 的屬性
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

從這裏可以看到,當對計算結果需要緩存,則會調用 createComputedGetter,如果計算結果不需要緩存,則會調用 createGetterInvoker

官方彩蛋

從這裏還可以看到一個可以在開發時的小技巧,當 computed 的屬性爲對象時,還可以自定義是否需要緩存。

官方文檔好像沒提到這一點,可能是覺得不緩存就和 methods 一樣,就沒有提到,這可能就是彩蛋吧。

computed: {
  noCacheDemo: {
    get () { ... },
    set () { ... },
    cache: false
  }
}

回到正題,看看 createComputedGetter 做了什麼。

// vue/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // watcher 爲 initComputed 中創建的 watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // watcher 初始化時,dirty 的值與 lazy 相同,都爲 true
      // 那麼第一次獲取 computed 屬性的值將會執行 watcher.evaluate()
      // evaluate 中會將 dirty 置爲 false
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 如果處於收集依賴的模式,調用 watcher 的 depend 進行依賴收集
      if (Dep.target) {
        watcher.depend()
      }
      // 返回 watcher.value,而不是執行 computed 屬性的 get 函數計算結果
      return watcher.value
    }
  }
}

再看下 watcher 的 evaluate 函數

// vue/sct/core/observe/watcher.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}

這裏可以看到,如果 computed 的計算結果需要緩存時,在第一次使用 computed 屬性時會執行 watcher 的 get 函數,在執行 computed 屬性的函數的過程中完成依賴的收集,並將計算結果賦值給 watcher的 value 屬性。

之後再調用 computed 的屬性則會取 watcher.value 的值,而不用執行 computed 屬性的 get 函數,就這樣做到了緩存的效果。也就驗證了開頭提到的第一點:computed 的計算結果會進行緩存。

最後再看看不使用緩存時的做法,createGetterInvoker 函數

// vue/sct/core/instance/state.js
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

其實做法非常簡單,就是每次調用就執行 computed 屬性的 get 函數。

總結

總結一下 computed 的實現過程,主要有以下幾個方面:

1、給 computed 的每個屬性創建 Watcher

2、第一個使用 computed 的屬性時,將會執行該屬性的 get 函數,並完成依賴收集,完後將結果保存在對應 Watcher 的 value 中,對計算結果進行緩存。

3、當依賴發生變化時,Dep 會發布通知,讓訂閱的 Watcher 進行更新的操作。

最後感謝各位小夥伴看到這裏,Vue computed 的實現過程都過了一遍,希望能夠對各位小夥伴有所幫助。

如果有講的不對的地方,可以評論指出哦。如果還有不瞭解的地方,歡迎關注我的公衆號給我留言哦。


如果你喜歡我的文章,希望可以關注我的公衆號【前端develop】

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