likes
comments
collection
share

Vue3.2 生命周期钩子函数简单分析

作者站长头像
站长
· 阅读数 13

Vue3.2 生命周期钩子函数简单分析

前言

大家好,我是咸鱼,今天的这篇文章让我们一起来探讨Vue3中生命周期函数的实现以及它是如何注入到Vue的执行流程中,在这篇文章中,可能会涉及到一些前面文章中的分析,如果没有看过前面的文章或者没有看过vue3源码的同学可以先去看看,好了那就让我们开始吧。

什么是生命周期钩子函数

在Vue3文档中是这样定义的:每一个Vue组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听、模板编译、挂载实例到DOM,以及数据改变时更新DOM,在此过程中,它也会允许被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。

在vue中,有很多个生命周期钩子函数,需要注意的是要区分Vue2和Vue3的生命周期钩子,vue2是使用选项的语法,vue3是使用hook的语法,并且vue3没有createdbeforeCreate(因为setup的执行的事件非常早),在组件卸载时,vue2执行的是beforeDestroydestroyed(需要开启兼容),vue3执行的是onBeforeUnmountonUnmount

vue2vue3
组件即将创建beforeCreate--
组件创建完成created--
组件即将挂载beforeMountonBeforeMount
组件挂载完成mountedonMounted
组件即将更新beforeUpdateonbeforeUpdate
组件更新完成updatedonUpdated
组件即将卸载beforeDestroyonBeforeUnmount
组件已经卸载destroyedonUnmount

除了上述的这些生命周期钩子,还有一些其他的生命周期钩子函数,比如内置组件KeepAliveonActivateonDeactivated(在vue2中是activateddeactivated选项),作用是DOM被移入或者移出时执行。剩下的生命周期钩子函数大家可以去文档中去查看。

生命周期钩子函数的实现

我们打开vue3源码的apiLifecycle.ts文件,在里面我们可以清楚看到vue3实现的各个生命周期函数。

Vue3.2 生命周期钩子函数简单分析

我们可以发现所有的onXXXX都是调用了createHook函数,这个函数它返回了一个函数,这个函数就是我们执行onXXX的时候所执行的函数。

Vue3.2 生命周期钩子函数简单分析

(伪代码)
(hook: T, target: ComponentInternalInstance | null = currentInstance) => {
    (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
    injectHook(lifecycle, hook, target)
}

这个函数有两个参数,第一个参数是需要在某一个特定时刻执行的函数hook,第二个参数文档中没有提及,我们在开发中也不会使用,它等同于this,也就是这个hook函数是给谁注册的,并且在执行时当前实例也会设置为它。

我们继续往后看,这个函数执行了一个injectHook函数,看函数的名字就知道它的作用,是将hook函数注入到实例中的。

注入的方式很简单,首先每一个生命周期钩子函数都是可以注册一到多个,所以它们在实例上都是以数组的形成保存,数组里面都是在某特定时刻需要执行的函数。

injectHook函数会先取出实例上对应的生命周期钩子函数的数组,如果没有默认给个空数组,包装hook函数,并且缓存,最后放入生命周期钩子函数数组中即可。包装器也会返回。

// (伪代码)
const hooks = target[type] || (target[type] = []) 
const wrappedHook =
  hook.__weh ||
  (hook.__weh = (...args: unknown[]) => {
    if (target.isUnmounted) {
      return
    }
    pauseTracking()
    setCurrentInstance(target)
    const res = callWithAsyncErrorHandling(hook, target, type, args)
    unsetCurrentInstance()
    resetTracking()
    return res
  })
hooks.push(wrappedHook)
return wrappedHook

经过上面的一系列步骤之后,在实例上的生命周期钩子函数数组中就会存在函数

Vue3.2 生命周期钩子函数简单分析

vue3的生命周期钩子函数是怎么和vue2的生命周期选项合在一起的

自Vue3推出后,vue2的生命周期函数选项得到了保留,虽然有那么两个被废弃,但是大部分依旧是可以运行,vue3的一定比vue2的会先执行,这是因为处理顺序,正常情况下,vue3的生命周期函数会在setup()中使用,而vue2的会在vue3的东西执行完毕后才会处理。

我们去看component.ts中的applyOptions函数,这个函数是用于处理vue2的内容,在里面我们看到一大堆的registerLifecycleHook,这就把vue2的生命周期函数选项注入到实例中,

Vue3.2 生命周期钩子函数简单分析

其实实现方法很简单,就是把写在vue2生命周期函数选项的函数拿到,然后通过vue3的方式去注入,我们来看registerLifecycleHook的实现。

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

registerLifecycleHook有两个参数,register拿到的是我们正常使用的vue3的生命周期钩子,hook就是写在生命周期选项中的函数,而把vue2的生命周期函数注入的方法就和我们自己手动调用vue3的生命周期钩子函数一样,唯一的区别在于需要更正函数的this指向,指向当前全局组件实例代理对象(publicThis是当前组件实例代理对象),而我们自己调用时当前全局组件实例代理对象就正确的目标对象。

特殊处理的生命周期钩子函数:activateddestroyed

activated当DOM是缓存树的一部分时,在移入时调用,destroyed当DOM是缓存树的一部分,在移出时调用。

在所有的生命周期钩子函数中,有两个生命周期钩子函数需要特殊处理,它们是vue内置组件KeepAlive的生命周期钩子函数。在正常情况下,和其他的生命周期钩子函数没有什么区别,但是在内置组件KeepAlive嵌套调用之后,一切就有所不同了。

我们都知道,在组件和其后代都注入生命周期钩子函数时,如果组件产生更新,组件的对应的生命周期函数会执行,并且,其后代对应的生命周期函数也有可能会执行,而KeepAlive的生命周期函数也不例外,当组件是缓存树的一部分,在移入和移出,如果其后代中存在KeepAlive,那么其生命周期函数也会执行。

那么这里便存在一个问题,我们要如何很好的知道一个KeepAlive中的ODM的后代中是否存在着KeepAlive,遍历DOM? 或者是在数组中去追踪后代?不不不,这些都是比较耗费性能。不如我们变化另外一种思路,在一个组件注入activateddestroyed时,从组件的父链往上找,vue中的做法就是这样。

我们找到源码中的KeepAlive.ts文件,可以看到onActivatedonDestroyed都是去调用了registerKeepAliveHook函数,我们直接去看这个函数的实现。

// (伪代码)
const wrappedHook =
    hook.__wdc ||
        (hook.__wdc = () => {
          // 仅当目标实例不在停用的分支中才触发钩子
          let current: ComponentInternalInstance | null = target
          // 跟着父链网上走,找外面的KeepAlive实例,如果实例上存在isDeactivated
          // 说明vdnoe已经被卸载,不需要执行一次onDeactivate钩子函数
          while (current) {
              if (current.isDeactivated) {
                  return
              }
              current = current.parent
          }
          // 执行钩子函数
          return hook()
    })
}
injectHook(type, wrappedHook, target)
if (target) {
    let current = target.parent
    while (current && current.parent) {
        if (isKeepAlive(current.parent.vnode)) {
            injectToKeepAliveRoot(wrappedHook, type, target, current)
        }
        current = current.parent
    }
}

它首先实现了一个分支停用检查包装器,并且缓存了这个包装器,方便后面调度正确的消除同一钩子函数的重复,接着就是正常的通过injectHook注入到目标实例中。

这最后一步就很关键,先拿到目标实例的parent(这里记作current),只有currentcurrentparent存在才会继续在父链上往上找,并且只有currentparentKeepAlive才会给current注入钩子,可能这里有点不好理解,我们来看一个结构。

<div>
    <KeepAlive> // 序号1
        <Child1 />
        <KeepAlive> // 序号2
            <div>
                <Child2 />
            </div>
        </KeepAlive>
    </KeepAlive>
</div>

我们给Comp2组件注册了一个activated钩子函数,先拿到current,也就是序号2的KeepAlivecurrentparent的序号1的KeepAlive,这样currentcurrent.parent存在与current.parentKeepAlive两个条件成立,开始执行injectToKeepAliveRoot函数。

下面我们就来看看injectToKeepAliveRoot的实现,看给KeepAliveRoot注入钩子函数和普通的注入钩子函数有啥区别,它的实现很简单,执行inject函数,拿到返回的warpperHook,然后立即给目标实例注入一个onUnmounted钩子函数,方便在目标实例卸载时把在KeepAliveRoot中的钩子函数数组中删除这个钩子函数。

function injectToKeepAliveRoot(hook, type, target, keepAliveRoot
) {
  const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
  onUnmounted(() => {
    remove(keepAliveRoot[type]!, injected)
  }, target)
}

其他的生命周期钩子函数

Vnode的生命周期钩子函数

在Vue源码中,有一种在文档中没有提及的生命周期钩子函数:Vnode生命周期函数vnode也是有类似组件的生命周期,Vue中也提供了对应的生命周期钩子函数,比如onVnodeBeforeMountonVnodeBeforeUnmount等等,但是没有created阶段的钩子函数。

有一点需要注意,vnode的生命周期钩子函数注册的方式必须要放到props中,比较常见的是组件给子组件去注册,又因为单向数据流,组件不能在自身注册,只能通过父组件注册。比如:<Comp onVnodeBeforeMount="fn" />

各个vnode生命周期钩子函数的执行时机如下:

什么时候会执行
onVnodeBeforeMount在vnode即将开始挂载时执行
onVnodeMounted在vnode挂载完成后执行
onVnodeBeforeUpdate在vnode即将开始更新时执行
onVnodeUpdated在vnode更新完毕后执行
onVnodeBeforeUnmount在vnode即将开始卸载时执行
onVnodeUnmountedvnode卸载完成后执行

只要是对vnode进行挂载、更新、卸载的操作,都会去触发这几个生命周期函数,比如KeepAlive内置组件,在移入的时候会触发onVnodeMounted、移出时会触发onVnodeUnmounted

自定义指令生命周期函数

Vue允许用户自己定义自定义指令(Custom Directives),并且自定义指令的对象中可以包含几个生命周期钩子选项,这几个生命钩子选项和组件的生命周期选项比较类似,只有执行的时机不一样,具体可以去看文档中的 指令钩子中有详细说明,

我们主要来看他是如何处理的,打开文件directives.ts,里面有两个函数:withDirectivesinvokeDirectiveHook,一个是在负责处理旧指令时把指令加入到vnode中,一个自定义指令生命周期钩子函数调用。

  • 将指令加入到vnode中

    这个没有什么好说的,就是单纯的解析指令的结构,然后将其加入到vnode的dir中

    // 伪代码
    function withDirectives(vnode, directives) {
      const internalInstance = currentRenderingInstance
      const instance = internalInstance.proxy
      // vnode中已经存在的指令
      const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
      for (let i = 0; i < directives.length; i++) {
        let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
        // 加入其中
        bindings.push({
          dir,
          instance,
          value,
          oldValue: void 0,
          arg,
          modifiers
        })
      }
      return vnode
    }
    
  • 自定义指令生命周期函数的执行

自定义指令生命周期函数的执行是通过一个函数去执行它,其实和事件调用者差不多,都是用一个函数去包裹要Fn,然后去执行这个函数,然后在这个函数内部做一些处理。

// 伪代码
export function invokeDirectiveHook(vnode, prevVNode, instance, name) {
  // 绑定的现指令
  const bindings = vnode.dirs!
  // 绑定的旧指令 会拿到之前的指令指定的value 会作为参数传递给指令函数
  const oldBindings = prevVNode && prevVNode.dirs!
  for (let i = 0; i < bindings.length; i++) {
    const binding = bindings[i]
    if (oldBindings) {
      // 保留指令的oldValue
      binding.oldValue = oldBindings[i].value
    }
    let hook = binding.dir[name] as DirectiveHook | DirectiveHook[] | undefined
    if (hook) {
      // 禁用生命周期钩子函数中的所有追踪
      // 因为它们可能会被称为内部效果
      pauseTracking()
      callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
        vnode.el,
        binding,
        vnode,
        prevVNode
      ])
      resetTracking()
    }
  }
}

生命周期函数和调用系统的关系

在生命周期钩子函数中,有一部分是可以直接执行的,但是有一部分是的生命周期钩子函数是需要加入到post队列中等待执行的。比如updated生命周期钩子函数,这个生命周期钩子函数是需要等待更新完毕之后再执行。

最后以上仅为个人的分析,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。