我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
这篇文章先从整体视角了解一下渲染器。
渲染器的作用是将 VNode 渲染到页面上,具体操作包括挂载和更新。第一次渲染的时候就是挂载操作,挂载只需要创建新的元素并将元素挂载到页面上即可。下次渲染的时候,由于页面上已经有真实 DOM 了,所以下次渲染是更新操作,更新操作需要细致的比较新老 VNode,然后对页面上的真实 DOM 进行最小量的更新。
首先看下自定义渲染器 API
自定义渲染器 API 的官方文档点击这里。
在 Vue2 中,如果我们想将 Vue 迁移到其他平台的话,必须完整的下载整个 Vue 的源码,然后进行源码层次的改写,这非常的麻烦。在 Vue3 中,官方提供了专门的 API 用于创建特定平台的渲染器,这在我们将 Vue 迁移到其他平台时可以避免对 Vue 源码进行更改,我们只需要写特定平台的代码,然后将这些代码和 Vue 的源码进行有机的结合即可。接下来看 createRenderer 的源码:
- // 创建一个渲染器
- export function createRenderer<
- HostNode = RendererNode,
- HostElement = RendererElement
- >(options: RendererOptions<HostNode, HostElement>) {
- // 创建渲染器使用的是 baseCreateRenderer 函数创建的
- return baseCreateRenderer<HostNode, HostElement>(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 算法我都会写。