Vue Computed底層原理深入探究

今天面瞭傢小公司,上來直接問 computed 底層原理,面試官是這樣問的,data 中定義瞭 a 和 b 變量。computed 裡面定義瞭 c 屬性,c 的結果依賴與 a 和 b,模板中使用瞭變量 c。假設改變瞭 a,請問底層是如何收集依賴,如何觸發更新的?

<div>{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->{ c }}</div>
data(){
	return {
		a: 'foo',
		b: 'bar'
	}
},
computed: {
	c() { 
		return this.a + ' - ' + this.b;
	 }
},
mounted(){
	setTimeout(() => { this.a = 'FOO' }, 1000)
}

頁面效果:先顯示瞭 foo – bar,一秒之後顯示 FOO – bar

這裡查源碼後,對 computed 原理簡述如下:

initState 函數中初始化瞭 initComputed

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)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initComputed 函數中遍歷 computed 中每一個屬性,並且 new Watcher(computed watcher),可以看到傳入 Watcher 構造函數的 cb 是 noop,它是一個空函數。並且 defineComputed

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  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]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

defineComputed 中使用瞭 Object.defineProperty 對屬性進行劫持,獲取是返回對應的 getter 即 createComputedGetter

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  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) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

看到這裡,我們大致可以理解,Vue 組件在初始化時,initState -> initComputed -> new Watcher() 計算watcher,傳入的 callback 是 computer 屬性的 getter,由於 computed 是 lazy: true watcher,所以 new Watcher 時並不會立即執行 watcher 實例上的 get(), 而是在 defineComputed 函數裡面調用 createComputedGetter 函數,在 createComputedGetter 函數執行瞭 watcher.evaluate() 進而執行瞭 watcher.get().

執行 watcher.get() 首先 pushTarget(this),將 Dep.target 賦值為當前的 computed watcher,然後執行 this.getter

也就是執行 computer 屬性配置的 getter,執行getter 就會訪問所依賴的每一個值,就會被 Object.defineProperty 劫持到進入 get ,執行 dep.depend() , 會為每一個屬性對應的 dep 實例添加一個 computed watcher,同時這個 computed watcher 也會保存對應的 dep。

說瞭這麼多都在講 computed watcher,那修改 this.a 頁面為什麼會發生更新呢?

答案:因為 this.a 的依賴中不僅有 computed watcher 還有一個 render watcher

原因:

$mount 是會執行 mountComponent,會創建一個 render watcher,它會立即執行 cb(目前 Dep.target 是 render watcher),調用 render 函數在底層編譯模板,最後訪問屬性計算屬性 c,訪問計算屬性 c 就必定會訪問 a,當訪問 a 時會觸發 defineComputed 中的 Object.defineProperty 進而劫持調用 createComputedGetter,進而調用 watcher.depend(),這個 watcher 是 computed watcher

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
// Watcher.js
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

調用 watcher.depend() , this 指的是 computed watcher,會將 computed watcher 裡面的 deps 保存在所有依賴調用 deps[i].depend(),進而調用 Dep 類中的 Dep.target.addDep(this),使得 render watcher 中保存瞭當前的 dep,dep 中同時保存瞭 render watcher。

dep 中同時保存瞭 render watcher。就可以看出,示例中的屬性 a 的 dep 中也會保存 render watcher,所以 a 屬性的 dep 中有兩個 watcher: [computedWatcher, renderWatcher]

所以,修改 a 屬性的值,最後 notify 會清空這個 保存 watcher 的隊列,進行頁面更新!Okay!

到此這篇關於Vue Computed底層原理深入探究的文章就介紹到這瞭,更多相關Vue Computed內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: