vue3 源码之生命周期钩子

生命周期钩子对应的生命周期Option如下:

beforeCreate -> 使用 setup() 
created -> 使用 use setup() 
beforeMount -> onBeforeMount 
mounted -> onMounted 
beforeUpdate -> onBeforeUpdate 
updated -> onUpdated 
beforeDestroy-> onBeforeUnmount 
destroyed -> onUnmounted 
activated -> onActivated 
deactivated -> onDeactivated 
errorCaptured -> onErrorCaptured

除此之外, 还新增了两个用于调试的生命周期:

  • onRenderTracked
  • onRenderTriggered

注册钩子函数

export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
export type DebuggerHook = (e: DebuggerEvent) => void
export const onRenderTriggered = createHook<DebuggerHook>(LifecycleHooks.RENDER_TRIGGERED)
export const onRenderTracked = createHook<DebuggerHook>(LifecycleHooks.RENDER_TRACKED)

createHook的实现如下:

const createHook = function(lifecycle)  { 
  return function (hook, target = currentInstance) { 
    injectHook(lifecycle, hook, target) 
  } 
}

 

其内部的injectHook实现如下:

function injectHook(type, hook, target = currentInstance, prepend = false) { 
  const hooks = target[type] || (target[type] = []) 
  // 封装 hook 钩子函数并缓存 
  const wrappedHook = hook.__weh || 
    (hook.__weh = (...args) => { 
      if (target.isUnmounted) { 
        return 
      } 
      // 停止依赖收集 
      pauseTracking() 
      // 设置 target 为当前运行的组件实例 
      setCurrentInstance(target) 
      // 执行钩子函数 
      const res = callWithAsyncErrorHandling(hook, target, type, args) 
      setCurrentInstance(null) 
      // 恢复依赖收集 
      resetTracking() 
      return res 
    }) 
  if (prepend) { 
    hooks.unshift(wrappedHook) 
  } 
  else { 
    hooks.push(wrappedHook) 
  } 
}

该函数主要是对用户注册的钩子函数hook做了一层封装, 然后添加到一个数组中, 把数组保存在当前组件实例的target上, 这里, key是用来区分钩子函数的字符串. 比如, onMounted注册的钩子函数在组件实例上就是通过instance.m来保存的.

组件实例在后续会通过不同的字符串key找到对应的钩子函数数组并执行.

对于相同的钩子函数, 会把封装的wrappedHook钩子函数缓存到hook.__weh中, 这样后续通过scheduler方式执行的钩子函数就会被去重.

后续执行wrappedHook函数时, 会先停止依赖手机, 因为钩子函数内部访问的响应式DJUI想爱你个, 通常都已经执行过依赖手机, 所以钩子函数执行的时候就没有必要再次进行.

接着是设置 target 为当前组件实例.

在接下来通过callWithAsyncErrorHandling方法执行注册的hook钩子函数, 执行完毕后设置当前运行组件实例为null, 并恢复依赖收集

 

onBeforeMount 和 onMounted

onBeforeMount注册的beforeMount钩子函数会在组件挂载之前执行

onMounted注册的mounted钩子会在组件挂载之后执行

我们回头看看组件渲染函数中关于组件挂载部分的实现:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { 
  // 创建响应式的副作用渲染函数 
  instance.update = effect(function componentEffect() { 
    if (!instance.isMounted) { 
      // 获取组件实例上通过 onBeforeMount 钩子函数和 onMounted 注册的钩子函数 
      const { bm, m } = instance; 
      // 渲染组件生成子树 vnode 
      const subTree = (instance.subTree = renderComponentRoot(instance)) 
      // 执行 beforemount 钩子函数 
      if (bm) { 
        invokeArrayFns(bm) 
      } 
      // 把子树 vnode 挂载到 container 中 
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) 
      // 保留渲染生成的子树根 DOM 节点 
      initialVNode.el = subTree.el 
      // 执行 mounted 钩子函数 
      if (m) { 
        queuePostRenderEffect(m, parentSuspense) 
      } 
      instance.isMounted = true 
    } 
    else { 
      // 更新组件 
    } 
  }, prodEffectOptions) 
}

 

在执行patch挂载组件之前, 会检测组件实例上是否有注册的beforeMonut钩子函数bm, 如果有则通过invokeArrayFns函数执行它, 挂载之后, 则检查是否有函数m, 然后通过queuePostRenderEffectmounted函数推入postFlushCbs, 然后在整个树render以后, 同步执行flushPostFlushCbs函数调用mounted钩子函数

对于嵌套组件, 组件会先执行父组件的beforeMount, 然后子组件的beforeMount, 然后子组件的mounted, 最后是父组件的mounted.

onBeforeUpdate 和 onUpdated

onBeforeUpdate注册的beforeUpdate会在组件更新之前执行, onUpdated会在组件更新之后执行.

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { 
  // 创建响应式的副作用渲染函数 
  instance.update = effect(function componentEffect() { 
    if (!instance.isMounted) { 
      // 渲染组件 
    } 
    else { 
      // 更新组件 
      // 获取组件实例上通过 onBeforeUpdate 钩子函数和 onUpdated 注册的钩子函数 
      let { next, vnode, bu, u } = instance 
      // next 表示新的组件 vnode 
      if (next) { 
        // 更新组件 vnode 节点信息 
        updateComponentPreRender(instance, next, optimized) 
      } 
      else { 
        next = vnode 
      } 
      // 渲染新的子树 vnode 
      const nextTree = renderComponentRoot(instance) 
      // 缓存旧的子树 vnode 
      const prevTree = instance.subTree 
      // 更新子树 vnode 
      instance.subTree = nextTree 
      // 执行 beforeUpdate 钩子函数 
      if (bu) { 
        invokeArrayFns(bu) 
      } 
      // 组件更新核心逻辑,根据新旧子树 vnode 做 patch 
      patch(prevTree, nextTree, 
 // 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点 
        hostParentNode(prevTree.el), 
   // 缓存更新后的 DOM 节点 
        getNextHostNode(prevTree), 
        instance, 
        parentSuspense, 
        isSVG) 
      // 缓存更新后的 DOM 节点 
      next.el = nextTree.el 
      // 执行 updated 钩子函数 
      if (u) { 
        queuePostRenderEffect(u, parentSuspense) 
      } 
    } 
  }, prodEffectOptions) 
}

 

不要在 updated 钩子函数中更改数据,因为这样会再次触发组件更新,导致无限递归更新

父组件的更新不一定会导致子组件的更新,因为 Vue.js 的更新粒度是组件级别的

onBeforeUnmount 和 onUnmounted

onBeforeUnmount 注册的 beforeUnMount 钩子函数会在组件销毁之前执行

onUnmounted 注册的 unmounted 钩子函数会在组件销毁之后执行

const unmountComponent = (instance, parentSuspense, doRemove) => { 
  const { bum, effects, update, subTree, um } = instance 
  // 执行 beforeUnmount 钩子函数 
  if (bum) { 
    invokeArrayFns(bum) 
  } 
  // 清理组件引用的 effects 副作用函数 
  if (effects) { 
    for (let i = 0; i < effects.length; i++) { 
      stop(effects[i]) 
    } 
  } 
  // 如果一个异步组件在加载前就销毁了,则不会注册副作用渲染函数 
  if (update) { 
    stop(update) 
    // 调用 unmount 销毁子树 
    unmount(subTree, instance, parentSuspense, doRemove) 
  } 
  // 执行 unmounted 钩子函数 
  if (um) { 
    queuePostRenderEffect(um, parentSuspense) 
  } 
}

组件销毁的逻辑比较简单, 就是清理组件实例上绑定的effect副作用函数和注册的副作用渲染函数update以及调用unmount销毁子树.

#onErrorCaptured

组件销毁的逻辑比较简单, 就是清理组件实例上绑定的effect副作用函数和注册的副作用渲染函数update以及调用unmount销毁子树.

我们在前面多次看到过一个函数callWithErrorHandling, 他用于执行一段函数, 并通过handleError处理错误. handleError的实现如下:

function handleError(err, instance, type) { 
  const contextVNode = instance ? instance.vnode : null 
  if (instance) { 
    let cur = instance.parent 
    // 为了兼容 2.x 版本,暴露组件实例给钩子函数 
    const exposedInstance = instance.proxy 
    // 获取错误信息 
    const errorInfo = (process.env.NODE_ENV !== 'production') ? ErrorTypeStrings[type] : type 
    // 尝试向上查找所有父组件,执行 errorCaptured 钩子函数 
    while (cur) { 
      const errorCapturedHooks = cur.ec 
      if (errorCapturedHooks) { 
        for (let i = 0; i < errorCapturedHooks.length; i++) { 
          // 如果执行的 errorCaptured 钩子函数并返回 true,则停止向上查找。、 
          if (errorCapturedHooks[i](err, exposedInstance, errorInfo)) { 
            return 
          } 
        } 
      } 
      cur = cur.parent 
    } 
  } 
  // 往控制台输出未处理的错误 
  logError(err, type, contextVNode) 
}

 

函数会从当前宝座的组件的父组件实例开始, 尝试去查找注册errorCaptured钩子函数, 如果有则遍历判断errorCaptured钩子函数是否为true, 如果是则说明这个错误已经正确处理, 否则会通过logError向控制台抛出错误.

所以errorCaptured本质上用于捕获来自子孙组件的错误, 返回true就可以阻止错误继续向上传播

 

函数会从当前宝座的组件的父组件实例开始, 尝试去查找注册errorCaptured钩子函数, 如果有则遍历判断errorCaptured钩子函数是否为true, 如果是则说明这个错误已经正确处理, 否则会通过logError向控制台抛出错误.

所以errorCaptured本质上用于捕获来自子孙组件的错误, 返回true就可以阻止错误继续向上传播

 

onRenderTracked 和 onRenderTriggered

 

onRenderTracked 和 onRenderTriggered 在开发阶段渲染调试用的

我们回顾一下创建副作用渲染函数的第二个参数, 在开发环境下他们是这样的:

 

onRenderTracked 状态跟踪

onRenderTracked直译过来就是状态跟踪,它会跟踪页面上所有响应式变量和方法的状态,也就是我们用return返回去的值,它都会跟踪。只要页面有update的情况,它就会跟踪,然后生成一个event对象,我们通过event对象来查找程序的问题所在。

使用onRenderTracked同样要使用import进行引入。

import { .... ,onRenderTracked,} from "vue";

引用后就可以在setup()函数中进行引用了。

onRenderTracked((event) => {
console.log("状态跟踪组件----------->");
console.log(event);
});

写完后可以到终端中启动测试服务npm run serve,然后看一下效果,在组件没有更新的时候onRenderTracked是不会执行的,组件更新时,它会跟组里边每个值和方法的变化。

 

onRenderTriggered 状态触发

onRenderTriggered直译过来是状态触发,它不会跟踪每一个值,而是给你变化值的信息,并且新值和旧值都会给你明确的展示出来。

如果把onRenderTracked比喻成散弹枪,每个值都进行跟踪,那onRenderTriggered就是狙击枪,只精确跟踪发生变化的值,进行针对性调试。

使用它同样要先用import进行引入

import { .... ,onRenderTriggered,} from "vue";

在使用onRenderTriggered前,记得注释相应的onRenderTracked代码,这样看起来会直观很多。 然后把onRenderTriggered()函数,写在setup()函数里边。

onRenderTriggered((event) => {
console.log("状态触发组件--------------->");
console.log(event);
});

对 event 对象属性的详细介绍:

- key 那边变量发生了变化
- newValue 更新后变量的值
- oldValue 更新前变量的值
- target 目前页面中的响应变量和函数

通过这些你能很好的对代码进行调试。这些调试用的钩子函数,如果你能正确合理的使用,是真的可以快速解决问题的。