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(四)
相关推荐
秦jh_12 分钟前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑21325 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy26 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与2 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun2 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss