vue3 源码学习,实现一个 mini-vue(六):构建 h 函数,生成 VNode

简介: vue3 源码学习,实现一个 mini-vue(六):构建 h 函数,生成 VNode

前言

终于来到渲染系统啦~

vue3 渲染系统学习的第一章,我们先来处理 h 函数的构建,关于 h 函数的介绍我这里就不多讲了,具体可以查询文档 h() 以及 创建VNode

我们知道 h 函数核心是用来:创建 vnode 的。但是对于 vnode 而言,它存在很多种不同的节点类型。

查看 packages/runtime-core/src/renderer.ts 中第 354patch 方法的代码可知,Vue 总共处理了:

  1. Text:文本节点
  2. Comment:注释节点
  3. Static:静态 DOM 节点
  4. Fragment:包含多个根节点的模板被表示为一个片段 (fragment)
  5. ELEMENT: DOM 节点
  6. COMPONENT:组件
  7. TELEPORT:新的 内置组件
  8. SUSPENSE:新的 内置组件

image.png

各种不同类型的节点,而每一种类型的处理都对应着不同的 VNode

所以我们在本章中,就需要把各种类型的 VNode 构建出来(不会全部处理所有类型,只会选择比较有代表性的部分),以便,后面进行 render 渲染。

1. 构建 h 函数,处理 ELEMENT + TEXT_CHILDREN

老样子,我们从下面这段代码的调试 开始 vue3 的源码阅读

<script>
  const { h } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )

  console.log(vnode)
</script>

这段代码很简单,就是使用 h 函数 创建了一个类型为 ELEMENT 子节点为 TEXTvnode

1.1 源码阅读

  1. 我们直接跳到源码 packages/runtime-core/src/h.ts 中的第 174 行,为 h 函数增加 debugger

image.png

  1. 通过源码可知,h 函数接收三个参数:

    1. type:类型。比如当前的 div 就表示 Element 类型
    2. propsOrChildrenprops 或者 children
    3. children:子节点

    而且最终代码将会触发 createVNode 方法,createVNode 方法实际就是调用了 _createVnode 方法 我们进入 _createVNode 方法:

image.png

3、 这里 _createVNodetype 做了一些条件判断,我们的 typediv 可以先跳过接着调试:

image.png

  1. _createVNode 接着对 props 做了 classstyle 的增强,我们也可以先不管,最终得到 shapeFlag 的值为 1shapeFlag 为当前的 类型标识shapeFlag。查看 packages/shared/src/shapeFlags.ts 的代码,根据 enum ShapeFlags 可知:1 代表为 Element

即当前 shapeFlag = ShapeFlags.Element,代码继续执行:

image.png

  1. 可以看到 _craeteVNode 最终是调用了 createBaseVNode 方法,我们进入到 createBaseVNode 方法:

image.png

  1. createBaseVnode 方法首先创建了一个 vnode,此时的 vnode 为上图右侧所示。我们做些简化,剔除对我们无用的属性之后,得到:
children: "hello render
props: {class: 'test'}
shapeFlag: 1 // 表示为 Element
type: "div"
__v_isVNode: true

createBaseVnode 中继续执行代码,会进入到 normalizeChildren 的方法中:

image.png

  1. normalizeChildren 的方法中,会执行最后的 else 以及一个 按位或赋值运算 最后得到 shapeFlag 的最终值为 9
  2. normalizeChildren 方法 结束, craeteBaseVNode 返回 vnode
  3. 至此,整个 h 函数执行完成,最终得到的打印有效值为:
children: "hello render
props: {class: 'test'}
shapeFlag: 9 // 表示为 Element | ShapeFlags.TEXT_CHILDREN 的值
type: "div"
__v_isVNode: true

总结:

  1. h 函数内部本质上只处理了参数的问题
  2. createVNode 是生成 vnode 的核心方法
  3. createVNode 中第一次生成了 shapeFlag = ShapeFlags.ELEMENT,表示为:是一个 Element 类型
  4. createBaseVNode 中,生成了 vnode 对象,并且对 shapeFlag 的进行 |= 运算,最终得到的 shapeFlag = 9,表示为:元素为 ShapeFlags.ELEMENTchildrenTEXT

1.2 代码实现

  1. 创建 packages/shared/src/shapeFlags.ts ,写入所有的对应类型:
export const enum ShapeFlags {
  /**
   * type = Element
   */
  ELEMENT = 1,
  /**
   * 函数组件
   */
  FUNCTIONAL_COMPONENT = 1 << 1,
  /**
   * 有状态(响应数据)组件
   */
  STATEFUL_COMPONENT = 1 << 2,
  /**
   * children = Text
   */
  TEXT_CHILDREN = 1 << 3,
  /**
   * children = Array
   */
  ARRAY_CHILDREN = 1 << 4,
  /**
   * children = slot
   */
  SLOTS_CHILDREN = 1 << 5,
  /**
   * 组件:有状态(响应数据)组件 | 函数组件
   */
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
  1. 创建 packages/runtime-core/src/h.ts ,构建 h 函数:
import { isArray, isObject } from '@vue/shared'
import { createVNode, isVNode, VNode } from './vnode'

export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  // 获取用户传递的参数数量
  const l = arguments.length
  // 如果用户只传递了两个参数,那么证明第二个参数可能是 props , 也可能是 children
  if (l === 2) {
    // 如果 第二个参数是对象,但不是数组。则第二个参数只有两种可能性:1. VNode 2.普通的 props
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 如果是 VNode,则 第二个参数代表了 children
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // 如果不是 VNode, 则第二个参数代表了 props
      return createVNode(type, propsOrChildren, [])
    }
    // 如果第二个参数不是单纯的 object,则 第二个参数代表了 props
    else {
      return createVNode(type, null, propsOrChildren)
    }
  }
  // 如果用户传递了三个或以上的参数,那么证明第二个参数一定代表了 props
  else {
    // 如果参数在三个以上,则从第二个参数开始,把后续所有参数都作为 children
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    }
    // 如果传递的参数只有三个,则 children 是单纯的 children
    else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    // 触发 createVNode 方法,创建 VNode 实例
    return createVNode(type, propsOrChildren, children)
  }
}
  1. 创建 packages/runtime-core/src/vnode.ts,处理 VNode 类型和 isVNode 函数:
export interface VNode {
  __v_isVNode: true
  type: any
  props: any
  children: any
  shapeFlag: number
}

export function isVNode(value: any): value is VNode {
  return value ? value.__v_isVNode === true : false
}
  1. packages/runtime-core/src/vnode.ts 中,构建 createVNode 函数:
   /**
    * 生成一个 VNode 对象,并返回
    * @param type vnode.type
    * @param props 标签属性或自定义属性
    * @param children 子节点
    * @returns vnode 对象
    */
   export function createVNode(type, props, children): VNode {
       // 通过 bit 位处理 shapeFlag 类型
       const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0
   
       return createBaseVNode(type, props, children, shapeFlag)
   }
   
   /**
    * 构建基础 vnode
    */
   function createBaseVNode(type, props, children, shapeFlag) {
       const vnode = {
           __v_isVNode: true,
           type,
           props,
           shapeFlag
       } as VNode
   
       normalizeChildren(vnode, children)
   
       return vnode
   }
   
   export function normalizeChildren(vnode: VNode, children: unknown) {
       let type = 0
       const { shapeFlag } = vnode
       if (children == null) {
           children = null
       } else if (isArray(children)) {
           // TODO: array
       } else if (typeof children === 'object') {
           // TODO: object
       } else if (isFunction(children)) {
           // TODO: function
       } else {
           // children 为 string
           children = String(children)
           // 为 type 指定 Flags
           type = ShapeFlags.TEXT_CHILDREN
       }
       // 修改 vnode 的 chidlren
       vnode.children = children
       // 按位或赋值
       vnode.shapeFlag |= type
   }
  1. index 中导出 h 函数
  2. 下面我们可以创建对应的测试实例,packages/vue/examples/runtime/h-element.html
<script>
  const { h } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )

  console.log(vnode)
</script>

最终打印的结果为:

children: "hello render"
props: {class: 'test'}
shapeFlag: 9
type: "div"
__v_isVNode: true

至此,我们就已经构建好了:type = Elementchildren = TextVNode 对象

2. 构建 h 函数,处理 ELEMENT + ARRAY_CHILDREN

将测试用例改为下面的代码:

<script>
  const { h } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    [h('p', 'p1'), h('p', 'p2'), h('p', 'p3')]
  )

  console.log(vnode)
</script>

我们很容易能看出上面的代码执行了四次 h 函数,分别为:

  1. h('p', 'p1')
  2. h('p', 'p2')
  3. h('p', 'p3')
  4. 以及最外层的 h(...)

前三次触发代码的流程和第一个节中相似,我们直接将代码 debugger 到第四次 h 函数

2.1 源码阅读

  1. 此时进入到 _createVNode 时的参数为:

image.png

  1. 代码继续,计算 shapeFlag = 1(与第一节一样)
  2. _createVNode 返回一个 createBaseVNode 方法, 进入 createBaseVNode
  3. createBaseVNode 创建 vnode, 接着执行 normalizeChildren(vnode, children)

image.png

  1. normalizeChildren 我们之前跟踪过得,这次 vnode.shapeFlag 计算出来是 17
  2. 我们最终将不重要的属性剔除,打印出的 vnode 结构为:
{
  "__v_isVNode": true,
  "type": "div",
  "props": { "class": "test" },
  "children": [
    {
      "__v_isVNode": true,
      "type": "p",
      "children": "p1",
      "shapeFlag": 9
    },
    {
      "__v_isVNode": true,
      "type": "p",
      "children": "p2",
      "shapeFlag": 9
    },
    {
      "__v_isVNode": true,
      "type": "p",
      "children": "p3",
      "shapeFlag": 9
    }
  ],
  "shapeFlag": 17
}

总结处理 ELEMENT + ARRAY_CHILDREN 的过程

  1. 整体的逻辑并没有变得复杂
  2. 第一次计算 shapeFlag,依然为 Element
  3. 第二次计算 shapeFlag,因为 childrenArray,所以会进入 else if (array) 判断

2.2 代码实现

根据上一小节的源码阅读可知,ELEMENT + ARRAY_CHILDREN 场景下的处理,我们只需要在 packages/runtime-core/src/vnode.ts 中,处理 isArray 场景即可:

  1. packages/runtime-core/src/vnode.ts 中,找到 normalizeChildren 方法:
 else if (isArray(children)) {
    // TODO: array
    + type = ShapeFlags.ARRAY_CHILDREN
  }
  1. 创建测试实例 packages/vue/examples/runtime/h-element-ArrayChildren.html
<script>
  const { h } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    [h('p', 'p1'), h('p', 'p2'), h('p', 'p3')]
  )

  console.log(vnode)
</script>

2.3 总结

到现在我们可以先做一个局部的总结。

对于 vnode 而言,我们现在已经知道,它存在一个 shapeFlag 属性,该属性表示了当前 VNode“类型” ,这是一个非常关键的属性,在后面的 render 函数中,还会再次看到它。

shapeFlag 分成两部分:

  1. createVNode:此处计算 “DOM” 类型,比如 Element
  2. createBaseVNode:此处计算 “children” 类型,比如 Text || Array

3. 构建 h 函数,处理组件

组件是 vue 中非常重要的一个概念,这一小节我们就来分析一下 组件 生成 VNode 的情况。

vue 中,组件本质上就是 一个对象或一个函数Function Component

我们这里 不考虑 组件是函数的情况,因为这个比较少见。

vue3 中,我们可以直接利用 h 函数 + render 函数渲染出一个基本的组件,就像下面这样:

<script>
  const { h, render } = Vue

  const component = {
    render() {
      const vnode1 = h('div', '这是一个 component')
      console.log(vnode1)
      return vnode1
    }
  }
  const vnode2 = h(component)
  console.log(vnode2)
  render(vnode2, document.querySelector('#app'))
</script>

3.1 案例分析

  1. 在当前代码中共触发了两次 h 函数,
  2. 第一次是在 component 对象中的 render 函数内,我们可以把 component 对象看成一个组件,实际上在 vue3 中你打印一个组件对象它的内部就有一个 render 函数,下面是我打印的一个 App 组件

image.png

  1. 第二次是在将 component 作为参数生成的 vnode2
  2. 最后将生成的 vnode2 通过 render 渲染函数 渲染到页面上(关于 render 函数我们之后在讲)
  3. 最终打印的 vnode2 如下图所示:

image.png

  • shapeFlag:这个是当前的类型表示,4 表示为一个 组件
  • type:是一个 对象,它的值包含了一个 render 函数,这个就是 component 的 真实渲染 内容
  • __v_isVNodeVNode 标记
  1. vnode1:与 ELEMENT + TEXT_CHILDREN 相同
{
  __v_isVNode: true,
  type: "div",
  children: "这是一个 component",
  shapeFlag: 9
}

总结:
那么由此可知,对于 组件 而言,它的一个渲染,与之前不同的地方主要有两个:

  1. shapeFlag === 4
  2. type:是一个 对象(组件实例),并且包含 render 函数

仅此而已,那么依据这样的概念,我们可以通过如下代码,完成同样的渲染:

const component = {
  render() {
    return {
      __v_isVNode: true,
      type: 'div',
      children: '这是一个 component',
      shapeFlag: 9
    }
  }
}

render(
  {
    __v_isVNode: true,
    type: component,
    shapeFlag: 4
  },
  document.querySelector('#app')
)

3.2 代码实现

在我们的代码中,处理 shapeFlag 的地方有两个:

  1. createVNode:第一次处理,表示 node 类型(比如:Element
  2. createBaseVNode:第二次处理,表示 子节点类型(比如:Text Children

因为我们这里不涉及到子节点,所以我们只需要在 createVNode 中处理即可:

  // 通过 bit 位处理 shapeFlag 类型
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : isObject(type)
    ? ShapeFlags.STATEFUL_COMPONENT
    : 0

此时创建测试实例 packages/vue/examples/runtime/h-component.html

<script>
  const { h, render } = Vue

  const component = {
    render() {
      const vnode1 = h('div', '这是一个 component')
      console.log(vnode1)
      return vnode1
    }
  }
  const vnode2 = h(component)
  console.log(vnode2)
</script>

可以得到相同的打印结果:

image.png

4. 构建 h 函数,处理 Text / Comment/ Fragment

当组件处理完成之后,最后我们来看下 TextCommentFragment 这三个场景下的 VNode。

  <script>
    const { h, render, Text, Comment, Fragment } = Vue
    const vnodeText = h(Text, '这是一个 Text')
    console.log(vnodeText)
    // 可以通过 render 进行渲染
    render(vnodeText, document.querySelector('#app1'))

    const vnodeComment = h(Comment, '这是一个 Comment')
    console.log(vnodeComment)
    render(vnodeComment, document.querySelector('#app2'))

    const vnodeFragment = h(Fragment, '这是一个 Fragment')
    console.log(vnodeFragment)
    render(vnodeFragment, document.querySelector('#app3'))
  </script>

查看打印:

image.png

可以看到 TextCommentFragment 三个的 type 分别为 Symbol(Text)Symbol(Comment)Symbol(Fragment),还是比较简单的。

实现:

  1. 直接在 packages/runtime-core/src/vnode.ts 中创建三个 Symbol
export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text')
export const Comment = Symbol('Comment')

然后导出即可。

  1. 创建测试实例 packages/vue/examples/runtime/h-other.html
<script>
  const { h, Text, Comment, Fragment } = Vue
  const vnodeText = h(Text, '这是一个 Text')
  console.log(vnodeText)

  const vnodeComment = h(Comment, '这是一个 Comment')
  console.log(vnodeComment)

  const vnodeFragment = h(Fragment, '这是一个 Fragment')
  console.log(vnodeFragment)
</script>

测试打印即可。

5. 构建 h 函数,完成虚拟节点下 class 和 style 的增强

我们在第一节中有讲过, vue_createVNode 的方法中对 classstyle 做了专门的增强,使其可以支持 ObjectArray

比如说:

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      class: {
        red: true
      }
    },
    '增强的 class'
  )

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

这样,我们可以得到一个 class: reddiv

这样的 h 函数,最终得到的 vnode 如下:

{
  __v_isVNode: true,
  type: "div",
  shapeFlag: 9,
  props: {class: 'red'},
  children: "增强的 class"
}

由以上的 VNode 可以发现,最终得出的 VNode

  const vnode = h('div', {
    class: 'red'
  }, 'hello render')

是完全相同的。

那么 vue 是如何来处理这种增强的呢?

我们一起从源码中一探究竟(style 的增强处理与 class 非常相似,所以我们只看 class 即可)

5.1 源码阅读

  1. 我们直接来到在第一节阅读源码有讲过的对 prop 进行处理的地方,也就是 packages/runtime-core/src/vnode.ts 文件中 _createVNode 方法内:

image.png

  1. 执行 props.class = normalizeClass(klass),这里的 normalizeClass 方法就是处理 class 增强的关键,进入 normalizeClass

image.png

总结:

  1. 对于 class 的增强其实还是比较简单的,只是额外对 classstyle 进行了单独的处理。
  2. 整体的处理方式也比较简单:

    1. 针对数组:进行迭代循环
    2. 针对对象:根据 value 拼接 name

5.2 代码实现

  1. 创建 packages/shared/src/normalizeProp.ts
import { isArray, isObject, isString } from '.'

/**
 * 规范化 class 类,处理 class 的增强
 */
export function normalizeClass(value: unknown): string {
  let res = ''
  // 判断是否为 string,如果是 string 就不需要专门处理
  if (isString(value)) {
    res = value
  }
  // 额外的数组增强。官方案例:https://cn.vuejs.org/guide/essentials/class-and-style.html#binding-to-arrays
  else if (isArray(value)) {
    // 循环得到数组中的每个元素,通过 normalizeClass 方法进行迭代处理
    for (let i = 0; i < value.length; i++) {
      const normalized = normalizeClass(value[i])
      if (normalized) {
        res += normalized + ' '
      }
    }
  }
  // 额外的对象增强。官方案例:https://cn.vuejs.org/guide/essentials/class-and-style.html#binding-html-classes
  else if (isObject(value)) {
    // for in 获取到所有的 key,这里的 key(name) 即为 类名。value 为 boolean 值
    for (const name in value as object) {
      // 把 value 当做 boolean 来看,拼接 name
      if ((value as object)[name]) {
        res += name + ' '
      }
    }
  }
  // 去左右空格
  return res.trim()
}
  1. packages/runtime-core/src/vnode.tscreateVNode 增加判定:
if (props) {
  // 处理 class
  let { class: klass, style } = props
  if (klass && !isString(klass)) {
    props.class = normalizeClass(klass)
  }
}

至此代码完成。

可以创建 packages/vue/examples/runtime/h-element-class.html 测试用例:

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      class: {
        red: true
      }
    },
    '增强的 class'
  )

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

打印可以获取到正确的 vnode

6. 总结

在本章中,完成了对:

  1. Element
  2. Component
  3. Text
  4. Comment
  5. Fragment

5 个标签类型的处理。

同时处理了:

  1. Text Children
  2. Array chiLdren

两个子节点类型。

在这里渲染中,我们可以发现,整个 Vnode 生成,核心的就是几个属性:

  1. type
  2. children
  3. shapeFlag
  4. __v_isVNode

另外,还完成了 class 的增强逻辑,对于 class 的增强其实是一个额外的 classarray 的处理,把复杂数据类型进行解析即可。

对于 style 的增强逻辑本质上和 class 的逻辑是一样的所以没有去实现。
它的源码是在 packages/shared/src/normalizeProp.ts 中的 normalizeStyle 方法,本身的逻辑也非常简单。

相关文章
|
6天前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
106 10
|
4月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
735 5
|
1月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
249 1
|
1月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
145 0
|
2月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
95 0
|
4月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
410 17
|
4月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
1500 0
|
9天前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
108 2
|
3月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
538 0
|
3月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能