Vue源码解析之 生命周期

lxf2023-04-22 13:12:02

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 6 篇,关注专栏

前言

每个 Vue 实例在被创建之前都要经过一系列的初始化过程,同时该过程会运行一些生命周期的钩子函数,用于处理不同时期的逻辑。最终执行生命周期的钩子函数都会调用 callHook 方法,它定义在 src/core/instance/lifecycle.js,该方法根据传入的字符串 hook,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执行,执行的时候把 vm 作为函数执行的上下文。本文着重主要介绍 beforeCreatecreatedbeforeMountmountedbeforeUpdateupdated 钩子函数。

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

Vue 的生命周期如图所示:

Vue源码解析之 生命周期

beforeCreate & created

beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的,它的定义在 src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
  // ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm) // 初始化props、data、methods、watch、computed 等属性
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
  // ...
}

可以看出执行 beforeCreate 函数时,无法获取 datamethodscomputed 等属性和方法。而在执行 initState(vm) 时,才会初始化 datamethodspropswatchcomputed等。所以执行 created 钩子函数时,可以获取到 datamethods等。需要注意的是,这两个钩子函数执行时,并没有渲染DOM,所以也无法获取到 DOM

beforeMount & mounted

beforeMount 钩子函数发生在 mount,即 DOM 挂载之前,它是在 mountComponent 函数中执行,定义在 src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // ...
  callHook(vm, 'beforeMount') // beforeMount 钩子函数

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted') // mounted 钩子函数
  }
  return vm
}

可以看出,在执行 vm.render() 函数渲染 VNode 之前,执行 beforeMount 钩子函数。之后执行完 vm._update() 把 虚拟 VNode 渲染为真实 DOM 后,执行 mounted 钩子函数。此时,在执行 mounted 阶段,我们可以获取到真实 DOM。

组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook 函数,该函数会遍历执行 insertedVnodeQueue 中保存的钩子函数,它定义在 src/core/vdom/patch.js

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

该函数执行 insert 方法时,对于组件而言,insert 方法定义在 src/core/vdom/create-component.js 中的 componentVNodeHooks 中:

const componentVNodeHooks = {
  // ...
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    // ...
  },
}

可以看出,每个子组件都在 insert 函数中执行 mounted 钩子函数。而之前提到的 insertedVnodeQueue 添加顺序是先子后父,所以同步渲染子组件时,mounted 钩子函数执行顺序也是先子后父。

beforeUpdate & updated

beforeUpdateupdated 钩子函数发生在数据更新时,beforeUpdate 钩子函数发生在渲染 Watcher 的 before 函数中。根据判断,beforeUpdate 是在 mounted 之后执行。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      // 根据判断 mounted 之后执行
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
}

updated 钩子函数在 flushSchedulerQueue 函数中执行,它的定义在 src/core/observer/scheduler.js

function flushSchedulerQueue () {
  // ...
  // 获取到 updatedQueue
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    // 需满足 两个条件 才会执行 updated 钩子函数
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

总结

  • created 钩子函数可以访问到 data 数据,但无法获取到 DOM
  • mounted 钩子函数可以获取到 DOM
  • destroyed 钩子函数可以处理一些定时器销毁任务

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick