这篇文章我们来了解下Vue3的初始化流程,总共包括以下几个流程👇:
思维导图
下面我们来看看每个步骤都做了什么,逐个击破🚀
createApp
js
// packages/runtime-dom/src/index.ts
// 这里的 createApp 方法是在写页面时实际调用的方法
export const createApp = ((...args) => {
// 1、获取渲染器,并执行渲染器的 createApp 方法,创建 app 应用实例
const app = ensureRenderer().createApp(...args)
// ...
// 2、扩展 mount 方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 获取根节点,即页面上 id 为 app 的div标签
const container = normalizeContainer(containerOrSelector)
//...
const component = app._component
// 获取模板
if (!isFunction(component) && !component.render && !component.template) {
// 将根节点下的HTML内容添加到组件的 template 上
component.template = container.innerHTML
// ...
}
const proxy = mount(container, false, container instanceof SVGElement)
// ...
return proxy
}
return app
}) as CreateAppFunction<Element>
在Vue3中创建Vue应用实例使用的 createApp
方法,调用的就是在 packages/runtime-dom/src/index.ts 中定义的 createApp
函数。
在 createApp
中主要做了两件事情:
- 调用
ensureRenderer
函数获取渲染器,然后执行渲染器的createApp
方法创建 app 应用实例。 - 获取
app
应用实例的mount
方法,对mount
方法进行扩展。
createRenderer
js
// packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
在 createRenderer
函数中,仅仅只是调用了 baseCreateRenderer
函数,传入 options,创建一个渲染器。在baseCreateRenderer 函数中,返回的对象就是渲染器,渲染器中的 createApp
方法,就是创建 app
应用实例最终执行的方法。
createAppAPI
js
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
// 返回开发者使用的app工厂函数
return function createApp(rootComponent, rootProps = null) {
// rootComponent 就是 createApp函数的 args,也就是options
// ...
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
// 应用程序实例
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
// ...
},
// 加载插件,和vue2不同的是,vue2的插件是全局的,这里只针对一个vue实例
use(plugin: Plugin, ...options: any[]) {
// ...
},
// 混入
mixin(mixin: ComponentOptions) {
// ...
},
// 加载组件
component(name: string, component?: Component): any {
// ...
},
// 指令
directive(name: string, directive?: Directive) {
// ...
},
// 挂载,核心渲染逻辑
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// ...
},
// 卸载
unmount() {
// ...
},
// 注入
provide(key, value) {
// ...
}
})
return app
}
}
在 createAppAPI
函数中,返回的工厂函数 createApp
就是开发者在页面中调用的createApp
方法,在工厂函数中创建了一个app应用程序实例,并将其添加到context对象的app属性上,最后将其返回出去。
mount
js
// packages/runtime-core/src/apiCreateApp.ts
// 挂载,核心渲染逻辑,将 vnode 转换成真实DOM
// 注意:mount方法只会执行一次
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 判断是否已挂载
if (!isMounted) {
// 用于检测当前是否已经有一个 Vue 应用实例挂载在指定的 DOM 元素上。
// 首先判断当前是否已经挂载了 Vue 应用实例,如果没有挂载,就继续执行下面的代码。如果已经挂载了,就在开发环境下使用 warn 函数输出警告信息,提示开发者需要先卸载之前的应用实例,然后再挂载新的应用实例。
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
// 首次挂载,创建根组件对应的vnode,即虚拟DOM
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 应用上下文(app context)是一个全局的对象,用于存储应用实例、全局配置、全局 API 等信息。在应用初始化时,会创建一个应用上下文,并将其传递给组件实例、指令、插件等。
vnode.appContext = context
// 开发环境下 热更新
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
// 转换 VNode 的处理逻辑
if (isHydrate && hydrate) {
// ssr 服务端渲染 的 VNode 转换逻辑
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// spa 前端渲染 的 VNode 转换逻辑
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// 根容器上设置一个特殊标记 __vue_app__,用于判断一个 DOM 元素上是否已经挂载了 Vue 应用实例
;(rootContainer as any).__vue_app__ = app
// 如果是开发环境或者开启了生产环境下的开发工具支持,还会将应用实例的 _instance 属性设置为根 VNode 对应的组件实例,然后调用 devtoolsInitApp 函数,将应用实例注册到开发工具中。这样,开发者就可以在开发工具中查看应用的状态和行为,方便调试和排查问题
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
},
mount
方法是 app
应用实例的一个属性,用来挂载节点,它会将根组件下的HTML内容渲染出来,并建立更新机制。
mount
方法只会执行一次,即在app
应用实例挂载时执行。它调用createVNode函数创建根组件对应的VNode,然后判断是否是服务端渲染,如果是,则执行外部传入的hydrate方法,将VNode转换成真实DOM;如果不是,则调用外部传入的render方法,将VNode转换成真实DOM,将HTML内容渲染出来。
render
js
// packages/runtime-core/src/renderer.ts
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
// 卸载组件
unmount(container._vnode, null, null, true)
}
} else {
// 执行patch过程,即 diff 过程
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
在 mount
方法中,Vue会判断当前的渲染时服务端渲染还是前端渲染,如果是前端渲染,就会执行从外部传入的render方法,将VNode转换成真实DOM,将HTML内容渲染出来。
在 render 方法中,如果vnode不为null,即根组件对应的vnode(即虚拟DOM)存在,则执行 patch 过程,即 Diff 过程。
patch
js
// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1, // 旧vnode 初始化时n1 为 null
n2, // 新vnode 初始化时n2 是根节点
container, // div#app
anchor = null, // 定位锚点dom,用于往锚点前插入节点
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// ...
const { type, ref, shapeFlag } = n2
// 这里的 type 是 component,也就是API createApp 的入参 args
switch (type) {
// ...
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// ...
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 首次初始化时会进入这里,处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
// ...
}
// ...
}
patch
过程,也就是 Diff
过程。patch
的过程就是以新的VNode
为基准,去更新旧的VNode
。在首次初始化时,会执行 processComponent
方法挂载组件。
processComponent
js
// packages/runtime-core/src/renderer.ts
const processComponent = (
n1: VNode | null, // 旧vnode 初始化时 n1 为 null
n2: VNode, // 新vnode 初始化时 n2 是根节点
container: RendererElement, // 初始化时是 div#app
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// ...
// 旧vnode为null,说明这是首次初始化
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// ...
} else {
// 首次初始化时 挂载组件
mountComponent(
n2, // 初始化时 n2 就是根节点的vnode
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 这里是 更新阶段
//...
}
}
在 processComponent
中,判断 n1
是否为null
,即旧的VNode
是否为null
,如果为null
,说明是首次初始化,则调用 mountComponent
挂载组件,其中传入 mountComponent
的 n2
就是根节点的VNode
。
下面我们来看下 mountComponent
挂载组件的实现过程
mountComponent
js
// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode, // 在初始化时 initialVNode 是根节点的vnode
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 在 Vue.js 2.x 中,因为组件实例的创建和挂载是分开进行的,即先创建组件实例,然后将组件实例挂载到指定的 DOM 元素上。而在 Vue.js 3 中,组件实例的创建和挂载是合并在一起的,即在 createComponentInstance 函数中同时创建组件实例和对应的 VNode。为了兼容 Vue.js 2.x,Vue.js 3 中提供了一个 __COMPAT__ 标记,用于判断当前是否处于 Vue.js 2.x 的兼容模式下。
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
// 1、创建根组件实例
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// ...
// 在 <keep-alive> 组件的上下文中,注入渲染器的内部实现,以便在组件缓存和激活时可以调用相应的方法
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 调用 setupComponent 函数,用于解析 props 和 slots,并将解析后的结果存储到组件实例的上下文 ctx 中
// 以便于在 setup 函数中,可以访问到组件的 props、slots、attrs 等信息
if (!(__COMPAT__ && compatMountInstance)) {
// ...
// 2、组件实例安装,相当于组件初始化,this._init
// 属性声明,响应式等等
setupComponent(instance)
// ..
}
// ...
// 3、设置并运行带有副作用的渲染函数,相当于是 updateComponent + watcher
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
// ...
}
mountComponent
的作用就是挂载组件,在 mountComponent
中,首先调用 createComponentInstance
创建根组件实例,然后将创建好的根组件实例传入 setupComponent
中,进行 props
以及 slots
的初始化,最后设置并运行带有副作用的渲染函数 setupRenderEffect
。
再来看看 setupComponent
中做了什么事情
setupComponent
js
// packages/runtime-core/src/component.ts
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
// 是否是状态型组件
const isStateful = isStatefulComponent(instance)
// 初始化组件的属性
initProps(instance, props, isStateful, isSSR)
// 初始化组件的插槽
initSlots(instance, children)
// 如果是状态型组件,则为其挂载setup信息
// 非状态型组件仅用来纯UI展示,不需要挂载状态信息
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
看起来不复杂,setupComponent
用于初始化props
、slots
等。首先判断当前根组件实例是否状态型组件,如果是状态型组件,则初始化组件的属性props
,并为根组件实例挂载setup
信息。
setupStatefulComponent
js
// packages/runtime-core/src/component.ts
// 状态型组件生成setup结果并进行信息挂载
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 组件的 options,也就是 vnode.type
const Component = instance.type as ComponentOptions
// ...
// 0. 创建渲染代理属性访问缓存
instance.accessCache = Object.create(null)
// 1. 为组件实例创建渲染代理,同时将代理标记为 raw,
// 为的是在后续过程中不会被误转化为响应式数据,
// 渲染代理源对象是组件实例上下文
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
// ...
// 2. 调用 setup 函数
// 这里的setup是开发者调用 createApp 时传入的 setup 函数
const { setup } = Component
if (setup) {
// 创建 setup上下文并挂载到组件实例上
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 记录当前正在初始化的组件实例
setCurrentInstance(instance)
// 执行 setup 前暂停依赖收集
// PS: 执行setup期间是不允许进行依赖收集的,setup只是为了获取需要为组件提供的状态信息,在它里面不应该有其它非必要的副作用
// 真正的依赖收集等有较强副作用的操作应该放到 setup挂载之后,以免产生不可预测的问题
pauseTracking()
// 执行 setup 函数,并获得安装结果信息,setup执行结构就是我们定义的响应式数据、函数、钩子等
const setupResult = callWithErrorHandling(
setup, // 开发者调用 createApp 时定义的 setup函数
instance, // 根组件实例
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// setup执行完毕后恢复依赖收集
resetTracking()
// 重置当前根组件实例
unsetCurrentInstance()
// 挂载 setup执行的结果
// 在 SSR 服务端渲染或者 suspense 时 setup 返回的是 promise
// 因此需要判断 setupResult 是否是 promise,进行不同的操作
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// 在SSR或者suspense时setup返回promise
// suspense因为有节点fallback,而setup中是正式渲染内容,因此是一个异步resolve的过程
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
}
// ...
} else {
// setupResult 返回的不是 promise
// 直接将 setup的执行结果挂载到组件实例上
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// ...
}
}
setupStatefulComponent
为状态型组件生成setup结果并进行信息挂载。
- 它首先会为组件实例创建渲染代理来访问缓存。
- 如果开发者创建app应用实例时传入了setup函数,则调用 createSetupContext 创建setup上下文并挂载到组件实例上,然后暂停 effect 的依赖收集,等到执行完setup并拿到其返回的结果setupResult后再继续进行effect的依赖收集。
- 最后将setup的执行结果挂载到组件实例上
再来看看我们最常用的 setup
函数
setup
这里的setup
是开发者调用 createApp 时传入的 setup
函数。setup
函数执行后,其执行结果会被挂载到根组件实例上。
html
<script>
Vue.createApp({
setup() {
const state = reactive({})
return {
state
}
},
}).mount('#app')
< /script>
handleSetupResult
handleSetupResult
将 setup的执行结果挂载到根组件实例上。
js
// packages/runtime-core/src/component.ts
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
// setup的执行结果是一个函数
if (isFunction(setupResult)) {
// 如果是函数,则说明 setup 函数返回了一个内联渲染函数,需要将其挂载到组件实例的 render 或 ssrRender 属性上,以便在组件渲染时调用。
if (__SSR__ && (instance.type as ComponentOptions).__ssrInlineRender) {
// 如果是SSR 服务端渲染,将 setup 执行结果挂载到根组件实例的 ssrRender 属性上
instance.ssrRender = setupResult
} else {
// SPA 前端渲染,将 setup执行结果挂载到根组件实例的 render 属性上
instance.render = setupResult as InternalRenderFunction
}
} else if (isObject(setupResult)) {
// ...
// setup的执行结果是一个Object对象
// 将setup的执行结果挂载到根组件实例的 setupState 属性上
instance.setupState = proxyRefs(setupResult)
// ...
}
//...
finishComponentSetup(instance, isSSR)
}
handleSetupResult
会将setup
的执行结果进行不同的处理。如果setup
的执行结果是一个函数,则将其挂载到根组件实例的 render 属性上。如果setup
的执行结果是一个Object对象,则将其挂载到根组件实例的 setupState
属性上。
finishComponentSetup
js
// packages/runtime-core/src/component.ts
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
// 组件的 options,也就是 vnode.type,也就是API: createApp 的参数
const Component = instance.type as ComponentOptions
// ...
// 首先判断根组件是否已经存在渲染函数。如果不存在,则需要对根组件的模板进行编译
if (!instance.render) {
// 如果当前不处于 SSR 服务端渲染模式,并且存在编译器 compile,并且根组件选项中没有指定 render 属性,则将根组件的模板编译为渲染函数,并将其挂载到 Component.render 属性上。
if (!isSSR && compile && !Component.render) {
// 获取根组件的 innerHTML 内容
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template
if (template) {
// ...
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } =
Component
// 编译的 options
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
if (__COMPAT__) {
// 在编译模板时,需要根据组件选项和全局配置中的编译选项进行合并,并调用编译器 compile 函数进行编译
finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
if (Component.compatConfig) {
extend(finalCompilerOptions.compatConfig, Component.compatConfig)
}
}
// 编译 template,即编译 根组件下的innerHTML内容
// 并把编译后的内容挂载到 Component 的render 属性上
// 开发者调用createApp 时传入的参数中是没有render属性,
// 我们再打印出createApp 时的参数看到的render属性就是在这里挂载上去的
Component.render = compile(template, finalCompilerOptions)
// ...
}
}
// 将编译结果挂载到根组件实例上
instance.render = (Component.render || NOOP) as InternalRenderFunction
// ...
}
// ...
}
根组件的模板或渲染函数可以通过组件选项的 template
或 render
属性来指定。如果指定了 template
属性,则需要将其编译为渲染函数,以便在组件渲染时调用。如果指定了 render
属性,则可以直接使用该渲染函数
如果开发者创建app应用实例时没有传入setup
函数,则执行finishComponentSetup
来挂载编译后的内容。首先获取根组件的 innerHTML
内容,对其进行编译,然后把编译后的内容挂载到 Component
的 render
属性上,再将编译结果挂载到根组件实例的render
属性上。
setupRenderEffect
setupRenderEffect 函数的作用运行带有副作用的render函数,其实现了 instance.update 方法,这个方法其实就是一个effect。
js
// packages/runtime-core/src/renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建更新函数
const componentUpdateFn = () => {
// 如果是首次挂载 isMounted 为 false,因此这里是初始化流程
if (!instance.isMounted) {
// 首次挂载阶段
// 首先会执行 组件实例的 render 函数获取VNode,
// 然后进入patch过程,将VNode转换成真实DOM,渲染到界面上
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)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeMount')
}
toggleRecurse(instance, true)
if (el && hydrateNode) {
// 服务端渲染
// 在水合操作中,首先执行根组件的渲染函数,获取根组件的 VNode。然后,调用 hydrateNode 函数进行水合操作,将服务器端渲染的 HTML 内容与客户端渲染的 VNode 进行比对,并尽可能地复用已有的 DOM 元素。
const hydrateSubTree = () => {
// ...
// 开始渲染根组件,执行当前组件实例的 render 函数获取VNode
instance.subTree = renderComponentRoot(instance)
// ...
hydrateNode!(
el as Node,
instance.subTree,
instance,
parentSuspense,
null
)
// ...
}
// 首先判断初始 VNode 是否是异步组件的占位符 VNode
if (isAsyncWrapperVNode) {
;(initialVNode.type as ComponentOptions).__asyncLoader!().then(
//如果是,则需要等待异步组件加载完成后再进行水合操作。在等待异步组件加载完成时,会调用异步组件的加载函数 __asyncLoader,并在加载完成后执行水合操作。
() => !instance.isUnmounted && hydrateSubTree()
)
} else {
// 如果初始 VNode 不是异步组件的占位符 VNode,则直接执行水合操作。
hydrateSubTree()
}
} else {
// ...
// 前端渲染
// 开始渲染根组件,执行当前组件实例的 render 函数获取VNode
// 首先获取 组件vnode,其实就是调用组件的 render
// 这次调用触发了依赖收集
// 生成子树结构,创建组件的vnode
// 执行组件内部的render函数 (手写或template编译生成) 生成渲染vnode,渲染vnode就是组件实际要渲染出来的
// 内容对应的vnode,并将渲染vnode存储到组件实例上
// 注意:执行render函数会访问组件实例上的响应式数据,从而触发依赖收集,当前定义的renderEffect会被收集到依赖仓库
// 当后续发生数据变化时,renderEffect则会被派发,触发re-render
const subTree = (instance.subTree = renderComponentRoot(instance))
// ...
// 向下递归更新
// 首次渲染是完整递归创建
// 进入 Diff 过程,将子树渲染到container中
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// ...
initialVNode.el = subTree.el
}
// mounted hook
// 执行mount(挂载之后)钩子函数
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeMounted)
) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// 判断初始 VNode 是否是 keep-alive 组件的 VNode。如果是,则需要在组件渲染后执行 activated 钩子函数。
if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
// 在执行 activated 钩子函数时,需要注意钩子函数可能是由子组件的 keep-alive 传递而来的。因此,需要在组件的首次渲染后再执行 activated 钩子函数。
instance.a && queuePostRenderEffect(instance.a, parentSuspense)
// 如果开启了 Vue.js 2.x 的兼容模式,并且当前组件实例已经被标记为具有兼容性问题,还会触发 hook:activated 事件,以兼容 Vue.js 2.x 中的 activated 钩子函数。
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:activated'),
parentSuspense
)
}
}
instance.isMounted = true
// ...
} else {
// updateComponent
// 更新阶段
// ...
}
}
// 创建 副作用,类似于 react 的 useEffect hooks
// 副作用:如果 componentUpdateFn 执行的过程中有响应式数据发生变化,则按照
// 参数2 () => queueJob(instance.update) 的方式执行参数1
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn, // 在响应式数据发生变化时 componentUpdateFn 会再次执行
() => queueJob(instance.update), // 执行instance.update 就是执行 componentUpdateFn
instance.scope // track it in component's effect scope
))
// 创建 update,这个update就是 effect 的run方法,
// 其实就是执行实例化 ReactiveEffect 时传入的 componentUpdateFn
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
// ...
// 执行更新
update()
}
最后,我们来总结下整个过程:
-
在
setupRenderEffect
函数中,首先创建了一个componentUpdateFn
函数,这个函数用来执行当前组件实例的render函数获取VNode,然后进入Diff
过程,将子树渲染到container
中。 -
在SSR 服务端渲染时,需要将服务器端渲染的
HTML
内容与客户端渲染的VNode
进行比对,以便在客户端渲染时尽可能地复用已有的DOM
元素,提高渲染性能。这个过程称为水合(hydration
)。 -
异步组件可以通过
defineAsyncComponent
函数来定义。当异步组件被渲染时,会先渲染一个占位符VNode
,然后在异步组件加载完成后再替换为真正的组件VNode
。 -
keep-alive
组件的activated
钩子函数实现原理:- 首先判断初始
VNode
是否是keep-alive
组件的VNode
。如果是,则需要在组件渲染后执行activated
钩子函数。 - 在执行
activated
钩子函数时,需要注意钩子函数可能是由子组件的keep-alive
传递而来的。因此,需要在组件的首次渲染后再执行activated
钩子函数。 - 如果开启了
Vue.js 2.x
的兼容模式,并且当前组件实例已经被标记为具有兼容性问题,还会触发 hook:activated
事件,以兼容Vue.js 2.x
中的activated
钩子函数。
- 首先判断初始
-
创建完更新函数
componentUpdateFn
后,将componentUpdateFn
传入new ReactiveEffect()
中创建一个副作用。如果componentUpdateFn
在执行的过程中有响应式数据发生了变化,则按照参数2:() => queueJob(instance.update)
的方式执行参数1。 -
接下来执行effect.run 方法创建一个
update
,这个update
其实就是effect
的run
方法,也就是执行实例化ReactiveEffect
时传入的componentUpdateFn
。 -
最后是执行这个
update
,其实就是执行componentUpdateFn
,通过执行componentUpdateFn
,来完成更新操作。