vue实例在被创建之前都要经历一系列的初始化的流程,这里面包括设置数据监听,编译模板,挂载实例到dom,更新dom等。在这整个过程,我们需要在默写时候去进行一些自己的操作,所以vue就在这个过程中运行了一些函数,叫做生命周期钩子函数。
上面的流程图能大概描述vue生命周期的触发过程,结合上一篇做一个自己的vue中new vue的整个过程来看会更清晰。

触发生命周期钩子函数

大概的流程我们已经了解了,都有那些生命周期钩子函数,我们也早已经知道了,但是这些钩子函数是怎么触发的呢?这里我们在源码中可以看到,所有的生命周期函数的调用都是通过callHook方法来触发的,而这个方法定义在src/core/instance/lifecycle中:

  1. export function callHook (vm: Component, hook: string) {
  2. // #7573 disable dep collection when invoking lifecycle hooks
  3. pushTarget()
  4. const handlers = vm.$options[hook]
  5. if (handlers) {
  6. for (let i = 0, j = handlers.length; i < j; i++) {
  7. try {
  8. handlers[i].call(vm)
  9. } catch (e) {
  10. handleError(e, vm, `${hook} hook`)
  11. }
  12. }
  13. }
  14. if (vm._hasHookEvent) {
  15. vm.$emit('hook:' + hook)
  16. }
  17. popTarget()
  18. }

根据传入callHook的字符串hook,去拿到对应的回调函数数组,然后遍历执行。
了解了触发方式,下面就一个一个钩子的去看是在哪里触发的

beforeCreate & created

beforeCreated和created都是在实例化vue的阶段,在_init方法中执行的,定义在src/core/instance/init.js中

  1. Vue.prototype._init = function (options?: Object) {
  2. // ...
  3. initLifecycle(vm)
  4. initEvents(vm)
  5. initRender(vm)
  6. callHook(vm, 'beforeCreate')
  7. initInjections(vm) // resolve injections before data/props
  8. initState(vm)
  9. initProvide(vm) // resolve provide after data/props
  10. callHook(vm, 'created')
  11. // ...
  12. }

两个生命周期函数触发在initState的前后,而这个方法就是用来初始化props,data,methods
,watch, computed等属性,所以在beforeCreate中是不能获取到props,data等定义的数据的。
而且这两个钩子函数触发的时候,还没有渲染dom,所以在这两个钩子中,也是无法访问到dom的。根据这些特性,我们可以很清楚的知道在这两个钩子函数中适合做些什么。

beforeMount & mounted

beforeMount发生在dom挂载之前,她的调用实在mountComponent函数中,定义在src/core/instace/lifecycle.js中

  1. export function mountComponent (
  2. vm: Component,
  3. el: ?Element,
  4. hydrating?: boolean
  5. ): Component {
  6. vm.$el = el
  7. // ...
  8. callHook(vm, 'beforeMount')
  9. let updateComponent
  10. /* istanbul ignore if */
  11. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  12. updateComponent = () => {
  13. const name = vm._name
  14. const id = vm._uid
  15. const startTag = `vue-perf-start:${id}`
  16. const endTag = `vue-perf-end:${id}`
  17. mark(startTag)
  18. const vnode = vm._render()
  19. mark(endTag)
  20. measure(`vue ${name} render`, startTag, endTag)
  21. mark(startTag)
  22. vm._update(vnode, hydrating)
  23. mark(endTag)
  24. measure(`vue ${name} patch`, startTag, endTag)
  25. }
  26. } else {
  27. updateComponent = () => {
  28. vm._update(vm._render(), hydrating)
  29. }
  30. }
  31. // we set this to vm._watcher inside the watcher's constructor
  32. // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  33. // component's mounted hook), which relies on vm._watcher being already defined
  34. new Watcher(vm, updateComponent, noop, {
  35. before () {
  36. if (vm._isMounted) {
  37. callHook(vm, 'beforeUpdate')
  38. }
  39. }
  40. }, true /* isRenderWatcher */)
  41. hydrating = false
  42. // manually mounted instance, call mounted on self
  43. // mounted is called for render-created child components in its inserted hook
  44. if (vm.$vnode == null) {
  45. vm._isMounted = true
  46. callHook(vm, 'mounted')
  47. }
  48. return vm
  49. }

vm._render()函数用来渲染VNode,在这之前执行beforeMount,之后执行vm._update()把VNode渲染到真实dom上,然后执行mounted钩子函数。
这里有一个区别,如果vm.$node是null,表明不是一次组件的初始化,而是外部new Vue初始化过程,那对于组件,它的mounted在哪里触发呢?
在渲染倒真是的dom之后,会执行invokeInsertHook函数,在这个函数里面会把保存的钩子函数依次执行一遍,它定义在src/core/vdom/patch.js中

  1. function invokeInsertHook (vnode, queue, initial) {
  2. // delay insert hooks for component root nodes, invoke them after the
  3. // element is really inserted
  4. if (isTrue(initial) && isDef(vnode.parent)) {
  5. vnode.parent.data.pendingInsert = queue
  6. } else {
  7. for (let i = 0; i < queue.length; ++i) {
  8. queue[i].data.hook.insert(queue[i])
  9. }
  10. }
  11. }

该函数会执行insert这个钩子函数,对于组件来说,insert定义在src/core/vdom/create-component.js中的componentVNodeHooks中

  1. const componentVNodeHooks = {
  2. // ...
  3. insert (vnode: MountedComponentVNode) {
  4. const { context, componentInstance } = vnode
  5. if (!componentInstance._isMounted) {
  6. componentInstance._isMounted = true
  7. callHook(componentInstance, 'mounted')
  8. }
  9. // ...
  10. },
  11. }

每个子组件都是在这个钩子函数中执行mounted的,另外invokeInsertHook的添加顺序是先子后父,所有mounted的执行顺序是先父后子。

beforeUpdate & updated

beforeUpdate的执行实在渲染Watcher的before函数中

  1. export function mountComponent (
  2. vm: Component,
  3. el: ?Element,
  4. hydrating?: boolean
  5. ): Component {
  6. // ...
  7. // we set this to vm._watcher inside the watcher's constructor
  8. // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  9. // component's mounted hook), which relies on vm._watcher being already defined
  10. new Watcher(vm, updateComponent, noop, {
  11. before () {
  12. if (vm._isMounted) {
  13. callHook(vm, 'beforeUpdate')
  14. }
  15. }
  16. }, true /* isRenderWatcher */)
  17. // ...
  18. }

这里有个判断,只有组件已经mounted之后,才会调用这个钩子函数

updated的执行实在flushSchedulerQueue函数调用的时候,定义在src/core/observer/scheduler.js中

  1. function flushSchedulerQueue () {
  2. // ...
  3. // 获取到 updatedQueue
  4. callUpdatedHooks(updatedQueue)
  5. }
  6. function callUpdatedHooks (queue) {
  7. let i = queue.length
  8. while (i--) {
  9. const watcher = queue[i]
  10. const vm = watcher.vm
  11. if (vm._watcher === watcher && vm._isMounted) {
  12. callHook(vm, 'updated')
  13. }
  14. }
  15. }

callUpdatedHooks函数中,对Watcher数组进行遍历,满足了当前Watcher为vm._watcher以及mounted以及触发这两个条件,就会触发updated函数。