这篇文章先从整体视角了解一下渲染器。
渲染器的作用是将 VNode 渲染到页面上,具体操作包括挂载和更新。第一次渲染的时候就是挂载操作,挂载只需要创建新的元素并将元素挂载到页面上即可。下次渲染的时候,由于页面上已经有真实 DOM 了,所以下次渲染是更新操作,更新操作需要细致的比较新老 VNode,然后对页面上的真实 DOM 进行最小量的更新。
首先看下自定义渲染器 API
自定义渲染器 API 的官方文档点击这里。
在 Vue2 中,如果我们想将 Vue 迁移到其他平台的话,必须完整的下载整个 Vue 的源码,然后进行源码层次的改写,这非常的麻烦。在 Vue3 中,官方提供了专门的 API 用于创建特定平台的渲染器,这在我们将 Vue 迁移到其他平台时可以避免对 Vue 源码进行更改,我们只需要写特定平台的代码,然后将这些代码和 Vue 的源码进行有机的结合即可。接下来看 createRenderer 的源码:
// 创建一个渲染器
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions) {
// 创建渲染器使用的是 baseCreateRenderer 函数创建的
return baseCreateRenderer(options)
}
createRenderer 函数的内部使用 baseCreateRenderer 创建渲染器。
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 依赖于平台的具体操作方法是从外部传递进来的,这样可以使用 createRenderer 创建出依托于不同平台的渲染器
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// 下面声明了一系列的函数,Vue 将渲染的功能拆分成了一个个小的函数,功能拆分,清晰明了
// 挂载和更新的入口函数,n1 是 oldVnode,n2 是 newVnode
const patch: PatchFn = () => { ...... }
// 用于处理元素节点的挂载和更新
const processElement = () => { ...... }
// 元素的挂载操作:创建元素 --> 添加到页面上
const mountElement = () => { ...... }
// 挂载元素子节点
const mountChildren: MountChildrenFn = () => { ...... }
// 更新元素节点
const patchElement = () => { ...... }
// 更新元素节点属性
const patchProps = () => { ...... }
// 挂载组件节点
const mountComponent: MountComponentFn = () => { ...... }
// 更新组件节点
const updateComponent = () => { ...... }
// diff 算法
const patchChildren: PatchChildrenFn = () => { ...... }
// 进行渲染的入口
// 渲染函数的参数是:最新的vnode,需要渲染到的容器
const render: RootRenderFunction = (vnode, container, isSVG) => {
// 如果 vnode 是 null 的话,说明有可能要进行卸载操作
if (vnode == null) {
// 上一次渲染的 vnode 会被保存到 container._vnode 中,如果 container._vnode 存在的话,这说明此时需要进行卸载操作
if (container._vnode) {
// 调用 unmount 函数进行卸载操作
unmount(container._vnode, null, null, true)
}
} else {
// 如果 vnode 不等于 null 的话,则说明此时需要进行挂载或者更新操作,具体是挂载还是更新操作要看 oldVnode 是否存在,其被保存在 container._vnode 中
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
// 将当前进行处理的 vnode 赋值到 container 上,下次渲染的时候,container._vnode 就是 oldVnode 了
container._vnode = vnode
}
// 最后返回渲染器,渲染器就是一个对象,一个渲染器和一个平台是对应的,这里主要看 render 函数是如何进行挂载和渲染的。
return {
render
}
}
baseCreateRenderer 函数首先从 options 对象中获取到针对特定平台的底层操作函数,这些函数我们可以根据想要迁移的平台进行自定义。
然后,baseCreateRenderer 函数开始声明一系列的功能函数,Vue 将渲染的这个大功能拆分到一个个函数中,每个函数实现具体的小功能。
最后 return 出去一个对象,这个对象就是创建出来的渲染器,渲染器上有一个 render 属性,这个属性是一个函数,它是进行渲染的入口。
这一小节,主要是看自定义渲染器是如何实现功能的,接下来看看几个比较重要的功能函数。
// 进行渲染的入口
// 渲染函数的参数是:最新的vnode,需要渲染到的容器
const render: RootRenderFunction = (vnode, container, isSVG) => {
// 如果 vnode 是 null 的话,说明有可能要进行卸载操作
if (vnode == null) {
// 上一次渲染的 vnode 会被保存到 container._vnode 中,如果 container._vnode 存在的话,这说明此时需要进行卸载操作
if (container._vnode) {
// 调用 unmount 函数进行卸载操作
unmount(container._vnode, null, null, true)
}
} else {
// 如果 vnode 不等于 null 的话,则说明此时需要进行挂载或者更新操作,具体是挂载还是更新操作要看 oldVnode 是否存在,其被保存在 container._vnode 中
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
// 将当前进行处理的 vnode 赋值到 container 上,下次渲染的时候,container._vnode 就是 oldVnode 了
container._vnode = vnode
}
源码解释在注释中,看注释即可,写的很详细,接下来看 patch。
// 下面声明了一系列的函数,Vue 将渲染的功能拆分成了一个个小的函数,功能拆分,清晰明了
// 挂载和更新的入口函数,n1 是 oldVnode,n2 是 newVnode
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// vnode 都是对象,对象是引用类型的值,如果 n1 === n2 的话,
// 则说明指向的是同一个对象,此时新老 vnode 完全相同,直接 return 即可
if (n1 === n2) {
return
}
// 如果 n1 和 n2 是不同类型 vnode 的话,需要将上一次渲染的内容全部卸载掉,
// 然后将 n1 设为 null,这样下面的操作就完全是挂载操作了
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 解构获取 n2 的 type、ref、shapeFlag
const { type, ref, shapeFlag } = n2
// 根据 n2 Vnode 的类型进行不同的处理
switch (type) {
// 如果当前的 vnode 是文本节点的话,使用 processText 进行处理
case Text:
processText(n1, n2, container, anchor)
break
// 如果当前的 vnode 是注释节点的话,使用 processCommentNode 进行处理
case Comment:
processCommentNode(n1, n2, container, anchor)
break
// 如果当前的 vnode 是静态节点的话,调用 mountStaticNode 和 patchStaticNode 进行处理
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
// 如果节点是 Fragment 的话,使用 processFragment 进行处理
// Fragment 节点的作用是使组件拥有多个根节点
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理元素节点
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件节点
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 TELEPORT 节点, 是 Vue3 中的一个新增内置组件
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 SUSPENSE 节点, 是 Vue3 中的一个新增内置组件
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
// 如果以上条件都不满足,并且是在开发模式下的话,则打印出相关警告:违法的 vnode 类型
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
}
patch 是挂载和更新的入口,n1 和 n2 分别是 oldVNode 和 newVNode,函数首先判断 n1 和 n2 是否相等,如果相等的话,不用进行挂载和更新操作,直接 return 即可。
接下来判断 n1 和 n2 是不是相同的节点,如果不相同的话,卸载 n1, 并将 n1 置为空,接下来的操作,因为 n1 为空,所以进行的是挂载操作。
最后,根据 n2 VNode 的类型调用对应的功能函数进行处理,这里以元素节点为例,接下来看 processElement 方法。
// 用于处理元素节点的挂载和更新
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
// 如果 n1 为 null 的话,说明此时是初次挂载,调用 mountElement 进行处理。
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 如果 n1 不为 null 的话,则说明是更新操作,调用 patchElement 进行处理。
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
processElement 函数用于处理元素节点的挂载和更新,当 n1 为 null 的时候,说明此时是初次挂载,调用 mountElement 进行处理,否则的话,说明是更新操作,调用 patchElement 函数进行处理,这里以 patchElement 函数为例。
// 更新元素节点
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
let { patchFlag, dynamicChildren, dirs } = n2
// #1426 take the old vnode's patch flag into account since user may clone a
// compiler-generated vnode, which de-opts to FULL_PROPS
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
let vnodeHook: VNodeHook | undefined | null
// disable recurse in beforeUpdate hooks
parentComponent && toggleRecurse(parentComponent, false)
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
parentComponent && toggleRecurse(parentComponent, true)
if (__DEV__ && isHmrUpdating) {
// HMR updated, force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// dynamicChildren 是一种更新子节点的优化操作
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if (!optimized) {
// 完整的 diff 算法,更新子节点
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
// 更新子节点完成后,进行当前节点的更新操作
// patchFlag 用于标记当前的节点有哪些动态内容,如果知道当前节点有哪些动态内容的话,直接更新动态内容即可
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
// 元素节点的 key 是动态的,此时进行属性的全部更新操作
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// 指定更新 class 即可
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// 指定更新 style 即可
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
// props
// This flag is matched when the element has dynamic prop/attr bindings
// other than class and style. The keys of dynamic prop/attrs are saved for
// faster iteration.
// Note dynamic keys like :[foo]="bar" will cause this optimization to
// bail out and go through a full diff because we need to unset the old key
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// 进行属性的全部更新操作
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
patchElement 函数主要做了两件事,分别是更新当前元素的子节点以及更新当前的元素节点,更新当前元素的子节点内容就是常说的 diff 算法,这个算法我们在后面的文章中细说,更新当前的元素节点具体是指更新元素节点上面的一系列属性。
这篇文章主要是从一个整体的视角介绍一下渲染器的工作流程,让大家有了整体的感知。我们可以发现,渲染器的代码量是非常多的,Vue 中的许多功能也是依托于渲染器实现的,所以不可能在一片博客中对渲染器进行全面的解读。接下来,当讲解到具体的功能时,如果这个功能的实现依托于渲染器,我会着重对渲染器中对应的代码进行细致解读。
下一篇博客的内容是渲染器中一个很重要的知识点 —— diff 算法,Vue2 和 Vue3 中的 diff 算法我都会写。