首頁 > 軟體

vue中created、watch和computed的執行順序詳解

2022-11-27 14:00:57

 前言

面試題:vuecreatedwatch(immediate: true)computed的執行順序是啥?

先看個簡單的例子:

// main.js
import Vue from "vue";

new Vue({
  el: "#app",
  template: `<div>
    <div>{{computedCount}}</div>
  </div>`,
  data() {
    return {
      count: 1,
    }
  },
  watch: {
    count: {
      handler() {
        console.log('watch');
      },
      immediate: true,
    }
  },
  computed: {
    computedCount() {
      console.log('computed');
      return this.count + 1;
    }
  },
  created() {
    console.log('created');
  },
});

當前例子的執行順序為:watch --> created --> computed

為什麼?

new Vue的範例化過程中,會執行初始化方法this._init,其中有程式碼:

Vue.prototype._init = function (options) {
    // ...
    initState(vm);
    // ...
    callHook(vm, 'created');
    // ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
}

function initState (vm) {
  vm._watchers = [];
  var 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(vm, opts.computed),然後執行initWatch(vm, opts.watch),再執行callHook(vm, 'created'),那為啥不是computed --> watch --> created呢?

不要著急,聽我娓娓道來。

1、關於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.
      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') {
        // ...
    }
  }
}

在通過initComputed初始化計算屬性的時候,通過遍歷的方式去處理當前元件中的computed。首先,在進行計算屬性範例化的時候,將{ lazy: true }作為引數傳入,並且範例化的Watcher中的getter就是當前例子中的computedCount函數;其次,通過defineComputed(vm, key, userDef)的方式在當前元件範例vm上為key進行userDef的處理。具體為:

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
  }
  // ...
  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
    }
  }
}

從以上可以看出,這裡通過Object.defineProperty(target, key, sharedPropertyDefinition)的方式,將函數computedGetter作為get函數,只有當對key進行存取的時候,才會觸發其內部的邏輯。內部邏輯watcher.evaluate()為:

evaluate () {
    this.value = this.get()
    this.dirty = false
}

get中有主要邏輯:

value = this.getter.call(vm, vm)

這裡的this.getter就是當前例子中的:

computedCount() {
  console.log('computed');
  return this.count + 1;
}

也就是說,只有當獲取computedCount的時候才會觸發computed的計算,也就是在進行vm.$mount(vm.$options.el)階段才會執行到console.log('computed')

2、關於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)
}

在通過initWatch初始化偵聽器的時候,如果watch為陣列,則遍歷執行createWatcher,否則直接執行createWatcher。如果handler是物件或者字串時,將其進行處理,最終作為引數傳入vm.$watch中去,具體為:

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()
    }
  }

這裡獲取到的options中會有immediate: true的鍵值,同時通過options.user = true設定usertrue,再將其作為引數傳入去進行Watcher的範例化。

當前例子中options.immediatetrue,所以會執行cb.call(vm, watcher.value),也就是以vm為主體,立刻執行cb。當前例子中cb就是handler

handler() {
    console.log('watch');
},

這裡就解釋了當前例子中console.log('watch')是最先執行的。

然後,執行完initComputedinitWatch以後,就會通過callHook(vm, 'created')執行到生命週期中的console.log('created')了。

最後通過vm.$mount(vm.$options.el)進行頁面渲染的時候,會先去建立vNode,這時就需要獲取到computedCount的值,進而觸發其get函數的後續邏輯,最終執行到console.log('computed')

附:為什麼vue中的watch在mounted之後執行

首先,在呼叫mounted的時候,會進入到defineReactive函數,然後呼叫函數裡面的set方法,將mounted中賦的新的值給傳遞過去,並通過呼叫dep.notify( )把訊息傳送給訂閱者,繼而更新訂閱者的列表

後面才開始渲染watch進入Watcher的class

總結

關於vuecreatedwatch的執行順序相對比較簡單,而其中computed是通過Object.defineProperty為當前vm進行定義,再到後續建立vNode階段才去觸發執行其get函數,最終執行到計算屬性computed對應的邏輯。

到此這篇關於vue中created、watch和computed執行順序詳解的文章就介紹到這了,更多相關vue created、watch和computed執行順序內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com