この記事は https://qiita.com/sin_tanaka/items/64b4a48bcb6dac924380 にも紐付いています。

前置き

Vue.jsにはVueインスタンス(コンポーネント)上のデータの変更を監視する watch というプロパティがあります。

さらに、 watch には immediate というオプションがあります。

watch は通常監視を始めて、データが変わった直後にコールバックが呼ばれますが、 immediate オプションを付与した watch は監視を始めた直後に一回コールバックが呼ばれます。

また、Vueにはインスタンスのライフサイクルに合わせて関数を実行する ライフサイクルフック という仕組みがあります。

そこで、 immediateオプション付きのwatchはライフサイクルにおけるどのタイミングで呼ばれるのか? という疑問が湧いたので調べてみました。

ライフサイクルフック

画像は Vue インスタンス — Vue.js より引用

TL;DR

  • watchのコードリーティングしてみた
  • 実行タイミングは beforeCreatecreated のあいだ
    • ドキュメントには記載無さそう?現状のコードではこのタイミングってぐらいなはず

ひとまず実行してみる

以下のコードで試してみたところ、immediate 付きの watchbeforeCreatecreated の間に実行されました。

https://codepen.io/sin-tanaka/pen/ExaxrZx

new Vue({
  el: '#app',
  data: function() {
    return {
      helloWorld: 'HelloWorld'
    };
  },
  beforeCreate: function() {
    console.log('call beforeCreate');
  },
  created: function() {
    console.log('call created');
  },
  mounted: function() {
    console.log('call mounted');
  },
  watch: {
    helloWorld: {
      handler: v => console.log('call watch', v),
      immediate: true
    }
  }
})
call beforeCreate
call watch HelloWorld
call created
call mounted

ライフサイクルの図で言うところの Init Injections & Reactivity でWatchを仕掛けているようです。

Vuejsのコードを読んでみる

改めてwatchのドキュメントを読んでみましたが、上記の動作を保証するような文言はなさそうでした。

そこで、2019.12.3時点でのdevブランチのコードを読んでみることにします

vuejs/vue: 🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

new Vueは何をしているのか?

new Vue したときにどのようなコードを実行しているのかを追ってみます package.jsonscriptsrollupruntime →…のように追っていくと new Vue の実態は以下のようでした。

https://github.com/vuejs/vue/blob/dev/src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

function Vue new演算子付きで呼ばれたときのみ、 this._init(option) を実行しています。実質コンストラクタですね。

_init は Vueの中に定義されていないので、 initMixin stateMixin eventsMixin lifecycleMixin renderMixin 辺りで Vue.prototype._init を仕掛けているとみます。

initMixin

initMixin を読んでみると Vue.prototype._init = function (options?: Object) という記述がありました。この関数でコンストラクタに該当するコードを仕込んでいるようです。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    /**
    * 中略
    */
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    /**
    * 中略
    */
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

また、 callHook(vm, 'beforeCreate')callHook(vm, 'created') の関数呼び出しの行があります。これは名前の通り、ライフサイクルフックを実行している関数でした。

その間には initInjections(vm) initState(vm) initProvide(vm) という関数呼び出しがありました。 これはまさしくライフサイクルの図でいうところの Init Injections & Reactivity に該当する関数に見えます。

initState

initState 関数を見てみると initWatch 関数を実行している行がありました。

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

さらに 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)
    }
  }
}

まだ immediate の記述はナシ createWatcher を追います

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

vm.$watch を実行しているようなので、これも _init のように prototype に仕掛けている箇所を探してみます

vm.$watchはどこで仕掛けているのか?

index.js で実行している stateMixin で仕掛けているようでした

export function stateMixin (Vue: Class<Component>) {
  /**
  * 中略
  */
  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()
    }
  }
}

ここで watch のオプション immediate がtruthyであるとき cb.call(vm, watcher.value) を実行しているのが分かります。長かった・・・

よって、コードベースでも見ても immediate: truewatch の実行タイミングは beforeCreatecreated の間 であることが分かりました

おまけ

コードリーティングしてたら $watch が何やら unwatchFn なるfunctionを返しているのを発見しました。 名前のとおりですが、 $watch の戻り値をコールすると監視が解除されます。

https://codepen.io/sin-tanaka/pen/oNgNmGz