生命周期钩子事件 hook events

背景

Vue 里有三种方式可以注册生命周期钩子函数,当发生到该生命周期时,钩子函数就会被调用:

  1. 在 Vue 组件选项对象里添加
  2. 父组件模板里调用子组件时,可以给子组件添加自定义的生命周期钩子事件
  3. 组件里通过vm.$on/$once('hook:xxx')注册生命周期钩子事件

我们一般最常用的是用第 1 种方式,比如:

<script>
export default {
    mounted() {
      console.log("mounted");
    }
};
</script>
1
2
3
4
5
6
7

第 2 种方式:

<template>
    <Child @hook:mounted="childMounted"/>
</template>

<script>
import Child from "./Child";

export default {
    components: { Child },
    methods: {
        childMounted() {
            console.log("Child was mounted");
        }
    }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

此时,若是子组件触发了mounted事件,则会调用父组件的childMounted事件。

这里需要注意的是,子组件标签上的hook:mounted事件,最终是挂载在子组件上的,类似于调用childVm.$on('hook:mounted'),详见: 事件监听器 - 添加自定义事件open in new window

第 3 种方式:

<script>
export default {
    mounted() {
        const picker = new Pickaday({
            // ...
        });

        // 或者使用 this.$on
        this.$once("hook:beforeDestroy", () => {
            picker.destroy();
        });
    }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如上所示,第 1 种和第 3 种方式是给组件自身注册生命周期函数,而第 2 种是给子组件注册生命周期函数。

而且,实际上第 2 种方式最终也是使用第 3 种方式的vm.$on/$once来注册事件的,而这是在模板的编译阶段完成的。

接下来,我们就以第 3 种方式为例,来看看源码里是如何实现生命周期钩子事件的。

生命周期钩子事件源码

当我们通过vm.$on注册事件时,如发现事件名称以hook:开头,则设置vm._hasHookEvent = true

// src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

而 Vue 内部在各个生命周期调用生命周期函数时,都是调用callHook函数,其中会判断vm._hasHookEvent的值并确定是否要vm.$emit对应的生命周期钩子事件。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // ...
    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')
    // ...
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

如此这般,我们就可以使用vm.$on给组件添加生命周期钩子事件啦。

参考