深入理解 Vue3 组件的实现原理:组件实例与组件的生命周期

Vue 组件的实例本质上是一个状态集合(或一个对象),它维护着组件运行过程中的所有信息,例如注册到组件的生命周期函数、组件渲染的子树(subTree)、组件是否已经被挂载、组件自身的状态(data),等等。

mountComponent 函数的作用是初始化组件实例,并将组件实例渲染到指定 DOM 元素中,同时处理生命周期钩子函数。

// packages/runtime-core/src/renderer.ts

export type MountComponentFn = (
  initialVNode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean
) => void

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  const compatMountInstance =
    __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component

  const instance: ComponentInternalInstance =
    compatMountInstance ||
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
}

本文所有 Vue.js 的源码均摘自 3.2.45 版本

mountComponent 函数各入参的含义:

  • initialVNode ,组件的初始虚拟节点。

  • container,挂载的容器元素。

  • anchor,挂载的位置。

  • parentComponent,父组件实例。

  • parentSuspense,父组件的 suspense 实例。

  • isSVG,是否是 SVG 元素。

  • optimized,是否是优化过的渲染

// packages/runtime-core/src/renderer.ts

const compatMountInstance =
  __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
  • __COMPAT__ ,rollup 构建的环境变量,true 为兼容 vue2 的构建

  • isCompatRoot ,是 Vue 虚拟节点的一个属性,用于是否做了兼容处理的判断。

  • component,也是 Vue 虚拟节点的一个属性之一,存储组件实例

深入理解 Vue3 组件的实现原理:组件实例与组件的生命周期

// packages/runtime-core/src/renderer.ts

const instance: ComponentInternalInstance =
  compatMountInstance ||
  (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ))

接口 ComponentInternalInstance 定义了 Vue 组件内部实例的数据结构,代表了组件的运行时实例。

// packages/runtime-core/src/component.ts
/**
* We expose a subset of properties on the internal instance as they are
* useful for advanced external libraries and tools.
*/
export interface ComponentInternalInstance {
uid: number
type: ConcreteComponent
parent: ComponentInternalInstance | null
root: ComponentInternalInstance
appContext: AppContext
/**
* Vnode representing this component in its parent's vdom tree
*/
vnode: VNode
/**
* The pending new vnode from parent updates
* @internal
*/
next: VNode | null
/**
* Root vnode of this component's own vdom tree
*/
subTree: VNode
/**
* Render effect instance
*/
effect: ReactiveEffect
/**
* Bound effect runner to be passed to schedulers
*/
update: SchedulerJob
/**
* The render function that returns vdom tree.
* @internal
*/
render: InternalRenderFunction | null
/**
* SSR render function
* @internal
*/
ssrRender?: Function | null
/**
* Object containing values this component provides for its descendents
* @internal
*/
provides: Data
/**
* Tracking reactive effects (e.g. watchers) associated with this component
* so that they can be automatically stopped on component unmount
* @internal
*/
scope: EffectScope
/**
* cache for proxy access type to avoid hasOwnProperty calls
* @internal
*/
accessCache: Data | null
/**
* cache for render function values that rely on _ctx but won't need updates
* after initialized (e.g. inline handlers)
* @internal
*/
renderCache: (Function | VNode)[]
/**
* Resolved component registry, only for components with mixins or extends
* @internal
*/
components: Record<string, ConcreteComponent> | null
/**
* Resolved directive registry, only for components with mixins or extends
* @internal
*/
directives: Record<string, Directive> | null
/**
* Resolved filters registry, v2 compat only
* @internal
*/
filters?: Record<string, Function>
/**
* resolved props options
* @internal
*/
propsOptions: NormalizedPropsOptions
/**
* resolved emits options
* @internal
*/
emitsOptions: ObjectEmitsOptions | null
/**
* resolved inheritAttrs options
* @internal
*/
inheritAttrs?: boolean
/**
* is custom element?
* @internal
*/
isCE?: boolean
/**
* custom element specific HMR method
* @internal
*/
ceReload?: (newStyles?: string[]) => void
// the rest are only for stateful components ---------------------------------
// main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null
// exposed properties via expose()
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null
/**
* alternative proxy used only for runtime-compiled render functions using
* `with` block
* @internal
*/
withProxy: ComponentPublicInstance | null
/**
* This is the target for the public instance proxy. It also holds properties
* injected by user options (computed, methods etc.) and user-attached
* custom properties (via `this.x = ...`)
* @internal
*/
ctx: Data
// state
data: Data
props: Data
attrs: Data
slots: InternalSlots
refs: Data
emit: EmitFn
/**
* used for keeping track of .once event handlers on components
* @internal
*/
emitted: Record<string, boolean> | null
/**
* used for caching the value returned from props default factory functions to
* avoid unnecessary watcher trigger
* @internal
*/
propsDefaults: Data
/**
* setup related
* @internal
*/
setupState: Data
/**
* devtools access to additional info
* @internal
*/
devtoolsRawSetupState?: any
/**
* @internal
*/
setupContext: SetupContext | null
/**
* suspense related
* @internal
*/
suspense: SuspenseBoundary | null
/**
* suspense pending batch id
* @internal
*/
suspenseId: number
/**
* @internal
*/
asyncDep: Promise<any> | null
/**
* @internal
*/
asyncResolved: boolean
// lifecycle
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
/**
* @internal
*/
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.CREATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.MOUNTED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.UPDATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.UNMOUNTED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.ACTIVATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.DEACTIVATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
/**
* For caching bound $forceUpdate on public proxy access
* @internal
*/
f?: () => void
/**
* For caching bound $nextTick on public proxy access
* @internal
*/
n?: () => Promise<void>
/**
* `updateTeleportCssVars`
* For updating css vars on contained teleports
* @internal
*/
ut?: (vars?: Record<string, string>) => void
}

Vue 导出 ComponentInternalInstance 的定义主要是提供给外部工具库使用的。

  • uid,组件实例的唯一标识符

  • type,表示组件实例的具体类型,可以是一个选项对象(ComponentOptions)或一个函数(FunctionalComponent)。用于在内部实现代码中进行相应的处理和判断。

深入理解 Vue3 组件的实现原理:组件实例与组件的生命周期

  • parent,当前组件实例的父级组件实例,如果没有父级则为 null

  • root,根组件的实例

  • appContext,应用上下文对象,存储了 Vue 创建的应用的全局配置,比如注册到应用全局的 mixins 、components 、directives 等

深入理解 Vue3 组件的实现原理:组件实例与组件的生命周期

  • vnode,代表组件的虚拟 DOM 节点

  • next,父级更新中的待处理的新虚拟 DOM 节点

  • subTree,存储组件的渲染函数返回的虚拟 DOM,即组件的子树(subTree)。

  • effect,渲染副作用函数实例

  • update,绑定给调度器的更新任务

  • render,返回虚拟 DOM 树的渲染函数

  • ssrRender,服务器端渲染函数

  • provides,对子组件提供的值的对象

  • scope,捕获组件关联的响应式副作用(即计算属性和侦听器),从而在组件卸载的时候可以自动停止。effectScope()

  • accessCache,渲染代理属性访问缓存,避免频繁调用 hasOwnProperty 方法

  • renderCache,缓存依赖 _ctx 但初始化后不需要更新的渲染函数值(例如,内联 handlers(处理程序))

  • components ,解析到的通过 mixinsextends 注册的组件

  • directives,解析到的通过 mixinsextends 注册的指令

  • filters,解析到注册的过滤器,仅用于兼容 Vue2 ,Vue3 中已废弃过滤器

  • propsOptions,解析后的 props 选项

  • emitsOptions,解析后的 emits 选项

  • inheritAttrs,解析的 inheritAttrs 选项,即是否透传 Attributes 。可见 透传 Attributes

  • isCE,是否为自定义元素

  • ceReload,自定义元素的热更新(HRM)方法

剩下的属性只与有状态的组件相关

  • proxy,公共实例的主代理(即 this)

  • exposed,通过 expose() 方法公开的属性。可见 暴露公共属性Vue3中的expose函数

  • exposeProxyexpose() 方法导出的对象的代理对象。

    // packages/runtime-core/src/component.ts
    export function getExposeProxy(instance: ComponentInternalInstance) {
    if (instance.exposed) {
    return (
    instance.exposeProxy ||
    (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
    get(target, key: string) {
    if (key in target) {
    return target[key]
    } else if (key in publicPropertiesMap) {
    return publicPropertiesMap[key](instance)
    }
    },
    has(target, key: string) {
    return key in target || key in publicPropertiesMap
    }
    }))
    )
    }
    }
    

    exposeProxy 代理对象拦截了 expose 导出的对象的 get 操作与 has 操作。

  • withProxy,仅用于 runtime-compiled render 函数的代理对象

    // packages/runtime-core/src/componentRenderUtils.ts
    export function renderComponentRoot(
    instance: ComponentInternalInstance
    ): VNode {
    // 省略无关代码
    const {
    proxy,
    withProxy
    } = instance
    const proxyToUse = withProxy || proxy
    result = normalizeVNode(
    render!.call(
    proxyToUse
    )
    )
    }
    
  • ctx,组件实例的上下文对象。公共实例代理对象的 target 。它保存了用户注入的 computed 、methods 等选项对象和用户附加的自定义属性(可通过 this.x = ... 访问)

  • data ,组件内部的状态,即 data 函数返回的对象

  • props ,组件的 props 对象

  • attrs ,没有定义在 props 中的属性对象

  • slots,插槽

  • refs ,存储模板引用的数据

  • emit,用于触发组件实例上的自定义事件

  • emitted,用于追踪之前已经触发过的 .once 事件处理程序,以避免重复触发。

  • propsDefaults,用于缓存从props默认工厂函数返回的值,以避免不必要的观察者触发(watcher trigger)。

  • setupState,保存 setup 中的状态

  • devtoolsRawSetupState,用于让开发工具访问额外的信息,一般不需要关注。

  • setupContext,用于提供给 setup 函数的上下文对象,包含一些常用的属性和方法。

  • suspense,与组件的异步依赖、异步组件相关 。Suspense

  • suspenseId,当一个异步组件加载时,Vue3 会在组件实例的 suspenseId 属性上设置一个唯一的标识。

  • asyncDep,表示与当前组件相关的异步依赖

  • asyncResolved,表示当前组件的异步依赖是否已经解决。

下面的实例属性与生命周期相关


{
// 表示当前组件是否已经挂载
isMounted: boolean
// 表示当前组件是否已经卸载
isUnmounted: boolean
// 表示当前组件是否已经停用
isDeactivated: boolean
// 存储组件 beforeCreate 生命周期钩子函数
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
// 存储组件 created 生命周期钩子函数
[LifecycleHooks.CREATED]: LifecycleHook
// 存储组件 beforeMount 生命周期钩子函数
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
// 存储组件 mounted 生命周期钩子函数
[LifecycleHooks.MOUNTED]: LifecycleHook
// 存储组件 beforeUpdate 生命周期钩子函数
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
// 存储组件 updated 生命周期钩子函数
[LifecycleHooks.UPDATED]: LifecycleHook
// 存储组件 beforeUnmount 生命周期钩子函数
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
// 存储组件 unmounted 生命周期钩子函数
[LifecycleHooks.UNMOUNTED]: LifecycleHook
// 存储组件 renderTracked 生命周期钩子函数,仅在 DEV 模式下可用
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
// 存储组件 renderTriggered 生命周期钩子函数,仅在 DEV 模式下可用
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
// 存储组件 activated 生命周期钩子函数
[LifecycleHooks.ACTIVATED]: LifecycleHook
// 存储组件 deactivated 生命周期钩子函数
[LifecycleHooks.DEACTIVATED]: LifecycleHook
// 存储组件 errorCaptured 生命周期钩子函数
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
// 存储组件 serverPrefetch 生命周期钩子函数,仅在 SSR 情况下可使用
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
}
// 组件实例使用数组来存储组件生命周期钩子函数,
// 因为一个组件可以注册多个相同的生命周期钩子
type LifecycleHook<TFn = Function> = TFn[] | null
  • f,用于在公共代理对象缓存绑定的 $forceUpdate 方法。$forceUpdate 是一个用于强制组件重新渲染的方法,通过调用此方法可以跳过响应式系统的追踪机制直接更新组件。缓存 forceUpdate 方法可以提高性能,减少每次访问 forceUpdate 时的运行时开销。

  • n,用于在公共代理访问中缓存绑定的 $nextTick 方法。$nextTick 方法用于在下次 DOM 更新周期之后执行回调函数。同样,缓存 nextTick 方法可以提高性能,减少每次访问 nextTick 时的运行时开销

  • ut,用于更新包含在传送门(Teleport)中的 css 变量。传送门(Teleport)是 Vue 3 新引入的一个功能,它可以将组件的内容渲染到 DOM 树的不同位置。

注意,被标记为 @internal 的属性仅用于内部使用。

createComponentInstance 函数的逻辑很简单,就是初始化一个 Vue3 组件实例对象

// packages/runtime-core/src/component.ts
let uid = 0
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
// 省略其他代码
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
// 省略其他代码    
}
// 省略其他代码
return instance
}

在 mountComponent 函数中调用了 setupRenderEffect 函数。在 setupRenderEffect 函数中完成 beforeMount 、mounted、beforeUpdate、updated 生命周期钩子函数的执行。

// packages/runtime-core/src/renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
const { bm, m } = instance
// 执行 beforeMount 生命周期钩子函数
if (bm) {
invokeArrayFns(bm)
}
// 执行 mounted 生命周期钩子函数
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
} else {
let { bu, u } = instance
// 执行 beforeUpdate 生命周期钩子函数
if (bu) {
invokeArrayFns(bu)
}
// 执行更新操作
patch(prevTree, nextTree)
// 执行 updated 生命周期钩子函数
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
}
}
}
export const invokeArrayFns = (fns: Function[], arg?: any) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg)
}
}
export const queuePostRenderEffect = __FEATURE_SUSPENSE__
? queueEffectWithSuspense
: queuePostFlushCb

👆 为了帮助理解,setupRenderEffect 函数省略了一些无关代码

// packages/runtime-core/src/components/Suspense.ts
export 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)
}
}
// packages/runtime-core/src/scheduler.ts
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
)
) {
pendingPostFlushCbs.push(cb)
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
pendingPostFlushCbs.push(...cb)
}
queueFlush()
}

mounted 、updated 生命周期钩子函数会被 Vue3 放入后置(Post)任务队列中执行。有关 Vue3 任务队列更多详情可见笔者的另外一篇文章 深入源码,理解 Vue3 的调度系统与 nextTick

我们会从组件实例中取得注册到组件上的生命周期函数,然后在合适的时机调用它们,这其实就是组件生命周期的实现原理。同时,由于一个组件可能存在多个同样的组件生命周期钩子,比如用户多次注册了相同的生命周期钩子、来自 mixins 中的生命周期钩子函数,因此我们会在组件实例对象上,使用数组来保存用户注册的生命周期钩子函数。

总结

Vue 组件的实例维护了组件运行过程中需要的所有数据。在渲染副作用函数内,Vue 通过判断组件实例中的状态来决定是对组件执行全新的挂载操作还是更新操作

参考

  1. 《Vue.js 设计与实现》霍春阳·著

原文链接:https://juejin.cn/post/7337148150620061746 作者:云浪

(0)
上一篇 2024年2月20日 上午10:26
下一篇 2024年2月20日 上午10:36

相关推荐

发表回复

登录后才能评论