本来这一章节的标题应该是“组件的渲染”,但是想来想去感觉渲染这个名词可能有歧义,因此还是换成更具体的说法。从组件模板到实际 DOM 可以分为下面几步
- 编译模板产生渲染函数
- 渲染函数调用产生 vnode
- 挂载 vnode,生成相应的 DOM 树
编译模板这个事情这章节不讨论了,要涉及到一些编译原理的知识,主要看二三步。我们依次讲解
vnode
虚拟节点(vnode)是用来描述 DOM 节点的 JavaScript 对象,简化后例子如下:
javascript
const vnode = {
type: "div",
props: {
class: "btn",
style: {
width: "100px",
height: "50px",
},
},
children: "hello",
};
这个 vnode 就对应着 <div class="btn" style="width: 100px; height: 50px">hello</div>
这个 DOM 节点。通过调用挂载函数就可以将他变成实际的 DOM 节点。但实际上一个 vnode 有更多更复杂的属性,更详细的字段信息,可以看 vue 有关 vnode 的源码。
除了可以表示一个普通元素,vnode 还可以描述组件,比如下面这个 vnode,就对应着 <custom-component msg="test" />
这个组件。除了普通节点和组件节点以外,还有其他一些节点类型,比方说文本节点,注释节点,更详细的节点类型可以看 Vue 节点 type 类型注释
javascript
const CustomComponent = {};
const vnode = {
type: CustomComponent,
props: {
msg: "test",
},
};
通过 vnode,可以将渲染过程抽象,从而更好的实现跨平台的特性。同时通过批量操作,还可以尽可能的减少 DOM 相关操作的耗时
TIP
这里虽然提到了可以减少相关操作耗时,但是声明式的范式操作速度终究比不上命令式操作范式,霍春阳老师在他的书中有讲过
创建 vnode
因为 vnode 大致可以分为普通节点和组件,因此相应的创建函数也有两个 createBaseVNode
和 createVNode
,前者创建一个普通节点的 vnode,后者创建组件或其他类型的 vnode
createBaseVNode
TypeScript
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag: number = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false,
): VNode {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance,
} as VNode
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// track vnode for block tree
if (
isBlockTreeEnabled > 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.
(vnode.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.
vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) {
currentBlock.push(vnode)
}
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}
这段代码看着内容很多,实际上做的事情还是比较少的
创建一个 vnode 实例,用传入的函数参数初始化实例字段
通过一个 elif 分支来对子节点做一些标准化或者标记的操作(shapeFlag)
NOTE
shapeFlag
是一个位标志(bitmap),同时编码了两种关键信息:- VNode 自身的类型:
ELEMENT
:HTML 元素FUNCTIONAL_COMPONENT
:函数式组件STATEFUL_COMPONENT
:有状态组件TEXT
:文本节点FRAGMENT
:Fragment 片段TELEPORT
:Teleport 传送门SUSPENSE
:Suspense 异步包装器
- 子节点的类型(通过位运算添加):
TEXT_CHILDREN
:子节点是文本ARRAY_CHILDREN
:子节点是数组SLOTS_CHILDREN
:子节点是插槽
使用 shapeFlag 主要原因是为了性能优化,位运算相比类型校验耗时较少(这个真的会有性能优化的效果吗?有没有 benchmark?)
- VNode 自身的类型:
在开发模式下,如果 vnode 的 key 是 NAN,就爆出警告(这里利用了
NAN !== NAN
)第三个分支做了 block tree 优化,后续再讲
最终做一些做一些兼容性处理(如果编译选项指定了的话),并返回 vnode
createVNode
TypeScript
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)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag = PatchFlags.BAIL
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 2.x async/functional component compat
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
// class & style normalization.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
props = guardReactiveProps(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 that 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,
)
}
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true,
)
}
这个函数有更多的判断逻辑
- 如果 type 为空或者为
NULL_DYNAMIC_COMPONENT
,则将这个节点类型设置为注释节点 - 如果传入的 type 已经是 vnode,则克隆一份
- 处理类组件和兼容性问题
- 处理 props,比如保护响应式 props,标准化 class,处理 style 等
- 确定 shapeFlag
- 防止组件是一个响应式对象,导致运行时性能消耗
- 创建基础 vnode
使用
并非遗憾,你不用亲自使用这两个函数,前面我们提到 Vue 会将模板编译成渲染函数,这两个函数就会在渲染函数中实际调用,比如下面这个模板
vue
<template>
<div>
<p>hello world</p>
<custom-component></custom-component>
</div>
</template>
会被编译成
javascript
import {
createElementVNode as _createElementVNode,
resolveComponent as _resolveComponent,
createVNode as _createVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_custom_component = _resolveComponent("custom-component");
return (
_openBlock(),
_createElementBlock("template", null, [
_createElementVNode("div", null, [
_createElementVNode("p", null, "hello world"),
_createVNode(_component_custom_component),
]),
])
);
}
组件的挂载
这一小节的内容全都是 runtime-core
中的代码,准确来说都是 runtime-core/src/renderer.ts
中 createBaseVNode
函数的代码,主要用来创建一个 Vue 渲染器,并且我们前面提到,runtime-core
中的渲染器是与平台无关的,因此这里利用的都是抽象后的 DOM 操作函数
渲染组件为 subTree
组件挂载的函数是 mountComponent
,大致流程如下:
typescript
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense,
));
setupComponent(instance);
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized,
);
};
可以看到主要做了如下三件事
- 先创建了一个组件实例
- 然后处理组件实例(比如设置 props,data,methods 等)
- 最后调用
setupRenderEffect
函数来设置渲染组件
我们放过第一步和第二步,主要看一下 setupRenderEffect
,
typescript
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
namespace,
);
initialVNode.el = subTree.el;
instance.isMounted = true;
} else {
// 触发更新组件逻辑
}
};
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn));
const update = (instance.update = effect.run.bind(effect));
effect.scheduler = () => queueJob(job);
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true);
update();
};
会创建一个副作用函数,当首次执行内部的 componentUpdateFn
的时候,判定为组件的初次渲染,而后续组件内部数据发生变化时,会自动重新执行 componentUpdateFn
,判定为组件的更新渲染。我们暂时只看初始渲染流程
可以看到初次渲染时调用 renderComponentRoot
来将组件渲染成 subTree(也是 vnode,通过调用组件内部定义的 render 方法获取)。他和 initialVNode
有很大的不同,比如有下面这样两个组件 App.vue
和 CustomComponent.vue
:
vue
<!-- App.vue -->
<template>
<div>
<p>hello world</p>
<custom-component></custom-component>
</div>
</template>
编译后的 render 函数为:
javascript
import {
createElementVNode as _createElementVNode,
resolveComponent as _resolveComponent,
createVNode as _createVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_custom_component = _resolveComponent("custom-component");
return (
_openBlock(),
_createElementBlock("template", null, [
_createElementVNode("div", null, [
_createElementVNode("p", null, "hello world"),
_createVNode(_component_custom_component),
]),
])
);
}
他内部的 custom-component
模板如下:
vue
<!-- CustomComponent.vue -->
<template>
<div>custom-component</div>
</template>
编译后的 render 函数为:
javascript
import {
createElementVNode as _createElementVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("template", null, [
_createElementVNode("div", null, "custom-component"),
])
);
}
编译后的父组件中包含一个 _createVNode(_component_custom_component)
,他创建的 vnode 在后续子组件的挂载中就会被作为 initialVNode
传入。而子组件挂载时候的 subTree 就是调用他自己的 render 函数返回的 vnode。前者可以称为渲染初始化 vnode,后者称为子树 vnode
挂载 subTree
渲染组件为子树 vnode 后,就可以通过 patch 方法来挂载了
patch
方法的简化定义如下
typescript
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
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:
// 处理静态节点
break;
case Fragment:
// 处理 Fragment
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(...args);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(...args);
} else if (shapeFlag & ShapeFlags.TELEPORT) {
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
} else if (__DEV__) {
warn("Invalid VNode type:", type, `(${typeof type})`);
}
}
};
patch 函数和 setupRenderEffect
一样,也有两个功能,当传入 n1 为 null 时,判定为初次挂载,而后续 n1 不为 null 时,判定为更新挂载。我们这里只看初次挂载的流程。在 switch 语句中,针对不同的 vnode 类型进行不同的处理,比如文本节点和注释节点,还有我们最常用的普通节点和组件,我们主要看这两个
普通节点
typescript
function baseCreateRenderer(options: RendererOptions) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
insertStaticContent: hostInsertStaticContent,
} = options;
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
if (n1 == null) {
mountElement(...args);
} else {
patchElement(...args);
}
};
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
let el: RendererElement;
const { props, shapeFlag, transition, dirs } = vnode;
el = vnode.el = hostCreateElement(...args);
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(...args);
}
// props
if (props) {
for (const key in props) {
if (key !== "value" && !isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], namespace, parentComponent);
}
}
}
hostInsert(el, container, anchor);
};
}
普通节点通过 processElement
-> mountElement
这个流程进行挂载,主要还是在 mountElement
中进行的,主要做了如下几件事
- 创建 DOM 元素
- 处理 children
- 处理 props
- 挂载 DOM 元素到 container 上
在第一步相关的代码中,我们看到这里并没有直接使用 document.createElement
来创建 DOM 元素,而是通过 hostCreateElement
来创建,这是因为 Vue 为了实现跨平台,对 DOM 操作进行了封装,这个函数实际上由传入渲染器的 options 决定,比方说如果选用浏览器平台,传入的 options 就类似于下面这样
typescript
export const nodeOps: Omit<RendererOptions<Node, Element>, "patchProp"> = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
remove: (child) => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
createElement: (tag, namespace, is, props): Element => {
const el =
namespace === "svg"
? doc.createElementNS(svgNS, tag)
: namespace === "mathml"
? doc.createElementNS(mathmlNS, tag)
: is
? doc.createElement(tag, { is })
: doc.createElement(tag);
if (tag === "select" && props && props.multiple != null) {
(el as HTMLSelectElement).setAttribute("multiple", props.multiple);
}
return el;
},
createText: (text) => doc.createTextNode(text),
createComment: (text) => doc.createComment(text),
setText: (node, text) => {
node.nodeValue = text;
},
setElementText: (el, text) => {
el.textContent = text;
},
parentNode: (node) => node.parentNode as Element | null,
nextSibling: (node) => node.nextSibling,
querySelector: (selector) => doc.querySelector(selector),
setScopeId(el, id) {
el.setAttribute(id, "");
},
};
第二步代码中可以看到针对不同的 children 类型,采用不同的策略,文本节点使用 hostSetElementText
,数组节点使用 mountChildren
,这个函数会递归调用 patch
函数,来挂载子节点,之所以不使用 mountElement
,是因为 subTree 的 children 也有可能是一个组件或者其他节点类型,如果使用 mountElement
就无法处理这部分流程。这里构成了 DFS,并且可以看到,递归是在创建元素之后,挂载元素之前进行的,假设我们将组件或者元素想成一棵树,根节点的层级最高,那么我们会从高到低创建节点,然后从低到高将这些创建的节点插入到传入的容器中
typescript
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
optimized,
start = 0,
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]));
patch(...args);
}
};
第三步和第四步都与前面有些重复,因此这里不做讲解
综上所述,普通节点挂载代码可以精简成下面的样子
typescript
function mountElement(vnode, container) {
const el = hostCreateElement(vnode.type);
for (const childEl of vnode.children) {
mountElement(childEl, el);
}
hostInsert(el, container);
}
组件节点
typescript
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
if (n1 == null) {
mountComponent(...args);
} else {
updateComponent(n1, n2, optimized);
}
};
mountComponent 我们在前面已经讲过了,在处理组件的时候就是通过三部曲进行操作,创建组件实例,处理组件实例,渲染组件实例
应用程序初始化
我们在使用 Vue 的时候,首先是会引入初始组件,然后使用 createApp
函数来创建 Vue app,最后利用返回的 mount
方法挂载到文档中,具体实例如下:
typescript
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
createApp(App).mount("#app");
createApp
的大致流程如下:
typescript
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args);
const { mount } = app;
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 重写 mount 方法
};
return app;
}) as CreateAppFunction<Element>;
主要做了两件事,创建 app 对象,重写 mount 方法,我们分别看一下
渲染器与创建 app
ensureRenderer
是为了创建针对某种平台的渲染器,相关简化代码如下:
typescript
// packages/runtime-dom/src/index.ts
const rendererOptions = /*@__PURE__*/ extend({ patchProp }, nodeOps);
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
);
}
上面的代码出自 packages/runtime-dom
子包,因此是用来创建浏览器平台的渲染器,他的 rendererOptions 部分配置我们前面也介绍过了,现在我们看渲染器的抽象实现
typescript
// packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement> {
return baseCreateRenderer<HostNode, HostElement>(options);
}
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions,
): any {
function render(vnode, container, namespace) {}
return {
render,
createApp: createAppAPI(render),
};
}
最终调用的是 baseCreateRenderer
函数,返回一个包含 render
和 createApp
方法的对象,其中 createApp
方法是通过调用 createAppAPI
获取的,createAppAPI
的定义如下:
typescript
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const app: App = (context.app = {
_component: rootComponent as ConcreteComponent,
_props: rootProps,
mount(rootContainer: HostElement): any {
const vnode = app._ceVNode || createVNode(rootComponent, rootProps);
render(vnode, rootContainer);
app._container = rootContainer;
return getComponentPublicInstance(vnode.component!);
},
});
return app;
};
}
我们在调用 createApp(App).mount("#app")
的时候,就会最终调用这个 createAppAPI
的返回值的 mount
方法,这里通过闭包等操作,简化了用户的输入参数(只需要挂载的位置即可),不需要传入包括 render 函数,组件对象,组件 props 等参数
重写 mount
上面代码中的 mount 方法并不能直接让用户使用,因为他是一个通用的,较为抽象的 mount 方法,我们要针对不同平台实现他们各自的 mount 方法,下面是浏览器平台的实现
typescript
export const createApp = (...args) => {
app.mount = (containerOrSelector: Element | ShadowRoot | 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
if (container.nodeType === 1) {
container.textContent = "";
}
const proxy = mount(container, false, resolveRootNamespace(container));
return proxy;
};
};
通过 normalizeContainer
来将用户传入参数规范化为一个 DOM 元素(用户可能传入一个字符串或者一个 DOM 元素),然后如果组件对象没有 render 函数或者 template 模板,就取容器的 innerHTML 作为模板,然后在挂载之前清空容器内容,最终调用之前保存的未重写的 mount 方法
执行 mount 渲染应用
未重写的 mount 函数中调用了 render
函数来渲染应用,这个 render
函数是在 baseCreateRenderer
中定义,并通过闭包传送进来的,他的定义如下
typescript
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions,
): any {
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
// 销毁组件
} else {
// 创建或者更新组件
patch(container._vnode || null, vnode, container);
}
// 缓存组件,表示已渲染
container._vnode = vnode;
};
return {
render,
createApp: createAppAPI(render),
};
}
传入的 vnode 是通过 createVNode(rootComponent)
创建的,container 是通过 normalizeContainer
获取的,有了这些信息,还有缓存的 _vnode
,就可以调用 patch
函数来挂载或者更新应用了!至此,应用渲染的流程已经跑通!