vue3生命周期顺序详解

前言

这是vue3系列源码的第十章,使用的vue3版本是3.4.15

背景

这篇文章来看一下vue3中生命周期的实现

前置

这里我们准备几个组件,能够更好的观察生命周期的作用实际。

js 复制代码
// app.vue

<template>
  <div>
    <HelloWorld v-if="show"/>
    <textVue v-else/>
  </div>
  <div>
    <button @click="check">点击</button>
  </div>
 </template>
 <script setup>
 import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated } from 'vue' 
 import HelloWorld from './HelloWorld.vue';
 import textVue from './text.vue';

 const show = ref(true)

 const check = () => {
  show.value = false
 }

 console.log('app setup')

 
 onBeforeMount(() => console.log('app beforeMount'))
 onMounted(() => console.log('app mounted'))
 onBeforeUpdate(() => console.log('app beforeUpdate'))
 onUpdated(() => console.log('app updated'))
 </script>
js 复制代码
// hellowWorld.vue

<template>
  <div>hellow word</div>
</template>
<script setup>
 import { onBeforeUnmount, onUnmounted } from 'vue' 

 console.log('hello setup')
 onBeforeUnmount(() => {
  console.log('hello beforeUnmount')
 })
 onUnmounted(() => console.log('hello unmounted'))

</script>
js 复制代码
// test.vue
<template>
  <div>text</div>
</template>

这里面我们通过v-if的切换,来实现组件的卸载。

setup

首先遇到的是setup的执行。

setupStatefulComponent 函数中,执行了setup函数。

那么也就是在这里,执行了第一个console.log('app setup')

beforeMount

接下来,在setupRenderEffect 函数中,定义了componentUpdateFn 函数。也就是在这个函数中,执行了beforeMount的内容。

js 复制代码
 const componentUpdateFn = () => {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance
        const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

        toggleRecurse(instance, false)
        // beforeMount hook
        if (bm) {
          invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if (
          !isAsyncWrapperVNode &&
          (vnodeHook = props && props.onVnodeBeforeMount)
        ) {
          invokeVNodeHook(vnodeHook, parent, initialVNode)
        }
        ...
   }

上面函数中的bm 就是指的beforeMounte,我们定义了这个钩子之后,就会调用。

js 复制代码
const invokeArrayFns = (fns: Function[], arg?: any) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg)
  }
}
js 复制代码
function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false,
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    // cache the error handling wrapper for injected hooks so the same hook
    // can be properly deduped by the scheduler. "__weh" stands for "with error
    // handling".
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return
        }
        // disable tracking inside all lifecycle hooks
        // since they can potentially be called inside effects.
        pauseTracking()
        // Set currentInstance during hook invocation.
        // This assumes the hook does not synchronously trigger other hooks, which
        // can only be false when the user does something really funky.
        const reset = setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        reset()
        resetTracking()
        return res
      })
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  } else if (__DEV__) {
   ...
  }
}

最终就是在injectHook函数中最终执行了我们传入的回调函数。

也就是第二个console.log('app beforeMount')

子组件的setup

接着,还是在componentUpdateFn 函数中,进行了对subTreepatch

js 复制代码
patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            namespace,
          )

那么也就是重复了父组件的上面的过程,也就是执行setupbeforeMountmount

这里打印了console.log('hello setup')

beforeCreate created

那么这里其实还有两个钩子,我们并没有用到。就是beforeCeatecreated钩子。

因为在setup 的写法里,并不支持这两个钩子,因为他们和setup的执行时机非常接近。

但是在选项式的写法里,还是可以定义这两个钩子,那么他们到底是在什么时候执行。

我们都知道setup 是在所有的生命周期之前执行的,那么其实,setup执行完毕后,就会去执行这两个钩子。

js 复制代码
handleSetupResult(instance, setupResult, isSSR)

我们在setupStatefulComponent 函数中执行了setup 函数后,会进入handleSetupResult函数中。

js 复制代码
function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean,
) {
finishComponentSetup(instance, isSSR)
}

最终会进入到finishComponentSetup函数中。

js 复制代码
function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean,
) {
 // support for 2.x options
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    const reset = setCurrentInstance(instance)
    pauseTracking()
    try {
      applyOptions(instance)
    } finally {
    ...
    }
  }
}

这里对选项式API的写法做了支持。

js 复制代码
function applyOptions(instance: ComponentInternalInstance) {
   if (options.beforeCreate) {
    callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
  }
  ...
   if (created) {
    callHook(created, instance, LifecycleHooks.CREATED)
  }
}

就是在applyOptions 函数中,先后执行了beforeCreatecreated

mounted

接下来,还是在componentUpdateFn 函数中,执行了mounted钩子中的回调函数。

js 复制代码
// mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }

这里的m ,代表的就是mounted

js 复制代码
function queueEffectWithSuspense(
  fn: Function | Function[],
  suspense: SuspenseBoundary | null,
): void {
  if (suspense && suspense.pendingBranch) {
    if (isArray(fn)) {
      suspense.effects.push(...fn)
    } else {
      suspense.effects.push(fn)
    }
  } else {
    queuePostFlushCb(fn)
  }
}

这里的mounted 中的回调函数会加入到post异步队列中。

js 复制代码
  const render: RootRenderFunction = (vnode, container, namespace) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(
        container._vnode || null,
        vnode,
        container,
        null,
        null,
        null,
        namespace,
      )
    }
    if (!isFlushing) {
      isFlushing = true
      flushPreFlushCbs()
      flushPostFlushCbs()
      isFlushing = false
    }
    container._vnode = vnode
  }

当组件patch 的流程走完了,也就是挂载完毕后,会在flushPostFlushCbs 函数中执行post 异步队列中的所有函数,也就是在这里,会执行console.log('app mounted')

所以这里也很容易想到一个面试题,那就是父子组件的这些钩子函数的执行顺序:

答案很明显了:

  • 父组件会先执行了setup beforeCreate created beforeMount
  • 然后进入subTreepatch,然后去执行子组件的setup beforeCreate created beforeMount
  • 子组件的patch过程结束后,会执行子组件的mounted
  • 最后才会执行父组件的mounted

那么渲染阶段的生命周期就是这么回事了,下面我们点击一下页面上的按钮,进入到组件的更新,和子组件的销毁过程。

beforeUpdate

再次进入app.vuesetupRenderEffect 函数中的componentUpdateFn函数中,只不过这次走的更新阶段的代码。

js 复制代码
 const componentUpdateFn = () => {
   if (!instance.isMounted) {
      ...
   else {
        let { next, bu, u, parent, vnode } = instance
        if (bu) {
          invokeArrayFns(bu)
        }
        ...
         if (u) {
          queuePostRenderEffect(u, parentSuspense)
        }
   }

这里的bu 就是beforeUpdate

就是在这里执行了console.log('app beforeUpdate')

beforeUnmount

接着就进入到了新旧元素的patch阶段。

js 复制代码
 patch(
         prevTree,
         nextTree,
         // parent may have changed if it's in a teleport
         hostParentNode(prevTree.el!)!,
         // anchor may have changed if it's in a fragment
         getNextHostNode(prevTree),
         instance,
         parentSuspense,
         namespace,
       )
js 复制代码
 const patch: PatchFn = (
   n1,
   n2,
   container,
   anchor = null,
   parentComponent = null,
   parentSuspense = null,
   namespace = undefined,
   slotScopeIds = null,
   optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
 ) => {
   if (n1 === n2) {
     return
   }

   // patching & not same type, unmount old tree
   if (n1 && !isSameVNodeType(n1, n2)) {
     anchor = getNextHostNode(n1)
     unmount(n1, parentComponent, parentSuspense, true)
     n1 = null
   }
   ...
 }

patch 里面,先执行了n1 也就是旧组件的unmount

js 复制代码
 const unmount: UnmountFn = (
   vnode,
   parentComponent,
   parentSuspense,
   doRemove = false,
   optimized = false,
 ) => {
  if (shapeFlag & ShapeFlags.COMPONENT) {
     unmountComponent(vnode.component!, parentSuspense, doRemove)
   }
 }

这里是对组件的卸载,调用了unmountComponent

js 复制代码
 const unmountComponent = (
   instance: ComponentInternalInstance,
   parentSuspense: SuspenseBoundary | null,
   doRemove?: boolean,
 ) => {
     const { bum, scope, update, subTree, um } = instance
     // beforeUnmount hook
    if (bum) {
     invokeArrayFns(bum)
   }
   ...
    // unmounted hook
   if (um) {
     queuePostRenderEffect(um, parentSuspense)
   }
 }

这里的bum 就是beforeUnmount ,也就是在这里执行了console.log('hello beforeUnmount')

unmounted

在上面的bum 执行后,就执行了queuePostRenderEffect , 这里的um 就是unmounted ,和mounted 一样,这里也是加入到post异步队列中,不是立即执行。

updated

那么当patch 过程结束了后,此时的页面上的元素已经发生了变化,我们再回到componentUpdateFn函数中看,最后执行了

js 复制代码
  // updated hook
        if (u) {
          queuePostRenderEffect(u, parentSuspense)
        }

这里还是一样,u 就是updated ,并且加入到post异步队列中。

最后,来到flushPostFlushCbs函数中,开始执行异步队列中的任务。

先执行了console.log('hello unmounted')

然后执行了console.log('app updated')

总结

那么这就是生命周期相关的内容,下面是实际跑出来的验证结果。

相关推荐
计算机学姐9 分钟前
基于微信小程序的调查问卷管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
学习使我快乐013 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19953 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈4 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水5 小时前
简洁之道 - React Hook Form
前端
正小安7 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch9 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光9 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   9 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发