Vue3源码解析之 render(四)

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 11 篇,关注专栏

前言

上篇我们分析了 render 函数对 HTML 标签属性、 DOM 属性、Style 样式、Event 事件的挂载更新,那么对于 Vue 特殊的 DOM 类型,比如 TextCommentFragment 类型是如何渲染更新的呢?下面我们就来逐一分析。

案例一

首先引入 hrender 函数和 Text 类型,先渲染类型为 Textvnode1 元素,两秒后修改子节点内容,渲染相同类型的 vnode2 元素。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render, Text } = Vue

      const vnode1 = h(Text, 'hello world')

      render(vnode1, document.querySelector('#app'))

      setTimeout(() => {
        const vnode2 = h(Text, '你好世界')

        render(vnode2, document.querySelector('#app'))
      }, 2000)
    </script>
  </body>
</html>

render Text 类型

我们知道 render 函数的渲染主要执行了 patch 方法:

ts 复制代码
const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 容器
    anchor = null, // 锚点
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 省略

    const { type, ref, shapeFlag } = n2
    // 根据 新节点类型 判断
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
       // 省略
      default:
       // 省略
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

当前 typeText 类型即 Symbol(Text)

接着执行 processText 方法:

ts 复制代码
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }

可以看出该方法先创建一个 Text 节点然后通过 hostInsert 方法插入到页面中,我们再看下 hostCreateText 方法,实际执行的是 createText,它被定义在 packages/runtime-dom/src/nodeOps.ts 文件中:

ts 复制代码
createText: text => doc.createTextNode(text)

之后页面呈现:

两秒后更新节点,再次执行 processText 方法,我们主要关注下面这段逻辑:

ts 复制代码
  const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      // 省略
    } else {
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }

由于当前新旧子节点不同,执行 hostSetText 方法,实际执行的是 setText,它被定义在 packages/runtime-dom/src/nodeOps.ts 文件中:

ts 复制代码
setText: (node, text) => {
    node.nodeValue = text
}

重新赋值新子节点后,页面呈现:

最后再将新节点赋值给旧节点 _vnoderender 函数执行完成。

案例二

首先引入 hrender 函数和 Comment 类型,然后通过 render 函数渲染类型为 Commentvnode 元素。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render, Comment } = Vue

      const vnode = h(Comment, 'hello world')

      render(vnode, document.querySelector('#app'))
    </script>
  </body>
</html>

render Comment 类型

由于注释节点不存在更新的问题,所以我们重新再看下 patch 方法:

ts 复制代码
const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 容器
    anchor = null, // 锚点
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 省略
    
    const { type, ref, shapeFlag } = n2
    // 根据 新节点类型 判断
    switch (type) {
      // 省略
      
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
        
      // 省略
      
      default:
        // 省略
    }

    // 省略
  }

当前 Type 类型为 CommentSymbol(Comment),执行 processCommentNode 方法:

ts 复制代码
const processCommentNode: ProcessTextOrCommentFn = (
    n1,
    n2,
    container,
    anchor
  ) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateComment((n2.children as string) || '')),
        container,
        anchor
      )
    } else {
      // there's no support for dynamic comments
      n2.el = n1.el
    }
  }

该方法同 processText 相似,通过创建一个 Comment 节点然后插入到页面中,我们再来看下 hostCreateComment 方法,实际执行的是 createComment,它被定义在 packages/runtime-dom/src/nodeOps.ts 文件中:

ts 复制代码
createComment: text => doc.createComment(text)

之后页面呈现:

案例三

首先引入 hrender 函数和 Fragment 类型,先渲染类型为 Fragmentvnode1 元素,两秒后修改子节点内容,渲染相同类型的 vnode2 元素。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render, Fragment } = Vue

      const vnode1 = h(Fragment, ['hello', ' world'])

      render(vnode1, document.querySelector('#app'))

      setTimeout(() => {
        const vnode2 = h(Fragment, ['你好', ' 世界'])

        render(vnode2, document.querySelector('#app'))
      }, 2000)
    </script>
  </body>
</html>

render Fragment 类型

我们继续看下 patch 方法:

ts 复制代码
const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 容器
    anchor = null, // 锚点
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 省略

    const { type, ref, shapeFlag } = n2
    // 根据 新节点类型 判断
    switch (type) {
      // 省略
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
       // 省略
    }

    // 省略
  }

当前 Type 类型为 FragmentSymbol(Fragment),执行 processFragment 方法:

ts 复制代码
const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    // 省略

    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // a fragment can only have array children
      // since they are either generated by the compiler, or implicitly created
      // from arrays.
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren &&
        // #2715 the previous fragment could've been a BAILed one as a result
        // of renderSlot() with no valid children
        n1.dynamicChildren
      ) {
        // 省略
      } else {
        // 省略
      }
    }
  }

根据判断逻辑,由于初次渲染旧节点 n1 不存在,之后执行 mountChildren 方法:

ts 复制代码
const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized,
    start = 0
  ) => {
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

需要注意的是这里 children 是数组类型即 案例三 传入的 ['hello', ' world'] 。之后遍历数组,先取第一个元素执行 normalizeVNode(children[i]))normalizeVNode('hello'),我们再看下 normalizeVNode 方法:

ts 复制代码
export function normalizeVNode(child: VNodeChild): VNode {
  if (child == null || typeof child === 'boolean') {
    // empty placeholder
    return createVNode(Comment)
  } else if (isArray(child)) {
    // fragment
    return createVNode(
      Fragment,
      null,
      // #3666, avoid reference pollution when reusing vnode
      child.slice()
    )
  } else if (typeof child === 'object') {
    // already vnode, this should be the most common since compiled templates
    // always produce all-vnode children arrays
    return cloneIfMounted(child)
  } else {
    // strings and numbers
    return createVNode(Text, null, String(child))
  }
}

当前 child 存在且为字符串类型,最终执行 createVNode(Text, null, String(child)),该方法我们在 Vue3源码解析之 h 文章中已经讲过,最终返回的是一个 Text 类型的 虚拟 DOM,之后再通过 patch 方法渲染到页面中:

第二次执行相同的逻辑,最终渲染:

两秒后重新更新节点,再次执行 processFragment 方法:

ts 复制代码
 const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    // 省略

    if (n1 == null) {
      // 省略
    } else {
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren &&
        // #2715 the previous fragment could've been a BAILed one as a result
        // of renderSlot() with no valid children
        n1.dynamicChildren
      ) {
        // 省略
      } else {
        // keyed / unkeyed, or manual fragments.
        // for keyed & unkeyed, since they are compiler generated from v-for,
        // each child is guaranteed to be a block so the fragment will never
        // have dynamicChildren.
        patchChildren(
          n1,
          n2,
          container,
          fragmentEndAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
    }
  }

当前旧节点 n1 存在,执行 patchChildren 方法:

ts 复制代码
const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized = false
  ) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const { patchFlag, shapeFlag } = n2
    // fast path
    if (patchFlag > 0) {
      // 省略
    }

    // children has 3 possibilities: text, array or no children.
    // 新节点为 text 节点
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 省略
    } else {
      // 旧节点为 array 节点
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
        // 新节点为 array 节点
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          // two arrays, cannot assume anything, do full diff
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else {
          // no new children, just unmount old
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
        }
      } else {
        // 省略
      }
    }
  }

根据判断,当前新旧子节点都为数组类型,执行 patchKeyedChildren 方法:

ts 复制代码
const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 // prev ending index
    let e2 = l2 - 1 // next ending index

    // 1. sync from start
    // (a b) c
    // (a b) d e
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i])) // 转为 vnode
      // 新旧节点类型是否相同   
      if (isSameVNodeType(n1, n2)) {
        // 更新节点
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        break
      }
      i++
    }
    
    // 省略
  }

由于节点类型相同,之后遍历执行 patch 方法,第一次更新完显示:

第二次更新完显示:

最后将新节点赋值给旧节点 _vnoderender 函数执行完毕。

总结

  1. Text 类型的渲染更新实际执行的是 processText 方法,该方法主要通过 createText 方法来创建文本节点,通过 setText 方法来修改文本内容。
  2. Comment 类型的渲染实际执行的是 processCommentNode 方法,该方法主要通过 hostCreateComment 方法来创建注释节点。
  3. Fragment 类型的渲染更新实际执行的是 processFragment 方法,该方法主要通过 mountChildren 方法创建节点,通过 patchChildren 方法更新节点。另外子节点的类型传入必须是一个数组,通过 normalizeVNode 方法创建了一个 Text 类型的 虚拟 DOM

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
相关推荐
祝余呀7 分钟前
html初学者第一天
前端·html
速易达网络2 小时前
RuoYi、Vue CLI 和 uni-app 结合构建跨端全家桶方案
javascript·vue.js·低代码
耶啵奶膘3 小时前
uniapp+firstUI——上传视频组件fui-upload-video
前端·javascript·uni-app
视频砖家3 小时前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能
lyj1689974 小时前
vue-i18n+vscode+vue 多语言使用
前端·vue.js·vscode
小白变怪兽5 小时前
一、react18+项目初始化(vite)
前端·react.js
ai小鬼头5 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
墨菲安全6 小时前
NPM组件 betsson 等窃取主机敏感信息
前端·npm·node.js·软件供应链安全·主机信息窃取·npm组件投毒
GISer_Jing6 小时前
Monorepo+Pnpm+Turborepo
前端·javascript·ecmascript
天涯学馆6 小时前
前端开发也能用 WebAssembly?这些场景超实用!
前端·javascript·面试