组件是对页面中内容的一种模块化封装,这篇博客讲解 Vue 中组件化的实现方式。
在讲解组件化的实现原理前,首先说明在 Vue 中,一个组件的本质到底是什么。Vue 中有两种形式的组件,分别是有状态组件和无状态组件,有状态组件的本质是一个对象字面量,无状态组件的本质是一个函数。也就是说,在 Vue 中,组件的本质是一个对象字面量或者一个函数,这里以有状态组件进行讲解。
VNode 的本质就是一个普通的对象字面量,只不过这个对象字面量能够很好的描述真实 DOM,通过 VNode 可以渲染出页面中真实的 DOM。
VNode 是通过组件的 render 函数创建出来的,我们平时在开发中,一般都是使用 template 字符串描述页面内容,这个模板字符串会被 Vue 的编译器编译成 render 函数,所以在 Vue 的运行时,用于描述组件渲染内容的是 render 函数。
下面展示一下 Vue3 中 VNode 的 TypeScript 的类型定义:
- export interface VNode<
- HostNode = RendererNode,
- HostElement = RendererElement,
- ExtraProps = { [key: string]: any }
- > {
- __v_isVNode: true
- [ReactiveFlags.SKIP]: true
- type: VNodeTypes
- props: (VNodeProps & ExtraProps) | null
- key: string | number | symbol | null
- ref: VNodeNormalizedRef | null
- scopeId: string | null
- slotScopeIds: string[] | null
- children: VNodeNormalizedChildren
- component: ComponentInternalInstance | null
- dirs: DirectiveBinding[] | null
- transition: TransitionHooks<HostElement> | null
- el: HostNode | null
- anchor: HostNode | null // fragment anchor
- target: HostElement | null // teleport target
- targetAnchor: HostNode | null // teleport target anchor
- staticCount: number
- suspense: SuspenseBoundary | null
- ssContent: VNode | null
- ssFallback: VNode | null
- shapeFlag: number
- patchFlag: number
- dynamicProps: string[] | null
- dynamicChildren: VNode[] | null
- appContext: AppContext | null
- memo?: any[]
- isCompatRoot?: true
- ce?: (instance: ComponentInternalInstance) => void
- }
VNode 对象的属性还是很多的,这里不用看这么多,先关注一下 type 属性。
- export interface VNode<
- HostNode = RendererNode,
- HostElement = RendererElement,
- ExtraProps = { [key: string]: any }
- > {
- type: VNodeTypes
- }
-
- export type VNodeTypes =
- | string
- | VNode
- | Component
- | typeof Text
- | typeof Static
- | typeof Comment
- | typeof Fragment
- | typeof TeleportImpl
- | typeof SuspenseImpl
type 属性用于描述 VNode 的类型,VNode 的类型有很多种,这里我们看下 string 和 Component 类型,当 VNode 的 type 属性是字符串的时候,说明当前的 VNode 描述的是普通的元素,当 VNode 的 type 是 Component 的时候,说明当前的 VNode 描述的是一个组件。
假设我们的 Vue 中有一个 MyComponent 组件,我们在一个模板字符串中使用了这个组件,代码如下所示:
- <template>
- <MyComponent>MyComponent>
- template>
上面的模板字符串会被编译成一个 render 函数,render 函数执行返回一个 VNode,这个 VNode 是一个组件类型的 VNode,表明需要渲染一个组件。
- componentVNode = {
- type: {
- ...组件的定义对象...
- },
- ......
- }
有了组件类型的 VNode,接下来看看这个组件 VNode 是如何渲染和更新的。
组件挂载和更新的逻辑都写在渲染器中,我们直接看源码。
- const patch: PatchFn = (
- n1,
- n2,
- container,
- anchor = null,
- parentComponent = null,
- parentSuspense = null,
- isSVG = false,
- slotScopeIds = null,
- optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
- ) => {
- // 解构获取 n2 的 type、ref、shapeFlag
- const { type, ref, shapeFlag } = n2
- // 根据 n2 Vnode 的类型进行不同的处理
- switch (type) {
- ......
- default:
- if (shapeFlag & ShapeFlags.ELEMENT) {
- // 处理元素节点
- processElement()
- } else if (shapeFlag & ShapeFlags.COMPONENT) {
- // 处理组件节点
- processComponent(
- n1,
- n2,
- container,
- anchor,
- parentComponent,
- parentSuspense,
- isSVG,
- slotScopeIds,
- optimized
- )
- } else if (__DEV__) {
- // 如果以上条件都不满足,并且是在开发模式下的话,则打印出相关警告:违法的 vnode 类型
- warn('Invalid VNode type:', type, `(${typeof type})`)
- }
- }
- }
在 patch 函数中,会根据 VNode 类型的不同使用不同的函数进行处理,如果当前的 VNode 表示的是组件的话,则会使用 processComponent 函数进行处理,processComponent 函数的内容如下所示:
- const processComponent = (
- n1: VNode | null,
- n2: VNode,
- container: RendererElement,
- anchor: RendererNode | null,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
- slotScopeIds: string[] | null,
- optimized: boolean
- ) => {
- if (n1 == null) {
- mountComponent(
- n2,
- container,
- anchor,
- parentComponent,
- parentSuspense,
- isSVG,
- optimized
- )
- } else {
- updateComponent(n1, n2, optimized)
- }
- }
在这里,判断 oldVNode 是否存在,如果存在的话,则执行 updateComponent 函数进行组件的更新,如果不存在的话,则执行 mountComponent 函数进行组件的挂载,我们首先看组件的挂载。
- // 挂载组件节点
- 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
- ))
-
- // resolve props and slots for setup context
- // 解析初始化一些数据
- if (!(__COMPAT__ && compatMountInstance)) {
- setupComponent(instance)
- }
-
- setupRenderEffect(
- instance,
- initialVNode,
- container,
- anchor,
- parentSuspense,
- isSVG,
- optimized
- )
- }
在 mountComponent 函数中,首先创建组件的实例,每渲染一次组件,就会创建一个对应的实例,组件实例就是一个对象,这个对象维护着组件运行过程中的所有信息,例如:注册的生命周期函数、组件上次渲染的 VNode,组件状态等等。一个组件实例的内容如下所示:
- const instance: ComponentInternalInstance = {
- uid: uid++,
- vnode,
- type,
- parent,
- appContext,
- root: null!, // to be immediately set
- next: null,
- subTree: null!, // will be set synchronously right after creation
- effect: null!,
- update: null!, // will be set synchronously right after creation
- scope: new EffectScope(true /* detached */),
- render: null,
- proxy: null,
- exposed: null,
- exposeProxy: null,
- withProxy: null,
- provides: parent ? parent.provides : Object.create(appContext.provides),
- accessCache: null!,
- renderCache: [],
-
- // local resolved assets
- components: null,
- directives: null,
-
- // resolved props and emits options
- propsOptions: normalizePropsOptions(type, appContext),
- emitsOptions: normalizeEmitsOptions(type, appContext),
-
- // emit
- emit: null!, // to be set immediately
- emitted: null,
-
- // props default value
- propsDefaults: EMPTY_OBJ,
-
- // inheritAttrs
- inheritAttrs: type.inheritAttrs,
-
- // state
- ctx: EMPTY_OBJ,
- data: EMPTY_OBJ,
- props: EMPTY_OBJ,
- attrs: EMPTY_OBJ,
- slots: EMPTY_OBJ,
- refs: EMPTY_OBJ,
- setupState: EMPTY_OBJ,
- setupContext: null,
-
- // suspense related
- suspense,
- suspenseId: suspense ? suspense.pendingId : 0,
- asyncDep: null,
- asyncResolved: false,
-
- // lifecycle hooks
- // not using enums here because it results in computed properties
- isMounted: false,
- isUnmounted: false,
- isDeactivated: false,
- bc: null,
- c: null,
- bm: null,
- m: null,
- bu: null,
- u: null,
- um: null,
- bum: null,
- da: null,
- a: null,
- rtg: null,
- rtc: null,
- ec: null,
- sp: null
- }
上面的对象包含着很多的状态信息,是实现组件化一个很重要的内容。
创建完组件实例后,Vue 使用 setupComponent 函数进行一些数据的解析和初始化,下面调用的 setupRenderEffect 函数是重点。
- const setupRenderEffect: SetupRenderEffectFn = (
- instance,
- initialVNode,
- container,
- anchor,
- parentSuspense,
- isSVG,
- optimized
- ) => {
- const componentUpdateFn = () => {
- // 使用组件实例的 isMounted 属性判断组件是否挂载
- // 如果为属性为 false,说明还未挂载,所以执行挂载逻辑
- // 如果属性为 true 的话,说明已经挂载,所以执行更新逻辑
- if (!instance.isMounted) {
- const { bm, m } = instance
-
- // beforeMount hook
- // 触发执行 beforeMount 生命周期函数
- if (bm) {
- invokeArrayFns(bm)
- }
- // 执行 render 函数,获取组件当前的 VNode
- const subTree = (instance.subTree = renderComponentRoot(instance))
- // 使用 patch 函数进行组件内容的渲染
- patch(
- null,
- subTree,
- container,
- anchor,
- instance,
- parentSuspense,
- isSVG
- )
-
- // mounted hook
- // 触发执行 mounted 生命周期函数
- if (m) {
- queuePostRenderEffect(m, parentSuspense)
- }
- // 将组件实例的 isMounted 属性设为 true,表明当前的组件已经完成了挂载操作
- instance.isMounted = true
- } else {
- let { bu, u } = instance
-
- // beforeUpdate hook
- // 触发执行 beforeUpdate 生命周期函数
- if (bu) {
- invokeArrayFns(bu)
- }
- // render
- // 执行 render 函数,获取组件最新的 VNode
- const nextTree = renderComponentRoot(instance)
- // 获取组件上次渲染的 VNode
- const prevTree = instance.subTree
- instance.subTree = nextTree
- // 使用 patch 函数进行组件的更新
- 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,
- isSVG
- )
- // updated hook
- // 触发执行 updated 生命周期函数
- if (u) {
- queuePostRenderEffect(u, parentSuspense)
- }
- }
- }
-
- // 组件的更新借助了响应式系统中的 ReactiveEffect 类
- const effect = (instance.effect = new ReactiveEffect(
- componentUpdateFn,
- () => queueJob(update),
- instance.scope // track it in component's effect scope
- ))
-
- const update: SchedulerJob = (instance.update = () => effect.run())
-
- update()
- }
上段代码中,借助 ReactiveEffect 类实现组件的更新,关于这个类的作用和源码可以看我的这篇博客,这里就不过多赘述了。
这里实现功能的重点在 componentUpdateFn 函数中,在上面代码的最后,执行了 update 函数,这会进而触发执行上面 componentUpdateFn 函数的执行,componentUpdateFn 函数的内部会执行组件的 render 函数,render 函数会读取组件的响应式数据,这会触发依赖收集。
componentUpdateFn 函数的解析看上面的注释即可。
当后续 render 函数依赖的响应式数据发生变化的时候,会再次触发执行 componentUpdateFn 函数进行组件的重新渲染,详细解释看上面源码的注释。
结论:Vue 通过代理让 render 函数执行时能够通过 this 访问到组件实例中的响应式数据。
Vue 通过 renderComponentRoot 函数执行 render 函数,获取 VNode。
instance.subTree = renderComponentRoot(instance)
- export function renderComponentRoot(
- instance: ComponentInternalInstance
- ): VNode {
- const {
- type: Component,
- vnode,
- proxy,
- props,
- propsOptions: [propsOptions],
- slots,
- attrs,
- emit,
- render,
- renderCache,
- data,
- setupState,
- ctx,
- inheritAttrs
- } = instance
-
- result = normalizeVNode(
- render!.call(
- proxy,
- proxy!,
- renderCache,
- props,
- setupState,
- data,
- ctx
- )
- )
-
- return result;
- }
render 函数执行的时候,函数中的 this 指向 instance.proxy,接下来看 instance.proxy 属性是如何创建出来的。
- export function setupComponent(instance) {
- ......
- setupStatefulComponent(instance);
- }
-
- function setupStatefulComponent(instance) {
- instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
- }
-
- export const PublicInstanceProxyHandlers = {
- get({ _: instance }: ComponentRenderContext, key: string) {
- const { ctx, setupState, data, props, accessCache, type, appContext } =
- instance
-
- if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
- accessCache![key] = AccessTypes.SETUP
- return setupState[key]
- } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
- accessCache![key] = AccessTypes.DATA
- return data[key]
- } else if (
- (normalizedProps = instance.propsOptions[0]) &&
- hasOwn(normalizedProps, key)
- ) {
- accessCache![key] = AccessTypes.PROPS
- return props![key]
- } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
- accessCache![key] = AccessTypes.CONTEXT
- return ctx[key]
- }
- }
- };
可以发现,在 render 函数中通过 this 读取某些属性的时候,代理会判断 instance 的 setupState、data、props 中有没有同名的属性,如果有的话,就进行数据的读取和返回,并且这些被读取属性已经是响应式的了。
setup 的官方文档点击这里。
setup 函数只会在组件挂载的时候执行一次,setup 函数既可以返回一个对象,也可以返回一个函数,如果返回的是一个对象的话,这个对象中的数据可以像 data 和 props 一样使用,如果返回的是一个函数的话,这个函数会被当成组件的 render 函数。
源码如下所示:
- // 挂载组件节点
- const mountComponent: MountComponentFn = (
- initialVNode,
- container,
- anchor,
- parentComponent,
- parentSuspense,
- isSVG,
- optimized
- ) => {
- ......
- // 解析初始化一些数据
- if (!(__COMPAT__ && compatMountInstance)) {
- setupComponent(instance)
- }
-
- setupRenderEffect(
- instance,
- initialVNode,
- container,
- anchor,
- parentSuspense,
- isSVG,
- optimized
- )
- }
-
- export function setupComponent(instance) {
- ......
- setupStatefulComponent(instance);
- }
-
- function setupStatefulComponent(instance) {
- const Component = instance.type as ComponentOptions
- ......
- const { setup } = Component
- if (setup) {
- setCurrentInstance(instance)
- const setupResult = callWithErrorHandling(
- setup,
- instance,
- ErrorCodes.SETUP_FUNCTION,
- [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
- )
- unsetCurrentInstance()
- handleSetupResult(instance, setupResult, isSSR)
- }
- }
-
- export function handleSetupResult(
- instance: ComponentInternalInstance,
- setupResult: unknown,
- isSSR: boolean
- ) {
- if (isFunction(setupResult)) {
- instance.render = setupResult as InternalRenderFunction
- } else if (isObject(setupResult)) {
- instance.setupState = proxyRefs(setupResult)
- }
- }
在 setupStatefulComponent 函数中,获取用户编写的 setup 函数,执行它并获取 setup 函数的返回值 setupResult。
接下来使用 handleSetupResult 函数处理结果,如果 setupResult 是一个函数的话,则将它赋值给组件实例的 render 属性,如果 setupResult 是一个对象的话,则将它赋值给组件实例的 setupState 属性上,当我们想在 render 函数中访问 setup 函数返回的数据时,Vue 会将读取操作代理到 setupState 属性上,源码如下所示:
- export const PublicInstanceProxyHandlers = {
- get({ _: instance }: ComponentRenderContext, key: string) {
- const { ctx, setupState, data, props, accessCache, type, appContext } =
- instance
-
- if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
- accessCache![key] = AccessTypes.SETUP
- return setupState[key]
- }
- ......
- }
- };
组件的生命周期原理很简单,主要分为两部分,分别是生命周期的注册以及生命周期的执行。
首先说生命周期的注册,这里以 setup 函数中进行的生命周期注册为例。
- function setupStatefulComponent(instance) {
- const Component = instance.type as ComponentOptions
- ......
- const { setup } = Component
- if (setup) {
- setCurrentInstance(instance)
- const setupResult = callWithErrorHandling(
- setup,
- instance,
- ErrorCodes.SETUP_FUNCTION,
- [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
- )
- unsetCurrentInstance()
- handleSetupResult(instance, setupResult, isSSR)
- }
- }
-
-
- export let currentInstance: ComponentInternalInstance | null = null
-
- export const setCurrentInstance = (instance: ComponentInternalInstance) => {
- currentInstance = instance
- }
-
- export const unsetCurrentInstance = () => {
- currentInstance = null
- }
在生命周期函数执行前,会执行一个 setCurrentInstance 函数,这个函数的作用是将当前的组件实例设置到全局中。
接下来看生命周期注册函数的内容,以 onMounted 函数为例:
- export const onMounted = createHook(LifecycleHooks.MOUNTED)
-
- export const createHook =
-
extends Function = () => any>(lifecycle: LifecycleHooks) => - (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
- // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
- (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
- injectHook(lifecycle, hook, target)
-
- export 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] = [])
- 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.
- setCurrentInstance(target)
- const res = callWithAsyncErrorHandling(hook, target, type, args)
- unsetCurrentInstance()
- resetTracking()
- return res
- })
- if (prepend) {
- hooks.unshift(wrappedHook)
- } else {
- hooks.push(wrappedHook)
- }
- return wrappedHook
- }
- }
当我们在 setup 函数中执行 onMounted 等生命周期注册函数时,Vue 会将我们想要注册的生命周期函数保存到组件实例中,组件实例用于保存生命周期函数的属性如下所示:
- const instance: ComponentInternalInstance = {
- // lifecycle hooks
- bc: null,
- c: null,
- bm: null,
- m: null,
- bu: null,
- u: null,
- um: null,
- bum: null,
- da: null,
- a: null,
- rtg: null,
- rtc: null,
- ec: null,
- sp: null
- }
知道了生命周期函数是如何注册的,接下来看看生命周期函数是如何触发的,生命周期函数触发的代码在 setupRenderEffect 函数中,代码如下所示:
- const setupRenderEffect: SetupRenderEffectFn = (
- instance,
- initialVNode,
- container,
- anchor,
- parentSuspense,
- isSVG,
- optimized
- ) => {
- const componentUpdateFn = () => {
- // 使用组件实例的 isMounted 属性判断组件是否挂载
- // 如果为属性为 false,说明还未挂载,所以执行挂载逻辑
- // 如果属性为 true 的话,说明已经挂载,所以执行更新逻辑
- if (!instance.isMounted) {
- const { bm, m } = instance
-
- // beforeMount hook
- // 触发执行 beforeMount 生命周期函数
- if (bm) {
- invokeArrayFns(bm)
- }
- // 执行 render 函数,获取组件当前的 VNode
- const subTree = (instance.subTree = renderComponentRoot(instance))
- // 使用 patch 函数进行组件内容的渲染
- patch(
- null,
- subTree,
- container,
- anchor,
- instance,
- parentSuspense,
- isSVG
- )
-
- // mounted hook
- // 触发执行 mounted 生命周期函数
- if (m) {
- queuePostRenderEffect(m, parentSuspense)
- }
- // 将组件实例的 isMounted 属性设为 true,表明当前的组件已经完成了挂载操作
- instance.isMounted = true
- } else {
- let { bu, u } = instance
-
- // beforeUpdate hook
- // 触发执行 beforeUpdate 生命周期函数
- if (bu) {
- invokeArrayFns(bu)
- }
- // render
- // 执行 render 函数,获取组件最新的 VNode
- const nextTree = renderComponentRoot(instance)
- // 获取组件上次渲染的 VNode
- const prevTree = instance.subTree
- instance.subTree = nextTree
- // 使用 patch 函数进行组件的更新
- 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,
- isSVG
- )
- // updated hook
- // 触发执行 updated 生命周期函数
- if (u) {
- queuePostRenderEffect(u, parentSuspense)
- }
- }
- }
-
- // 组件的更新借助了响应式系统中的 ReactiveEffect 类
- const effect = (instance.effect = new ReactiveEffect(
- componentUpdateFn,
- () => queueJob(update),
- instance.scope // track it in component's effect scope
- ))
-
- const update: SchedulerJob = (instance.update = () => effect.run())
-
- update()
- }
在 componentUpdateFn 函数中,进行了组件的初始挂载和更新,生命周期函数就是在这些操作的前后触发执行的,在上面的源码中,使用 invokeArrayFns 函数进行生命周期函数的触发执行,它的源码如下所示:
- export const invokeArrayFns = (fns: Function[], arg?: any) => {
- for (let i = 0; i < fns.length; i++) {
- fns[i](arg)
- }
- }
这篇博客讲解了 Vue3 中是如何实现组件化的,下面的博客详细讲讲异步组件以及 Vue 提供的一些内置组件。