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

总结

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

相关推荐
Angel_girl3196 分钟前
vue项目使用svg图标
前端·vue.js
難釋懷10 分钟前
vue 项目中常用的 2 个 Ajax 库
前端·vue.js·ajax
Qian Xiaoo12 分钟前
Ajax入门
前端·ajax·okhttp
爱生活的苏苏35 分钟前
vue生成二维码图片+文字说明
前端·vue.js
拉不动的猪37 分钟前
安卓和ios小程序开发中的兼容性问题举例
前端·javascript·面试
炫彩@之星43 分钟前
Chrome书签的导出与导入:步骤图
前端·chrome
贩卖纯净水.1 小时前
浏览器兼容-polyfill-本地服务-优化
开发语言·前端·javascript
前端百草阁1 小时前
从npm库 Vue 组件到独立SDK:打包与 CDN 引入的最佳实践
前端·vue.js·npm
夏日米米茶1 小时前
Windows系统下npm报错node-gyp configure got “gyp ERR“解决方法
前端·windows·npm
且白2 小时前
vsCode使用本地低版本node启动配置文件
前端·vue.js·vscode·编辑器