菜鳥初探Vue源碼(十)-- 計算屬性和偵聽屬性

在開發過程當中,咱們對這兩個屬性已經很是熟悉了:computedwatch。可是究其實現原理,或者說兩者到底有何區別,以及何時使用計算屬性,何時使用偵聽屬性,相信有很多朋友們仍存在疑惑。下面就一塊兒來探討一下:react

computed

關於計算屬性的使用,見以下代碼。express

export default {
    data() {
        return {
            msg : {a : 1},
            count : 1
        }
    }
    methods : {
        changeA() {
            this.count++;
        }
    }
    computed: {
        newMsg() {
            if (this.count < 3) return this.count;
            return 5;
        }
    }   
}
複製代碼

在初始化時(initState),判斷若是opts.computed存在(用戶定義了computed屬性),執行initComputed數組

const computedWatcherOptions = { lazy: true }
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 (!isSSR) {
      // create internal watcher for the computed property.
      // self-notes : it won't evaluate value immediately
      watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else {
      // throw a warn(The computed property id already defined in data / prop
    }
  }
}
複製代碼

initComputed中,首先獲取用戶定義的computed(能夠是函數,也能夠是對象)。若是不是服務端渲染,就調用new Watcher()實例化一個 Watcher(咱們稱之爲computed Watcher),實例化的具體過程咱們稍後討論。緊接着,判斷若是當前 vm 對象上找不到computed對象的 key 值,調用defineComputed,而該函數就完成了將 key 值變成了 vm 的屬性。函數

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼

除此以外,在該函數中還作了另外一件更重要的事情,設置了computed的 getter 和 setter,將createComputedGetter函數的返回值做爲 getter,而 setter 並不經常使用,能夠暫且不關注。oop

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
    }
  }
}
複製代碼

該函數返回一個函數,函數中進行了computed值的計算和依賴收集。那何時會觸發這個 getter 呢?一樣是在執行render時,會訪問到咱們定義的相關數據。以上就是計算屬性初始化的大體過程。ui

可是,若是順着以上思路走,在調試源碼時會發現一個問題,在走到if(!(key in vm))時,條件是不成立的,這樣就沒法執行defineComputed,還談何初始化。那這是爲何呢?其實,查找源碼會發現,在Vue.extend(也就是處理子組件的構造器時)中已經對計算屬性作了一些處理。下面一塊兒來看一下。this

Vue.extend = function (extendOptions: Object): Function {
    if (Sub.options.computed) {
        initComputed(Sub)
    }
}
function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}
複製代碼

所以總體邏輯是這樣的。在最外層組件的initState時,判斷opts.computedundefined,繼續走到Vue.extend(生成子組件的構造器),此時Sub.options.computed是存在的,調用initComputed(函數內調用defineComputed),設置計算屬性的 getter 爲createComputedGetter函數的返回值(是一個函數)。lua

當再次走到子組件的initState時,此時opts.computed是存在的,調用initComputed()(實例化computed Watcher,值爲undefined),此時if(!(key in vm))條件不成立,計算屬性完成的初始化。spa

因爲設置了 getter,在render過程當中訪問到計算屬性時,就會調用其 getter,在該函數中調用watcher.evaluate()計算出計算屬性的值【調用watcher.get() -> watcher.getter()(即用戶定義的計算屬性的 getter,在過程當中會訪問到計算屬性依賴的其餘數據(即執行數據的 getter,同時進行依賴收集),將訂閱該數據變化的computed Watcher存入new Dep().subs中)】,最後調用watcher.depend()(再次依賴收集,將render Watcher存入new Dep().subs中)。此時就完成了計算屬性的初次計算以及依賴收集。當咱們更改數據時,就進入了派發更新的過程。prototype

set: function reactiveSetter(newVal) {
    var value = getter ? getter.call(obj) : val;
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
        return
    }
    // #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();
}
複製代碼

當咱們更改計算屬性依賴的數據時,會觸發它的 setter,若是值發生變化,會調用dep.notify()。在該函數中,會遍歷依賴數組分別執行updatesubs中有兩個 Watcher(computed Watcherrender Watcher)),當執行到render Watcherupdate時,調用queueWatcher(在過程當中從新計算計算屬性的值,從新渲染頁面)。

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}
Watcher.prototype.update = function update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}
複製代碼

以上就是關於計算屬性的內容,接下來看偵聽屬性。

watch

關於偵聽屬性的使用,見以下代碼。

export default {
  data() {
    return {
      msg: { a: 1 },
      count: 1,
      nested: {
        a: {
          b: 1
        }
      }
    };
  },
  methods: {
    change() {
      this.count++;
      this.nested.a.b++;
    }
  },
  computed: {
    newMsg() {
      if (this.count < 4) {
        return this.count;
      }
      return 5;
    }
  },
  watch: {
    newMsg(newVal) {
      console.log("newMsg : " + newVal);
    },
    count: {
      immediate: true,
      handler(newVal) {
        console.log("count : " + newVal);
      }
    },
    nested: {
      deep: true,
      sync: true,
      handler(newVal) {
        console.log("nested : " + newVal.a.b);
      }
    }
  }
}
複製代碼

同計算屬性同樣,在initState中,判斷若是opts.watch存在,則調用initWatch對偵聽屬性進行初始化。

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
}
複製代碼

初始的流程也很是簡單,大體通過了initWatch -> createWatcher -> vm.$watch,在vm.$watch中,建立了user Watcher,且該函數返回一個函數,能夠用來銷燬user Watcher。接下來一塊兒探討user Watcher的建立過程。

// new Watcher(vm, expOrFn, cb, options)
export default class Watcher {
    constructor(vm, exporFn, cb, options){
        this.deep = !!options.deep
        this.user = !!options.user
        this.sync = !!options.sync
        this.getter = parsePath(exporFn)
        this.value = this.get()
    }
    get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
            value = this.getter.call(vm, vm)
        } finally {
            if (this.deep) {
                traverse(value)
            }
            popTarget()
            this.cleanupDeps()
        }
        return value
    }
}

export function parsePath (path: string): any {
    const segments = path.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            obj = obj[segments[i]]
        }
        return obj
    }
}
複製代碼

user Watcher的建立過程當中,因爲傳入的getter是一個字符串,須要通過parsePath函數處理,該函數返回一個函數,賦值給watcher.getter。與計算屬性不一樣,偵聽屬性會當即調用watcher.get()進行求值(過程當中執行watcher.getter會訪問到相關數據,觸發數據的 getter,進行依賴收集)。user Watcher建立完畢後,判斷options.immediate若是爲 true,則當即執行用戶定義的回調函數。 當數據發生更改時,調用dep.notify()進行派發更新,總體更新過程仍是相似以前,此處就再也不進行詳細解釋。 總結一下,計算屬性的本質是computed Watcher,偵聽屬性的本質是user Watcher,那究竟什麼時候使用計算屬性,什麼時候使用偵聽屬性呢?通常來講,計算屬性適合使用在模板渲染中,某個值是依賴了某些響應式對象而計算得來的;而偵聽屬性適用於觀測某個值的變化去完成一段邏輯。

相關文章
相關標籤/搜索