Skip to content

第八章:runtime 运行时 - 构建 h 函数,生成 Vnode

01:前言

本章中,我们先来处理 h 函数的构建。

我们知道 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:新的 内置组件

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

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

那么把这些内容,明确完成之后,我们就开始本章的学习吧~~~~

02:阅读源码:初见 h 函数,跟踪 Vue 3 源码实现基础逻辑

本小节我们通过 h 函数生成 ElementVNode 来去查看 h 函数的源码实现。

  1. 创建测试实例 packages/vue/examples/imooc/runtime/h-element.html
html
   <script>
     const { h } = Vue
   
     const vnode = h('div', {
       class: 'test'
     }, 'hello render')
   
     console.log(vnode);
   </script>
  1. h 函数的代码位于 packages/runtime-core/src/h.ts 中,为 174 行增加 debugger

h 函数

  1. 代码进入 h 函数
    1. 通过源码可知,h 函数接收三个参数:
    2. type:类型。比如当前的 div 就表示 Element 类型
    3. propsOrChildrenprops 或者 children
    4. children:子节点
    5. 在这三个参数中,第一个和第三个都比较好理解,它的第二个参数代表的是什么意思呢?查看 官方示例 可知:h 函数存在多种调用方式
js
   import { h } from 'vue'
   
   // 除了 type 外,其他参数都是可选的
   h('div')
   h('div', { id: 'foo' })
   
   // attribute 和 property 都可以用于 prop
   // Vue 会自动选择正确的方式来分配它
   h('div', { class: 'bar', innerHTML: 'hello' })
   
   // class 与 style 可以像在模板中一样
   // 用数组或对象的形式书写
   h('div', { class: [foo, { bar }], style: { color: 'red' } })
   
   // 事件监听器应以 onXxx 的形式书写
   h('div', { onClick: () => {} })
   
   // children 可以是一个字符串
   h('div', { id: 'foo' }, 'hello')
   
   // 没有 prop 时可以省略不写
   h('div', 'hello')
   h('div', [h('span', 'hello')])
   
   // children 数组可以同时包含 vnode 和字符串
   h('div', ['hello', h('span', 'hello')])
  1. 这些内容在源码中也存在对应的说明(查看 h.ts 的顶部注释),并且这种方式在其他的框架或者 web api 中也是比较常见的。
  2. 那么这样的功能是如何实现的呢?我们继续来看源代码
  3. 以下为这一块逻辑的详细注释:
js
   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,则 第二个参数代表了 children
       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. 最终代码将会触发 createVNode 方法:

    1. 代码进入 createVNode

      1. 此时三个参数的值为:
        1. typediv
        2. props{class: 'test'}
        3. childrenhello render
    2. 代码执行:

      js
      const shapeFlag = isString(type)
            ? ShapeFlags.ELEMENT
            : __FEATURE_SUSPENSE__ && isSuspense(type)
            ? ShapeFlags.SUSPENSE
            : isTeleport(type)
            ? ShapeFlags.TELEPORT
            : isObject(type)
            ? ShapeFlags.STATEFUL_COMPONENT
            : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0
    3. 最终得到 shapeFlag 的值为 1shapeFlag 为当前的 类型标识

      1. 这个 1 代表的是什么意思呢?查看 packages/shared/src/shapeFlags.ts 的代码
      2. 根据 enum ShapeFlags 可知:1 代表为 Element
      3. 当前 shapeFlag = ShapeFlags.Element
    4. 代码继续执行,触发 createBaseVNode

      1. 进入 createBaseVNode

      2. 执行:

        js
          const vnode = {
            __v_isVNode: true,
            __v_skip: true,
            type,
            props,
            key: props && normalizeKey(props),
            ref: props && normalizeRef(props),
            scopeId: currentScopeId,
            slotScopeIds: null,
            children,
            component: null,
            suspense: null,
            ssContent: null,
            ssFallback: null,
            dirs: null,
            transition: null,
            el: null,
            anchor: null,
            target: null,
            targetAnchor: null,
            staticCount: 0,
            shapeFlag,
            patchFlag,
            dynamicProps,
            dynamicChildren: null,
            appContext: null
          } as VNode
      3. 生成 vnode 对象,此时生成的 vnode 值为:

        js
        anchor: null
        appContext: null
        children: "hello render"
        component: null
        dirs: null
        dynamicChildren: null
        dynamicProps: null
        el: null
        key: null
        patchFlag: 0
        props: {class: 'test'}
        ref: null
        scopeId: null
        shapeFlag: 1
        slotScopeIds: null
        ssContent: null
        ssFallback: null
        staticCount: 0
        suspense: null
        target: null
        targetAnchor: null
        transition: null
        type: "div"
        __v_isVNode: true
        __v_skip: true

        剔除对我们无用的属性之后,得到:

        js
        children: "hello render
        props: {class: 'test'}
        shapeFlag: 1 // 表示为 Element
        type: "div"
        __v_isVNode: true
      4. 代码执行 normalizeChildren(vnode, children)

        1. 进入 normalizeChildren 方法

        2. 代码进入最后的 else,执行 type = ShapeFlags.TEXT_CHILDREN,执行完成之后,type = 8,此时的 8 表示为 ShapeFlags.TEXT_CHILDREN

        3. 注意:最后执行 vnode.shapeFlag |= type

          1. 此时 vnode.shapeFlag 原始值为 1,即 ShapeFlags.ELEMENT

          2. type 的值为 8,即 ShapeFlags.TEXT_CHILDREN

          3. |= 表示为按位或赋值运算:x |= y 意为 x = x | y

            1. 即:vnode.shapeFlag |= type 表示为 vnode.shapeFlag = vnode.shapeFlag | type

            2. 代入值后表示 vnode.shapeFlag = 1 | 8

            3. 110进制,转化为32位的二进制之后为 :

              1. 00000000 00000000 00000000 00000001
            4. 810 进制,转化为 转化为 32 位的二进制之后为 :

              1. 00000000 00000000 00000000 00001000
            5. 两者进行 按位或赋值 之后,得到的二进制为:

            6. 00000000 00000000 00000000 00001000

            7. 两者进行 按位或赋值 之后,得到的二进制为:

              1. 00000000 00000000 00000000 00001001

              2. 转化为 10进制 即为 9

      5. 所以,此时 vnode.shapeFlag 的值为 9

至此,整个 h 函数执行完成,最终得到的打印有效值为:

js
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

03:框架实现:构建 h 函数,处理 ELEMENT + TEXT_CHILDREN 场景

那么接下来我们依据刚才所查看的源码场景,处理自己的对应逻辑。

  1. 创建 packages/shared/src/shapeFlags.ts ,写入所有的对应类型:
js
   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 函数:

    js
       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)
       	}
       }
  2. 创建 packages/runtime-core/src/vnode.ts,处理 VNode 类型和 isVNode 函数:

    js
       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
       }
  3. packages/runtime-core/src/vnode.ts 中,构建 createVNode 函数:

    js
     /**
        * 生成一个 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
       }
  4. index 中导出 h 函数

至此 h 函数创建完成。

下面我们可以创建对应的测试实例,packages/vue/examples/runtime/h-element.html

html
<script>
  const { h } = Vue

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

  console.log(vnode);
</script>

最终打印的结果为:

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

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

04:源码阅读:h 函数,跟踪 ELEMENT + ARRAY_CHILDREN 场景下的源码实现

在前两节我们处理了 h 函数下比较简单的场景:Element + Text Children

那么这一小节,我们来看 Element + Array Children 的场景。

我们先来看测试实例 packages/vue/examples/imooc/runtime/h-element-ArrayChildren.html

js
<script>
  const { h } = Vue

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

  console.log(vnode);
</script>

最终打印为(剔除无用的):

js
{
  "__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
}

通过以上的打印其实我们可以看出存在一些不同的地方:

  1. children:为数组
  2. shapeFlag17

而这两点,也是 h 函数处理这种场景下,最不同的地方。

那么下面我们就跟踪源码,来看一下这次 h 函数的执行逻辑,由测试案例可知,我们一共触发了 4h 函数:

  1. 第一次触发 h 函数:

    js
    h('p', 'p1')
  2. 进入 _createVNode 方法,此时的参数为:

    image-20230812220122363

  3. 触发 createBaseVNode 时,shapeFlag = 1

    1. 进入 createBaseVNode
    2. 触发 normalizeChildren(vnode, children)
      1. 进入 normalizeChildren
      2. 进入 else,执行 type = ShapeFlags.TEXT_CHILDREN,此时 type = 8
      3. 最后执行 vnode.shapeFlag |= type ,得到 vnode.shapeFlag = 9
  4. 以上整体流程与 02 小节 看到的完全相同

接下来是 第二次、第三次 触发 h 函数,这两次触发代码流程与第一次相同,我们可以跳过。

  1. 进入到 第四次 触发 h 函数:

    js
    h('div', {
        class: 'test'
      }, [
        h('p', 'p1'),
        h('p', 'p2'),
        h('p', 'p3')
      ])
  2. 此时进入到 _createVNode 时的参数为:

    image-20230812223819976

    1. 展开 children 数据为 解析完成之后的 vnode

      image-20230812223855114

  3. 代码继续,计算 shapeFlag = 1

  4. 触发 createBaseVNode

    1. 进入 createBaseVNode

    2. 执行 normalizeChildren(vnode, children)

      1. 进入 normalizeChildren

      2. 因为当前 children = Array,所以代码会进入到 else if (isArray(children))

      3. 执行 type = ShapeFlags.ARRAY_CHILDREN,即:type = 16

      4. 接下来执行 vnode.shapeFlag |= type

        1. 此时 vnode.shapeFlag = 1,转化为二进制:

          00000000 00000000 00000000 00000001
        2. 此时 type = 16,转化为二进制:

          00000000 00000000 00000000 00010000
        3. 所以最终 |= 之后的二进制为:

          00000000 00000000 00000000 00010001

          转化为 10进制17

代码执行结束。

由以上代码可知,当我们处理 ELEMENT + ARRAY_CHILDREN 场景时:

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

05:框架实现:构建 h 函数,处理 ELEMENT + ARRAY_CHILDREN 场景

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

  1. packages/runtime-core/src/vnode.ts 中,找到 normalizeChildren 方法:

    js
     else if (isArray(children)) {
     +		type = ShapeFlags.ARRAY_CHILDREN
     	}
  2. 创建测试实例 packages/vue/examples/runtime/h-element-ArrayChildren.html

    html
    <script>
      const { h } = Vue
    
      const vnode = h('div', {
        class: 'test'
      }, [
        h('p', 'p1'),
        h('p', 'p2'),
        h('p', 'p3')
      ])
    
      console.log(vnode);
    </script>

可以得出同样的打印结果。(大家也可以直接把打印的 vnode 传递给 vue 3render 函数,发现可以正常渲染)

局部总结

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

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

shapeFlag 分成两部分:

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

06:源码阅读:h 函数,组件的本质与对应的 VNode

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

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

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

我们可以直接利用 h 函数 +render 函数渲染出一个基本的组件:

  1. 创建 packages/vue/examples/imooc/runtime/h-component.html

    js
    <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>
  2. 在当前代码中共触发了两次 h 函数,我们来查看两次打印的结果:

    1. vnode 2

      image-20230812224330928

      1. 大家重点关注我们标注的属性。
        1. shapeFlag:这个是当前的类型表示,4 表示为一个 组件
        2. type:是一个 对象,它的值包含了一个 render 函数,这个就是 component真实渲染 内容
        3. __v_isVNodeVNode 标记
    2. vnode1:与 ELEMENT + TEXT_CHILDREN 相同

      json
      {
        __v_isVNode: true,
        type: "div",
        children: "这是一个 component",
        shapeFlag: 9
      }

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

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

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

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

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

07:框架实现:处理组件的 VNode

根据上一小节的了解,我们知道 组件的 VNode 其实只存在两个不同的地方:

  1. type
  2. shapeFlag

对于 type 而言,它是 h 函数的第一个参数,我们其实不需要单独进行处理,所以我们只需要处理 shapeFlag 即可。

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

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

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

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

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

html
<script>
  const { h, render } = Vue

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

可以得到相同的打印结果

image-20230812224653079

08:源码阅读:h 函数,跟踪 Text 、 Comment、Fragment 场景

当组件处理完成之后,最后我么来看下 Text 、 Comment、Fragment 这三个场景下的 VNode

Text

Text标记为 文本。即:纯文本的 VNode

创建 packages/vue/examples/imooc/runtime/h-other.html 测试实例,查看 Text 的打印:

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

查看打印:

json
{
  __v_isVNode: true
	children: "这是一个 Text",
  type: Symbol(Text),
  shapeFlag: 8 // TEXT_CHILDREN
}

那么由以上代码可知,对于 Text ,它存在的唯一一个比较特殊的地方就是:**Text 类型是一个 Symbol(Text) **,这个类型是在 Vue 中被导出的。

Comment

Comment 标记为 注释。即:注释节点的 VNode

packages/vue/examples/imooc/runtime/h-other.html 测试实例中,查看 vnodeComment 的打印:

js
const { h, render, Text, Comment, Fragment } = Vue
const vnodeComment = h(Comment, '这是一个 Comment')
console.log(vnodeComment);
render(vnodeComment, document.querySelector('#app'))

查看打印:

json
{
  __v_isVNode: true
	children: "这是一个 Comment",
  type: Symbol(Comment),
  shapeFlag: 8 // TEXT_CHILDREN
}

那么由以上代码可知,对于 Comment ,它存在的唯一一个比较特殊的地方就是:**Comment 类型是一个 Symbol(Comment) **,这个类型是在 Vue 中被导出的。

Fragment

Fragment 标记为 片段。它相对比较特殊,是 Vue3 中新提出的一个概念,主要应对与 包含多个根节点的模板

即:包含多个根节点的模板被表示为一个片段 (fragment)。

packages/vue/examples/imooc/runtime/h-other.html 测试实例中,查看 vnodeComment 的打印:

js
const { h, render, Text, Comment, Fragment } = Vue
const vnodeFragment = h(Fragment, '这是一个 Fragment')
console.log(vnodeFragment);
render(vnodeFragment, document.querySelector('#app'))

查看打印(可以看一下 Element 比较 特殊):

json
{
  __v_isVNode: true
	children: "这是一个 Fragment",
  type: Symbol(Fragment),
  shapeFlag: 8 // TEXT_CHILDREN
}

那么由以上代码可知,对于 Fragment ,它存在的唯一一个比较特殊的地方就是:**Fragment 类型是一个 Symbol(Fragment) **,这个类型是在 Vue 中被导出的。

总结

由以上代码可知, Text 、 Comment、Fragment 三块的处理还是比较简单的,比较重要的是搞明白他们三者的意思即可。

09:框架实现:实现剩余场景 Text 、 Comment、Fragment

根据上一小节的描述,我们可以直接在 packages/runtime-core/src/vnode.ts 中创建三个 Symbol

js
export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text')
export const Comment = Symbol('Comment')

然后导出即可。

创建测试实例 packages/vue/examples/runtime/h-other.html

js
<script>
  const { h, render, 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>

测试打印即可。

10:源码阅读:对 class 和 style 的增强处理

vueclass 和 style 做了专门的增强,使其可以支持 ObjectArray

比如说,我们可以写如下测试案例 packages/vue/examples/imooc/runtime/h-element-class.html

js
<script>
  const { h, render } = Vue

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

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

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

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

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

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

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

是完全相同的。

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

我们下面就来一探究竟(style 的增强处理与 class 非常相似,所以我们只看 class 即可):

  1. 进入 _createVNodedebugger仅关注 class 的处理

  2. 此时 props 为:

    props: {
    	class: {
        'red': true
      }
    }
  3. 执行 if (props),存在。进入 判断:

    1. 执行 let { class: klass, style } = props,得到 klass: {red: true}

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

      1. 进入 normalizeClass 方法:

        js
        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()
        }
      2. 此时 propsclass 即为 red

以上代码可知:

  1. 对于 class 的增强其实还是比较简单的,只是额外对 classstyle 进行了单独的处理。
  2. 整体的处理方式也比较简单:
    1. 针对数组:进行迭代循环
    2. 针对对象:根据 value 拼接 name

11:框架实现:完成虚拟节点下的 class 和 style 的增强

那么明确好了增强的原理之后,那么下面我们就可以进行对应的实现了。

  1. 创建 packages/shared/src/normalizeProp.ts

    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()
       }
  2. packages/runtime-core/src/vnode.tscreateVNode 增加判定:

    js
       if (props) {
         // 处理 class
         let { class: klass, style } = props
         if (klass && !isString(klass)) {
           props.class = normalizeClass(klass)
         }
       }

至此代码完成。

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

js
<script>
  const { h, render } = Vue

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

  console.log(vnode);
</script>

打印可以获取到正确的 vnode

12:总结

本章中,我们完成了:

  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

但是大家也要注意,在 vue 3 的源码中,整个 VNode 的处理是非常复杂的,咱们是因为剔除了所有的边缘情况,所以才会看起来比较简单而已。

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

对于 style 的增强逻辑,我们没有专门去看,它的代码是在 packages/shared/src/normalizeProp.ts 中的 normalizeStyle 方法,本身的逻辑也非常简单,大家可以去看一下。咱们就不在课程中专门讲解了。

Released under the MIT License.