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

1. 前言

本章原文来自 我的个人博客

终于来到渲染系统啦~ ,在 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 渲染。

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

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

<script>
  const { h } = Vue

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

  console.log(vnode)
</script>

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

2.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

2.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 对象

3. 构建 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 函数

3.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) 判断

3.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>

3.3 总结

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

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

shapeFlag 分成两部分:

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

4. 构建 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>

4.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')
)

4.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

5. 构建 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>

测试打印即可。

6. 构建 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 即可)

6.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

6.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

7. 总结

在本章中,完成了对:

  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 方法,本身的逻辑也非常简单。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容