这里是 Vue3 文档 提供的一个 Hello World 代码:
<div id="app">
<p>{{ message }}</p>
</div>
const HelloVueApp = {
data() {
return {
message: 'Hello Vue!!'
}
}
}
Vue.createApp(HelloVueApp).mount('#app')
它可以在浏览器上渲染出“Hello Vue”这个字符串,当然对于开发者来说这不要太简单——在 HTML 中添加一个文本节点。但 Vue 作为一个框架,它要实现这个简单的功能要经过哪些步骤呢?
从上面代码可以看到,需要调用两个和 Vue 相关的函数 createApp
和 mount
,这就是两个主要过程:
- 创建应用
createApp
- 挂载应用
mount
创建应用
const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
return app
})
函数 createApp
主要职责有二:
- 创建 App 实例
- 调用
ensureRenderer
创建 Renderer 单例 - 调用 Renderer 的
createApp
方法创建 App 实例
- 调用
- 代理 App 的
mount
方法,在调用原方法前处理一下 App 容器(即mount
函数的入参#app
)
创建渲染器
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
可以看到 ensureRenderer
其实是一个简单的单例实现,保证渲染器(Renderer)只会被创建一次。传入 createRenderer
的参数 rendererOptions
是一些指令集合,由 DOM 操作和 prop patch 两个部分组成,比如 renderOptions
提供的 createText
方法就是调用 DOM 的 document.createTextNode
方法创建真实的 DOM 节点(等下会用到它)。
createRenderer
function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
函数 createRenderer
干啥啥不行透传第一名,是否就是一个冗余的函数呢?其实不是的,它存在一个“兄弟函数” createHydrationRenderer
用于创建 SSR 的 Renderer ,他们虽然都调用同一个 baseCreateRenderer
,但传参不同,并且 createHydrationRenderer
还会依赖注水(hydration
)相关的逻辑。
所以这里用两个函数分治两种逻辑,有两个优点:
- tree-shaking,WEB 环境打包时不会打包
hydration
相关代码。 - 代码易读,不用通过参数或则注释来区分创建两种渲染器的区别。
baseCreateRenderer
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 给 renderOptions 中的指令起别名,添加 host 前缀
const {
insert: hostInsert,
...
createText: hostCreateText,
...
} = options
// 非常重要的方法,把 VNode -> DOM
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
// 和本文相关,处理文本节点的方法(patch 方法中会调用)
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
...
// 非常重要的方法,Renderer 暴露的唯三方法之一(其中还有个是 SSR 的),在内部调用 patch 和 unmount 两个方法
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
这个就是创建 Renderer 最核心的函数了,这里只能展示部分代码。baseCreateRenderer
的主要功能是创建 VNode 节点的处理方法,在这些处理方法中会调用 renderOptions
中封装的 DOM API ,实现参考上边代码中的 processText
,另外还会导出两个重要的入口方法(忽略 SSR):
render
负责把 VNode “渲染”为真实的 DOM,或则移除 VNode 关联的 DOM ,它总会在 VNode 发生改变后被调用。createApp
创建 App 实例的方法。
我们关注的 createApp
,是由 createAppAPI(render, hydrate)
返回的一个方法。
createAppAPI
function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
...
}
}
函数 createAppAPI
形成了一个闭包环境,让内部的 createApp
方法可以调用渲染器的 render
方法。
创建应用
function createApp(rootComponent, rootProps = null) {
...
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
...
mount(rootContainer: HostElement, isHydrate?: boolean): any {
...
},
})
...
return app
}
可以看到 ensureRenderer().createApp(...args)
调用的方法其实就是 createAppAPI
返回的 createApp
方法,所以 Demo 代码中传入的 HelloVueApp
对象,被赋值给 App 的 component
属性了。
App 其实就是经常被提到的 Vue 实例,它存在如下属性和方法(在 runtime-core/src/apiCreateApp.ts
的 createApp
方法中实现):
export interface App<HostElement = any> {
version: string
config: AppConfig
use(plugin: Plugin, ...options: any[]): this
mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined
component(name: string, component: Component): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
mount(
rootContainer: HostElement | string,
isHydrate?: boolean
): ComponentPublicInstance
unmount(rootContainer: HostElement | string): void
provide<T>(key: InjectionKey<T> | string, value: T): this
// internal, but we need to expose these for the server-renderer and devtools
_uid: number
_component: ConcreteComponent
_props: Data | null
_container: HostElement | null
_context: AppContext
}
可以注意到 App 有一个 mount
方法,他很重要,下文会讲到。
挂载应用
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
回到最初的 createApp
方法,它创建了 App 后还会代理 mount
函数,在 proxy mount
中会优先处理调用 normalizeContainer
处理入参 containerOrSelector
,也就是 Demo 代码中的 #app
,然后才执行原始的 mount
方法。
标准化应用容器
function normalizeContainer(container: Element | string): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
if (__DEV__ && !res) {
warn(`Failed to mount app: mount target selector returned null.`)
}
return res
}
return container
}
函数 normalizeContainer
只是把字符串的入参替换为相应的 DOM 对象,看到这个函数我们也该明白 proxy mount
方法是支持两种参数类型的:
- 字符串(Demo 中的
#app
字符串被用于寻找id
为app
的 DOM) - DOM 对象
接下来我们继续来看 mount
真正的实现,上文提过它在 runtime-core/src/apiCreateApp.ts
这个文件的 createApp
方法中。
挂载应用
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
...
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer)
}
isMounted = true
...
}
},
可以看到 mount
方法其实只有两个职责:
- 创建 VNode
createVNode
- 调用渲染器的渲染函数
render
我们先看创建 VNode ...
createVNode
_createVNode
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
if (isVNode(type)) {
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// class & style normalization.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`,
`\nComponent that was made reactive: `,
type
)
}
const vnode: VNode = {
__v_isVNode: true,
[ReactiveFlags.SKIP]: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
const { content, fallback } = normalizeSuspenseChildren(vnode)
vnode.ssContent = content
vnode.ssFallback = fallback
}
if (
shouldTrack > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
return vnode
}
看图和代码应该很清楚,createVNode
创建了一个特定对象用来描述节点,值得注意的是 VNode 中的 shapeFlag
属性,因为它的值和子节点的类型相关,这通常代表着属性将在递归过程中扮演重要角色。在本例中,传入的 createApp
的参数是一个对象且没有子节点,所以 shapeFlag
的值是 ShapeFlags.STATEFUL_COMPONENT
,表示有状态组件。
如果不熟悉 VNode ,可以翻翻这两篇文章:
normalizeChildren
function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
if (children == null) {
children = null
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') {
if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
// Normalize slot to plain children for plain element and Teleport
const slot = (children as any).default
if (slot) {
// _c marker is added by withCtx() indicating this is a compiled slot
slot._c && setCompiledSlotRendering(1)
normalizeChildren(vnode, slot())
slot._c && setCompiledSlotRendering(-1)
}
return
} else {
type = ShapeFlags.SLOTS_CHILDREN
const slotFlag = (children as RawSlots)._
if (!slotFlag && !(InternalObjectKey in children!)) {
// if slots are not normalized, attach context instance
// (compiled / normalized slots already have context)
;(children as RawSlots)._ctx = currentRenderingInstance
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
// a child component receives forwarded slots from the parent.
// its slot type is determined by its parent's slot type.
if (
currentRenderingInstance.vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS
) {
;(children as RawSlots)._ = SlotFlags.DYNAMIC
vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
} else {
;(children as RawSlots)._ = SlotFlags.STABLE
}
}
}
} else if (isFunction(children)) {
children = { default: children, _ctx: currentRenderingInstance }
type = ShapeFlags.SLOTS_CHILDREN
} else {
children = String(children)
// force teleport children to array so it can be moved around
if (shapeFlag & ShapeFlags.TELEPORT) {
type = ShapeFlags.ARRAY_CHILDREN
children = [createTextVNode(children as string)]
} else {
type = ShapeFlags.TEXT_CHILDREN
}
}
vnode.children = children as VNodeNormalizedChildren
vnode.shapeFlag |= type
}
该方法判断子节点类型并改变 VNode 的 shapeFlag
值,这样操作 VNode 时通过 shapeFlag
值就可以知道子节点的类型,方便进行下一步处理。另外,在上方的 createVNode
的函数图解中,我之所以说 Suspense 组件判断存在问题,就是因为它在 shapeFlag
被改变后才进行判断,这个我后面确认了再补充剩余信息。
render
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
上文提到过渲染函数的作用是把 VNode 和 DOM 关联起来,它通过 patch
方法把 VNode 翻译为 DOM ,patch
方法的参数是:
- 容器的 VNode
- 当前要渲染节点的 VNode (
HelloVueApp
对象生成的 VNode) - 容器本身(id 为
app
的 DOM)
patch
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else ...
}
我们已经知道 patch
入参中的 n2
是 HelloVueApp
对象生成的 VNode ,并且上文说过它的 shapeFlags
的值是 4 (有状态组件),而 ShapeFlags.COMPONENT
的值是 6 包含有状态组件 4 和函数组件 2 ,所以它会进入 processComponent
方法的处理流程。
注:4 & 6 === true
processComponent
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
函数 processComponent
只有一个分发职责,即判断是否当前节点的父级 VNode 是否存在,存在就执行更新 updateComponent
,不存在就执行挂载 mountComponent
,本文不讨论更新,直接看挂载吧。
mountComponent
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
setupComponent(instance)
...
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
...
}
函数 mountComponent
主要处理三件事:
- 创建组件实例
createComponentInstance
- 准备组件内容
setupComponent
- 渲染组件
setupRenderEffect
createComponentInstance
function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
...
}
return instance
函数 createComponentInstance
创建一个对象并返回,这个对象便是我们在 Options API 中用 this
访问到的组件实例。
setupComponent
function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
...
const { setup } = Component
if (setup) {
...
} else {
finishComponentSetup(instance, isSSR)
}
}
函数 setupComponent
的职责是处理组件的一些配置信息,它分为两步:
- 初始化
props
和slots
- 调用
setupStatefulComponent
处理组件的其他配置信息
函数 setupStatefulComponent
主要处理组件的其他配置信息(比如 methods / data / watch
等等),通过调用 finishComponentSetup
方法实现。在此之前,还会通过是否存在 setup
区分 Composition API 和 Options API ,如果是 Composition API 则需要先执行 setup
函数。
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
...
if (compile && Component.template && !Component.render) {
if (__DEV__) {
startMeasure(instance, `compile`)
}
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
})
if (__DEV__) {
endMeasure(instance, `compile`)
}
}
...
// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance
applyOptions(instance, Component)
currentInstance = null
}
...
}
函数 finishComponentSetup
的职责也很清晰,两件:
- 如果存在
compile
函数且模板未被编译,则执行编译 - 调用
applyOptions
处理组件配置信息
Vue3 的 compile
的流程虽然比 Vue2 更清晰,但也需要较长篇幅来解释,故留到其他文章,这里只放一张编译的流程图解,方便了解 compile
函数的输入输出以及核心流程:
函数 applyOptions
会处理很多选项,每个选项都涉及有些浪费时间,既然我们这里只配置了 data
选项,那就只分析 data
的处理流程。
export function applyOptions(
instance: ComponentInternalInstance,
options: ComponentOptions,
deferredData: DataFn[] = [],
deferredWatch: ComponentWatchOptions[] = [],
deferredProvide: (Data | Function)[] = [],
asMixin: boolean = false
) {
const { data: dataOptions, ... } = options
...
isInBeforeCreate = true
callSyncHook(
'beforeCreate',
LifecycleHooks.BEFORE_CREATE,
options,
instance,
globalMixins
)
isInBeforeCreate = false
...
if (dataOptions) {
resolveData(instance, dataOptions, publicThis)
}
...
callSyncHook(
'created',
LifecycleHooks.CREATED,
options,
instance,
globalMixins
)
...
}
可以看到 applyOptions
的职责如下:
- 调用
beforeCreate
钩子,此时配置项还未被处理,所以只能通过实例取到之前就处理了的 props 和 slots - 分发处理函数,比如这里的
resolveData
- 调用
created
钩子,此时已经能取到所有配置项内容了 - 把其他生命周期钩子注入到实例相应的属性中,比如
instance.bm.push(beforeMount)
(未展示代码)
用数组来存放 created
之后的生命周期钩子,是因为 mixin 和 extends 的父类中可能也有配置这些钩子函数
function resolveData(
instance: ComponentInternalInstance,
dataFn: DataFn,
publicThis: ComponentPublicInstance
) {
const data = dataFn.call(publicThis, publicThis)
if (!isObject(data)) {
__DEV__ && warn(`data() should return an object.`)
} else if (instance.data === EMPTY_OBJ) {
instance.data = reactive(data)
} else {
// existing data: this is a mixin or extends.
extend(instance.data, data)
}
}
这里的 dataFn
就是用户配置的 data
选项,resolveData
会先调用 dataFn
函数获得返回对象(如果 dataFn
不是函数会输告警,未贴代码),再判断是否 data
来源是 mixin/extends ,如果是则表示已经是响应式对象直接合并到组件实例的 data
属性中,如果不是则先调用 reactive
使其变成响应式对象再合并。
选项处理流程都是一致的,其他选项如有疑问,相信也能很快找到原因,代码位置 componentOptions.ts 。
setupRenderEffect
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {...}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
函数 setupRenderEffect
内部给组件实例提供了一个 update
方法,该方法由 effect
函数返回。
function isEffect(fn: any): fn is ReactiveEffect {
return fn && fn._isEffect === true
}
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
函数 effect
职责:
- 判断入参
fn
(也就是调用时传入的componentEffect
)是否已经被effect
处理过了,如果是则返回处理前的函数fn.raw
。 - 调用
createReactiveEffect
函数获得代理函数,其内部在调用componentEffect
前进行一些记录trackStack
记录一个布尔值,暂时不知道这里追踪的是什么东西effectStack
记录在执行中的effect
函数栈,渲染完成便出栈activeEffect
记录最新在执行的effect
函数
- 执行代理函数
effect
所以组件实例的更新函数,其实就是 createReactiveEffect
返回的代理函数 effect
。
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
可以看到 createReactiveEffect
返回的代理函数会记录一些信息外,还会给其添加一些属性:
id
自增标识allowRecurse
是否允许递归_isEffect
是否被副作用函数处理(代理)过active
是否停止函数的副作用raw
原始函数deps
收集当前组件跟踪的其他副作用函数options
副作用选项
接下来我们看 fn()
执行的原始函数 componentEffect
,也是实际生效的挂载和更新函数。
function componentEffect() {
if (!instance.isMounted) {
...
const { bm, m, parent } = instance
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
...
// 获取组件 VNode
const subTree = (instance.subTree = renderComponentRoot(instance))
...
// patch
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
...
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
...
instance.isMounted = true
} else {
// 更新逻辑
...
}
可以看到,函数 componentEffect
挂载部分的逻辑如下:
- 调用
beforeMount
的钩子 - 调用
renderComponentRoot
获取组件 VNode (里边会调用组件的render
函数) - 调用
patch
函数 - 调用
mount
钩子
当前组件通过 renderComponentRoot
取得的 VNode 如下:
{
children: "Hello Vue!!",
patchFlag: 0,
shapeFlag: 8,
type: Symbol(Text),
__v_isVNode: true,
...
}
然后我们继续看 patch
函数:
...
switch (type) {
case Text:
processText(n1, n2, container, anchor)
...
由于 VNode 的 type
属性为 Symbol(Text)
,所以会调用一开始说的 processText
函数处理文本节点。
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
函数 processText
中的 hostCreateText
其实就是 document.createTextNode
方法,hostInsert
内部调用的也是 document.insertBefore
,代码如下:
const doc = (typeof document !== 'undefined' ? document : null) as Document
...
export {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
...
createText: text => doc.createTextNode(text),
}
至此 Hello Vue!!
就被渲染为 HTML ,在网页上展示了,紧接着就会调用 mounted
生命周期钩子(当然调用之前会判断是否为 Suspense 组件)。
总结
对 Vue3 来说,渲染 Hello World 组件经过了几个主要阶段:
- 创建渲染器
createRenderer
- 创建应用
createApp
- 创建虚拟节点
_createVNode
- 执行渲染
render
我们在 Vue2 中常提及的 patch
过程,就处于 Vue3 的渲染阶段。
渲染分发函数 patch
不处理实际逻辑,只是针对不同的节点类型分配对应的处理函数,由于节点树的原因,它会在内部形成递归。