菜鸟初探Vue源码(六)-- 生命周期

312 阅读2分钟

对于 Vue 的生命周期,可以阅读如下流程图(图中不包含activateddeactivated,这两个生命周期与keepAlive相关,会在之后系列文章中介绍)。本篇会从源码的角度分析 Vue 的生命周期函数都在什么时间节点执行以及做了什么。

生命周期
Vue.js 定义了一个callHook专门用来执行生命周期函数,只需传入 Vue 实例和钩子函数名称即可。

export function callHook (vm: Component, hook: string) {
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) vm.$emit('hook:' + hook)
}

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
    let res = args ? handler.apply(context, args) : handler.call(context)
    // ...
    return res
}

了解callHook方法之后,我们从 init 开始探讨生命周期。

beforeCreate & created

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

    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}

在 init 中调用了两个生命周期函数beforeCreatecreated。可以看到最初对 events 进行了初始化(还包括 $attrs、$listeners等等),随后调用了beforeCreate,此时定义的数据还未初始化。紧接着对injections、data、props、provide进行了初始化,之后再调用created,此时就可以获取到数据了。接下来是挂载阶段。

beforeMount & mounted

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

  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

在调用mountComponent最初会执行beforeMount,那mounted在什么时间执行呢?有两处地方:(1)当前实例为根节点时,在mountComponent结束之前执行mounted。(2)在子组件patch结束之前会调用invokeInsertHook,遍历insertedVnodeQueue并执行insert钩子函数,而在insert钩子函数中调用了mounted

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
    createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
    )
    // ...
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
}
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])
        }
    }
}
function insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
        componentInstance._isMounted = true
        callHook(componentInstance, 'mounted')
    }
    // ...
}

insertedVnodeQueue又是什么鬼?它实际上是在 patch 过程中不断添加的一个队列,比如在createElm中,如果vnode.data.hook.insert存在,就将此vnode添加到队列中。另外在组件 patch 的createComponent中,会将vnode添加到队列中。因此我们可以知道,子组件的mounted生命周期函数要先于父组件执行。

function createElm (...) {
    // ...
    if (isDef(vnode.data)) invokeCreateHooks(vnode, insertedVnodeQueue)}
    // ...
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
    // ...
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        // ...
    }
}
function initComponent (vnode, insertedVnodeQueue) {
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
      registerRef(vnode)
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode)
    }
}

beforeUpdate & updated

接下来探讨beforeUpdateupdatedmountComponent执行时会执行new Watcher(),传入一个参数,定义的 before 函数中调用了beforeUpdate,那么 before 函数是什么时候调用呢?

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
}

observer/scheduler.js中在nextTick(之后会提到)调用之前会调用flushSchedulerQueue,在该函数中遍历渲染Watcher,调用 before 函数(相当于调用了beforeUpdate)。之后调用callUpdatedHooks函数(相当于调用了updated)。以上过程均发生在数据更新之后。

const queue: Array<Watcher> = []
function flushSchedulerQueue () {
  // ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
  }
  const updatedQueue = queue.slice()
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    // vm._watcher === watcher表示是渲染watcher
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

beforeDestroy & destroyed

最后还有beforeDestroydestroyed。在lifecycle.js中给 Vue 原型上定义了$destroy方法(该方法在 destroy 钩子函数中执行)

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // ...
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // ...
}

$destroy中首先调用了beforeDestroy,之后又做了解除父子关系、递归销毁子组件等事情,最后调用destroyed,组件销毁过程结束。