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

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

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 节点,<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 节点,<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 算法我都会写。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我来回答你的问题。 Vue 组件 object_vue 源码系列一是关于 Object.defineProperty 的。Object.defineProperty 是 JavaScript 中的一个方法,可以用来定义对象的属性。这个方法可以让我们定义一个新的属性或者修改一个已经存在的属性。这个方法的语法如下: ```javascript Object.defineProperty(obj, prop, descriptor) ``` 其中,obj 是要定义属性的对象,prop 是要定义或修改的属性名,descriptor 是属性的描述符,它是一个对象,可以包含以下属性: - value:属性的值,默认为 undefined。 - writable:属性是否可写,默认为 false。 - enumerable:属性是否可枚举,默认为 false。 - configurable:属性是否可配置,默认为 false。 使用 Object.defineProperty 方法,可以实现一些高级的对象操作,例如: 1. 将一个属性设置为只读,即无法修改。 2. 将一个属性设置为不可枚举,即无法通过 for...in 循环遍历到该属性。 3. 将一个属性设置为不可配置,即无法删除该属性或者修改该属性的描述符。 在 Vue 中,Object.defineProperty 方法被广泛地应用于组件的实现中,例如: 1. 监听数据变化,通过设置 getter 和 setter 方法,实现数据的响应式更新。 2. 实现 computed 计算属性,通过设置 getter 方法,实现计算属性的缓存和响应式更新。 3. 实现 watch 监听器,通过设置 getter 方法,监听数据的变化并触发回调函数。 以上就是我对你提出的问题的回答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值