• Vue3 源码阅读(8):渲染器 —— 总体思路


     我的开源库:

    这篇文章先从整体视角了解一下渲染器

    渲染器的作用是将 VNode 渲染到页面上,具体操作包括挂载和更新。第一次渲染的时候就是挂载操作,挂载只需要创建新的元素并将元素挂载到页面上即可。下次渲染的时候,由于页面上已经有真实 DOM 了,所以下次渲染是更新操作,更新操作需要细致的比较新老 VNode,然后对页面上的真实 DOM 进行最小量的更新。

    首先看下自定义渲染器 API

    1,自定义渲染器 API

    自定义渲染器 API 的官方文档点击这里

    在 Vue2 中,如果我们想将 Vue 迁移到其他平台的话,必须完整的下载整个 Vue 的源码,然后进行源码层次的改写,这非常的麻烦。在 Vue3 中,官方提供了专门的 API 用于创建特定平台的渲染器,这在我们将 Vue 迁移到其他平台时可以避免对 Vue 源码进行更改,我们只需要写特定平台的代码,然后将这些代码和 Vue 的源码进行有机的结合即可。接下来看 createRenderer 的源码:

    1. // 创建一个渲染器
    2. export function createRenderer<
    3. HostNode = RendererNode,
    4. HostElement = RendererElement
    5. >(options: RendererOptions<HostNode, HostElement>) {
    6. // 创建渲染器使用的是 baseCreateRenderer 函数创建的
    7. return baseCreateRenderer<HostNode, HostElement>(options)
    8. }

    createRenderer 函数的内部使用 baseCreateRenderer 创建渲染器。

    1. function baseCreateRenderer(
    2. options: RendererOptions,
    3. createHydrationFns?: typeof createHydrationFunctions
    4. ): any {
    5. // 依赖于平台的具体操作方法是从外部传递进来的,这样可以使用 createRenderer 创建出依托于不同平台的渲染器
    6. const {
    7. insert: hostInsert,
    8. remove: hostRemove,
    9. patchProp: hostPatchProp,
    10. createElement: hostCreateElement,
    11. createText: hostCreateText,
    12. createComment: hostCreateComment,
    13. setText: hostSetText,
    14. setElementText: hostSetElementText,
    15. parentNode: hostParentNode,
    16. nextSibling: hostNextSibling,
    17. setScopeId: hostSetScopeId = NOOP,
    18. cloneNode: hostCloneNode,
    19. insertStaticContent: hostInsertStaticContent
    20. } = options
    21. // 下面声明了一系列的函数,Vue 将渲染的功能拆分成了一个个小的函数,功能拆分,清晰明了
    22. // 挂载和更新的入口函数,n1 是 oldVnode,n2 是 newVnode
    23. const patch: PatchFn = () => { ...... }
    24. // 用于处理元素节点的挂载和更新
    25. const processElement = () => { ...... }
    26. // 元素的挂载操作:创建元素 --> 添加到页面上
    27. const mountElement = () => { ...... }
    28. // 挂载元素子节点
    29. const mountChildren: MountChildrenFn = () => { ...... }
    30. // 更新元素节点
    31. const patchElement = () => { ...... }
    32. // 更新元素节点属性
    33. const patchProps = () => { ...... }
    34. // 挂载组件节点
    35. const mountComponent: MountComponentFn = () => { ...... }
    36. // 更新组件节点
    37. const updateComponent = () => { ...... }
    38. // diff 算法
    39. const patchChildren: PatchChildrenFn = () => { ...... }
    40. // 进行渲染的入口
    41. // 渲染函数的参数是:最新的vnode,需要渲染到的容器
    42. const render: RootRenderFunction = (vnode, container, isSVG) => {
    43. // 如果 vnode 是 null 的话,说明有可能要进行卸载操作
    44. if (vnode == null) {
    45. // 上一次渲染的 vnode 会被保存到 container._vnode 中,如果 container._vnode 存在的话,这说明此时需要进行卸载操作
    46. if (container._vnode) {
    47. // 调用 unmount 函数进行卸载操作
    48. unmount(container._vnode, null, null, true)
    49. }
    50. } else {
    51. // 如果 vnode 不等于 null 的话,则说明此时需要进行挂载或者更新操作,具体是挂载还是更新操作要看 oldVnode 是否存在,其被保存在 container._vnode 中
    52. patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    53. }
    54. // 将当前进行处理的 vnode 赋值到 container 上,下次渲染的时候,container._vnode 就是 oldVnode 了
    55. container._vnode = vnode
    56. }
    57. // 最后返回渲染器,渲染器就是一个对象,一个渲染器和一个平台是对应的,这里主要看 render 函数是如何进行挂载和渲染的。
    58. return {
    59. render
    60. }
    61. }

    baseCreateRenderer 函数首先从 options 对象中获取到针对特定平台的底层操作函数,这些函数我们可以根据想要迁移的平台进行自定义。

    然后,baseCreateRenderer 函数开始声明一系列的功能函数,Vue 将渲染的这个大功能拆分到一个个函数中,每个函数实现具体的小功能。

    最后 return 出去一个对象,这个对象就是创建出来的渲染器,渲染器上有一个 render 属性,这个属性是一个函数,它是进行渲染的入口。

    这一小节,主要是看自定义渲染器是如何实现功能的,接下来看看几个比较重要的功能函数。

    2,render

    1. // 进行渲染的入口
    2. // 渲染函数的参数是:最新的vnode,需要渲染到的容器
    3. const render: RootRenderFunction = (vnode, container, isSVG) => {
    4. // 如果 vnode 是 null 的话,说明有可能要进行卸载操作
    5. if (vnode == null) {
    6. // 上一次渲染的 vnode 会被保存到 container._vnode 中,如果 container._vnode 存在的话,这说明此时需要进行卸载操作
    7. if (container._vnode) {
    8. // 调用 unmount 函数进行卸载操作
    9. unmount(container._vnode, null, null, true)
    10. }
    11. } else {
    12. // 如果 vnode 不等于 null 的话,则说明此时需要进行挂载或者更新操作,具体是挂载还是更新操作要看 oldVnode 是否存在,其被保存在 container._vnode 中
    13. patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    14. }
    15. // 将当前进行处理的 vnode 赋值到 container 上,下次渲染的时候,container._vnode 就是 oldVnode 了
    16. container._vnode = vnode
    17. }

    源码解释在注释中,看注释即可,写的很详细,接下来看 patch。

    3,patch

    1. // 下面声明了一系列的函数,Vue 将渲染的功能拆分成了一个个小的函数,功能拆分,清晰明了
    2. // 挂载和更新的入口函数,n1 是 oldVnode,n2 是 newVnode
    3. const patch: PatchFn = (
    4. n1,
    5. n2,
    6. container,
    7. anchor = null,
    8. parentComponent = null,
    9. parentSuspense = null,
    10. isSVG = false,
    11. slotScopeIds = null,
    12. optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
    13. ) => {
    14. // vnode 都是对象,对象是引用类型的值,如果 n1 === n2 的话,
    15. // 则说明指向的是同一个对象,此时新老 vnode 完全相同,直接 return 即可
    16. if (n1 === n2) {
    17. return
    18. }
    19. // 如果 n1 和 n2 是不同类型 vnode 的话,需要将上一次渲染的内容全部卸载掉,
    20. // 然后将 n1 设为 null,这样下面的操作就完全是挂载操作了
    21. if (n1 && !isSameVNodeType(n1, n2)) {
    22. anchor = getNextHostNode(n1)
    23. unmount(n1, parentComponent, parentSuspense, true)
    24. n1 = null
    25. }
    26. // 解构获取 n2 的 type、ref、shapeFlag
    27. const { type, ref, shapeFlag } = n2
    28. // 根据 n2 Vnode 的类型进行不同的处理
    29. switch (type) {
    30. // 如果当前的 vnode 是文本节点的话,使用 processText 进行处理
    31. case Text:
    32. processText(n1, n2, container, anchor)
    33. break
    34. // 如果当前的 vnode 是注释节点的话,使用 processCommentNode 进行处理
    35. case Comment:
    36. processCommentNode(n1, n2, container, anchor)
    37. break
    38. // 如果当前的 vnode 是静态节点的话,调用 mountStaticNode 和 patchStaticNode 进行处理
    39. case Static:
    40. if (n1 == null) {
    41. mountStaticNode(n2, container, anchor, isSVG)
    42. } else if (__DEV__) {
    43. patchStaticNode(n1, n2, container, isSVG)
    44. }
    45. break
    46. // 如果节点是 Fragment 的话,使用 processFragment 进行处理
    47. // Fragment 节点的作用是使组件拥有多个根节点
    48. case Fragment:
    49. processFragment(
    50. n1,
    51. n2,
    52. container,
    53. anchor,
    54. parentComponent,
    55. parentSuspense,
    56. isSVG,
    57. slotScopeIds,
    58. optimized
    59. )
    60. break
    61. default:
    62. if (shapeFlag & ShapeFlags.ELEMENT) {
    63. // 处理元素节点
    64. processElement(
    65. n1,
    66. n2,
    67. container,
    68. anchor,
    69. parentComponent,
    70. parentSuspense,
    71. isSVG,
    72. slotScopeIds,
    73. optimized
    74. )
    75. } else if (shapeFlag & ShapeFlags.COMPONENT) {
    76. // 处理组件节点
    77. processComponent(
    78. n1,
    79. n2,
    80. container,
    81. anchor,
    82. parentComponent,
    83. parentSuspense,
    84. isSVG,
    85. slotScopeIds,
    86. optimized
    87. )
    88. } else if (shapeFlag & ShapeFlags.TELEPORT) {
    89. // 处理 TELEPORT 节点, 是 Vue3 中的一个新增内置组件
    90. ;(type as typeof TeleportImpl).process(
    91. n1 as TeleportVNode,
    92. n2 as TeleportVNode,
    93. container,
    94. anchor,
    95. parentComponent,
    96. parentSuspense,
    97. isSVG,
    98. slotScopeIds,
    99. optimized,
    100. internals
    101. )
    102. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
    103. // 处理 SUSPENSE 节点, 是 Vue3 中的一个新增内置组件
    104. ;(type as typeof SuspenseImpl).process(
    105. n1,
    106. n2,
    107. container,
    108. anchor,
    109. parentComponent,
    110. parentSuspense,
    111. isSVG,
    112. slotScopeIds,
    113. optimized,
    114. internals
    115. )
    116. } else if (__DEV__) {
    117. // 如果以上条件都不满足,并且是在开发模式下的话,则打印出相关警告:违法的 vnode 类型
    118. warn('Invalid VNode type:', type, `(${typeof type})`)
    119. }
    120. }
    121. }

    patch 是挂载和更新的入口,n1 和 n2 分别是 oldVNode 和 newVNode,函数首先判断 n1 和 n2 是否相等,如果相等的话,不用进行挂载和更新操作,直接 return 即可。

    接下来判断 n1 和 n2 是不是相同的节点,如果不相同的话,卸载 n1, 并将 n1 置为空,接下来的操作,因为 n1 为空,所以进行的是挂载操作。

    最后,根据 n2 VNode 的类型调用对应的功能函数进行处理,这里以元素节点为例,接下来看 processElement 方法。

    4,processElement

    1. // 用于处理元素节点的挂载和更新
    2. const processElement = (
    3. n1: VNode | null,
    4. n2: VNode,
    5. container: RendererElement,
    6. anchor: RendererNode | null,
    7. parentComponent: ComponentInternalInstance | null,
    8. parentSuspense: SuspenseBoundary | null,
    9. isSVG: boolean,
    10. slotScopeIds: string[] | null,
    11. optimized: boolean
    12. ) => {
    13. isSVG = isSVG || (n2.type as string) === 'svg'
    14. // 如果 n1 为 null 的话,说明此时是初次挂载,调用 mountElement 进行处理。
    15. if (n1 == null) {
    16. mountElement(
    17. n2,
    18. container,
    19. anchor,
    20. parentComponent,
    21. parentSuspense,
    22. isSVG,
    23. slotScopeIds,
    24. optimized
    25. )
    26. } else {
    27. // 如果 n1 不为 null 的话,则说明是更新操作,调用 patchElement 进行处理。
    28. patchElement(
    29. n1,
    30. n2,
    31. parentComponent,
    32. parentSuspense,
    33. isSVG,
    34. slotScopeIds,
    35. optimized
    36. )
    37. }
    38. }

    processElement 函数用于处理元素节点的挂载和更新,当 n1 为 null 的时候,说明此时是初次挂载,调用 mountElement 进行处理,否则的话,说明是更新操作,调用 patchElement 函数进行处理,这里以 patchElement 函数为例。

    5,patchElement

    1. // 更新元素节点
    2. const patchElement = (
    3. n1: VNode,
    4. n2: VNode,
    5. parentComponent: ComponentInternalInstance | null,
    6. parentSuspense: SuspenseBoundary | null,
    7. isSVG: boolean,
    8. slotScopeIds: string[] | null,
    9. optimized: boolean
    10. ) => {
    11. const el = (n2.el = n1.el!)
    12. let { patchFlag, dynamicChildren, dirs } = n2
    13. // #1426 take the old vnode's patch flag into account since user may clone a
    14. // compiler-generated vnode, which de-opts to FULL_PROPS
    15. patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
    16. const oldProps = n1.props || EMPTY_OBJ
    17. const newProps = n2.props || EMPTY_OBJ
    18. let vnodeHook: VNodeHook | undefined | null
    19. // disable recurse in beforeUpdate hooks
    20. parentComponent && toggleRecurse(parentComponent, false)
    21. if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
    22. invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
    23. }
    24. if (dirs) {
    25. invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
    26. }
    27. parentComponent && toggleRecurse(parentComponent, true)
    28. if (__DEV__ && isHmrUpdating) {
    29. // HMR updated, force full diff
    30. patchFlag = 0
    31. optimized = false
    32. dynamicChildren = null
    33. }
    34. const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
    35. // dynamicChildren 是一种更新子节点的优化操作
    36. if (dynamicChildren) {
    37. patchBlockChildren(
    38. n1.dynamicChildren!,
    39. dynamicChildren,
    40. el,
    41. parentComponent,
    42. parentSuspense,
    43. areChildrenSVG,
    44. slotScopeIds
    45. )
    46. if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
    47. traverseStaticChildren(n1, n2)
    48. }
    49. } else if (!optimized) {
    50. // 完整的 diff 算法,更新子节点
    51. patchChildren(
    52. n1,
    53. n2,
    54. el,
    55. null,
    56. parentComponent,
    57. parentSuspense,
    58. areChildrenSVG,
    59. slotScopeIds,
    60. false
    61. )
    62. }
    63. // 更新子节点完成后,进行当前节点的更新操作
    64. // patchFlag 用于标记当前的节点有哪些动态内容,如果知道当前节点有哪些动态内容的话,直接更新动态内容即可
    65. if (patchFlag > 0) {
    66. // the presence of a patchFlag means this element's render code was
    67. // generated by the compiler and can take the fast path.
    68. // in this path old node and new node are guaranteed to have the same shape
    69. // (i.e. at the exact same position in the source template)
    70. if (patchFlag & PatchFlags.FULL_PROPS) {
    71. // element props contain dynamic keys, full diff needed
    72. // 元素节点的 key 是动态的,此时进行属性的全部更新操作
    73. patchProps(
    74. el,
    75. n2,
    76. oldProps,
    77. newProps,
    78. parentComponent,
    79. parentSuspense,
    80. isSVG
    81. )
    82. } else {
    83. // 指定更新 class 即可
    84. if (patchFlag & PatchFlags.CLASS) {
    85. if (oldProps.class !== newProps.class) {
    86. hostPatchProp(el, 'class', null, newProps.class, isSVG)
    87. }
    88. }
    89. // 指定更新 style 即可
    90. if (patchFlag & PatchFlags.STYLE) {
    91. hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
    92. }
    93. // props
    94. // This flag is matched when the element has dynamic prop/attr bindings
    95. // other than class and style. The keys of dynamic prop/attrs are saved for
    96. // faster iteration.
    97. // Note dynamic keys like :[foo]="bar" will cause this optimization to
    98. // bail out and go through a full diff because we need to unset the old key
    99. if (patchFlag & PatchFlags.PROPS) {
    100. // if the flag is present then dynamicProps must be non-null
    101. const propsToUpdate = n2.dynamicProps!
    102. for (let i = 0; i < propsToUpdate.length; i++) {
    103. const key = propsToUpdate[i]
    104. const prev = oldProps[key]
    105. const next = newProps[key]
    106. // #1471 force patch value
    107. if (next !== prev || key === 'value') {
    108. hostPatchProp(
    109. el,
    110. key,
    111. prev,
    112. next,
    113. isSVG,
    114. n1.children as VNode[],
    115. parentComponent,
    116. parentSuspense,
    117. unmountChildren
    118. )
    119. }
    120. }
    121. }
    122. }
    123. // text
    124. // This flag is matched when the element has only dynamic text children.
    125. if (patchFlag & PatchFlags.TEXT) {
    126. if (n1.children !== n2.children) {
    127. hostSetElementText(el, n2.children as string)
    128. }
    129. }
    130. } else if (!optimized && dynamicChildren == null) {
    131. // 进行属性的全部更新操作
    132. patchProps(
    133. el,
    134. n2,
    135. oldProps,
    136. newProps,
    137. parentComponent,
    138. parentSuspense,
    139. isSVG
    140. )
    141. }
    142. if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
    143. queuePostRenderEffect(() => {
    144. vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
    145. dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    146. }, parentSuspense)
    147. }
    148. }

    patchElement 函数主要做了两件事,分别是更新当前元素的子节点以及更新当前的元素节点,更新当前元素的子节点内容就是常说的 diff 算法,这个算法我们在后面的文章中细说,更新当前的元素节点具体是指更新元素节点上面的一系列属性。

    6,结语

    这篇文章主要是从一个整体的视角介绍一下渲染器的工作流程,让大家有了整体的感知。我们可以发现,渲染器的代码量是非常多的,Vue 中的许多功能也是依托于渲染器实现的,所以不可能在一片博客中对渲染器进行全面的解读。接下来,当讲解到具体的功能时,如果这个功能的实现依托于渲染器,我会着重对渲染器中对应的代码进行细致解读。

    下一篇博客的内容是渲染器中一个很重要的知识点 —— diff 算法,Vue2 和 Vue3 中的 diff 算法我都会写。

  • 相关阅读:
    Docker镜像制作
    SimpleDateFormat类的parse和format方法的线程安全问题
    SpringBoot整合RocketMQ,老鸟们都是这么玩的!
    【内存管理】从程序进入内存开始说起
    适用于物联网数据共享的区块链节点存储优化方案
    轻量应用服务器部署vue项目
    【AI领域+餐饮】| 论ChatGPT在餐饮行业的应用展望
    【Leetcode刷题Python】416. 分割等和子集
    层次分明井然有条,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang包管理机制(package)EP10
    Springboot整合Mybatis-Plus
  • 原文地址:https://blog.csdn.net/f18855666661/article/details/126409786