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

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

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

首先看下自定义渲染器 API

1,自定义渲染器 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 属性,这个属性是一个函数,它是进行渲染的入口。

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

2,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。

3,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 方法。

4,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 函数为例。

5,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 算法,这个算法我们在后面的文章中细说,更新当前的元素节点具体是指更新元素节点上面的一系列属性。

6,结语

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

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

你可能感兴趣的:(vue3源码阅读系列,vue.js,前端,javascript)