67
33

More than 3 years have passed since last update.

【Vuejs】watch immediate: trueはライフサイクルのどこで実行されるのか?

Last updated at Posted at 2019-12-03

この記事は、Vue #2 Advent Calendar 2019 の2日目の記事です(4日目に飛び入り)。

前置き

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

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

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

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

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

ライフサイクルフック

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

TL;DR

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

ひとまず実行してみる

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

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 の実態は以下のようでした。

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) という記述がありました。この関数でコンストラクタに該当するコードを仕込んでいるようです。

src/core/instance/init.js
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 関数を実行している行がありました。

src/core/instance/state.js
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 関数を追ってみます

src/core/instance/state.js
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 を追います

src/core/instance/state.js
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 で仕掛けているようでした

src/core/instance/state.js
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 の戻り値をコールすると監視が解除されます。

67
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67
33