第八章:runtime 运行时 - 构建 h 函数,生成 Vnode
01:前言
本章中,我们先来处理 h
函数的构建。
我们知道 h
函数核心是用来:创建 vnode
的。但是对于 vnode
而言,它存在很多种不同的节点类型。
查看 packages/runtime-core/src/renderer.ts
中第 354
行 patch
方法的代码可知,Vue
总共处理了:
Text
:文本节点Comment
:注释节点Static
:静态DOM
节点Fragment
:包含多个根节点的模板被表示为一个片段 (fragment)ELEMENT
:DOM
节点COMPONENT
:组件TELEPORT
:新的 内置组件SUSPENSE
:新的 内置组件- …
各种不同类型的节点,而每一种类型的处理都对应着不同的 VNode
。
所以我们在本章中,就需要把各种类型的 VNode
构建出来(不会全部处理所有类型,只会选择比较有代表性的部分),以便,后面进行 render
渲染。
那么把这些内容,明确完成之后,我们就开始本章的学习吧~~~~
02:阅读源码:初见 h 函数,跟踪 Vue 3 源码实现基础逻辑
本小节我们通过 h
函数生成 Element
的 VNode
来去查看 h
函数的源码实现。
- 创建测试实例
packages/vue/examples/imooc/runtime/h-element.html
:
<script>
const { h } = Vue
const vnode = h('div', {
class: 'test'
}, 'hello render')
console.log(vnode);
</script>
h
函数的代码位于packages/runtime-core/src/h.ts
中,为174
行增加debugger
h 函数
- 代码进入
h
函数- 通过源码可知,
h
函数接收三个参数: type
:类型。比如当前的div
就表示Element
类型propsOrChildren
:props
或者children
children
:子节点- 在这三个参数中,第一个和第三个都比较好理解,它的第二个参数代表的是什么意思呢?查看 官方示例 可知:
h
函数存在多种调用方式:
- 通过源码可知,
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')])
- 这些内容在源码中也存在对应的说明(查看
h.ts
的顶部注释),并且这种方式在其他的框架或者web api
中也是比较常见的。 - 那么这样的功能是如何实现的呢?我们继续来看源代码
- 以下为这一块逻辑的详细注释:
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)
}
}
最终代码将会触发
createVNode
方法:代码进入
createVNode
- 此时三个参数的值为:
type
:div
props
:{class: 'test'}
children
:hello render
- 此时三个参数的值为:
代码执行:
jsconst 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
最终得到
shapeFlag
的值为1
,shapeFlag
为当前的 类型标识:- 这个
1
代表的是什么意思呢?查看packages/shared/src/shapeFlags.ts
的代码 - 根据
enum ShapeFlags
可知:1
代表为Element
- 即当前
shapeFlag = ShapeFlags.Element
- 这个
代码继续执行,触发
createBaseVNode
:进入
createBaseVNode
执行:
jsconst 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
生成
vnode
对象,此时生成的vnode
值为:jsanchor: 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
剔除对我们无用的属性之后,得到:
jschildren: "hello render props: {class: 'test'} shapeFlag: 1 // 表示为 Element type: "div" __v_isVNode: true
代码执行
normalizeChildren(vnode, children)
进入
normalizeChildren
方法代码进入最后的
else
,执行type = ShapeFlags.TEXT_CHILDREN
,执行完成之后,type = 8
,此时的8
表示为ShapeFlags.TEXT_CHILDREN
注意:最后执行
vnode.shapeFlag |= type
此时
vnode.shapeFlag
原始值为1
,即ShapeFlags.ELEMENT
type
的值为8
,即ShapeFlags.TEXT_CHILDREN
而
|=
表示为按位或赋值运算:x |= y
意为x = x | y
即:
vnode.shapeFlag |= type
表示为vnode.shapeFlag = vnode.shapeFlag | type
代入值后表示
vnode.shapeFlag = 1 | 8
1
是10
进制,转化为32
位的二进制之后为 :00000000 00000000 00000000 00000001
8
是10
进制,转化为 转化为32
位的二进制之后为 :00000000 00000000 00000000 00001000
两者进行 按位或赋值 之后,得到的二进制为:
00000000 00000000 00000000 00001000
两者进行 按位或赋值 之后,得到的二进制为:
00000000 00000000 00000000 00001001
转化为
10进制
即为9
所以,此时
vnode.shapeFlag
的值为9
至此,整个 h
函数执行完成,最终得到的打印有效值为:
children: "hello render
props: {class: 'test'}
shapeFlag: 9 // 表示为 Element | ShapeFlags.TEXT_CHILDREN 的值
type: "div"
__v_isVNode: true
由以上代码可知:
h
函数内部本质上只处理了参数的问题createVNode
是生成vnode
的核心方法- 在
createVNode
中第一次生成了shapeFlag = ShapeFlags.ELEMENT
,表示为:是一个Element
类型 - 在
createBaseVNode
中,生成了vnode
对象,并且对shapeFlag
的进行|=
运算,最终得到的shapeFlag = 9
,表示为:元素为ShapeFlags.ELEMENT
,children
为TEXT
03:框架实现:构建 h 函数,处理 ELEMENT + TEXT_CHILDREN 场景
那么接下来我们依据刚才所查看的源码场景,处理自己的对应逻辑。
- 创建
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
}
创建
packages/runtime-core/src/h.ts
,构建h
函数:jsimport { 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) } }
创建
packages/runtime-core/src/vnode.ts
,处理VNode
类型和isVNode
函数:jsexport 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 }
在
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 }
在
index
中导出h
函数
至此 h
函数创建完成。
下面我们可以创建对应的测试实例,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 = Element
,children = Text
的 VNode
对象
04:源码阅读:h 函数,跟踪 ELEMENT + ARRAY_CHILDREN 场景下的源码实现
在前两节我们处理了 h
函数下比较简单的场景:Element + Text Children
。
那么这一小节,我们来看 Element + Array Children
的场景。
我们先来看测试实例 packages/vue/examples/imooc/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>
最终打印为(剔除无用的):
{
"__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
}
通过以上的打印其实我们可以看出存在一些不同的地方:
children
:为数组shapeFlag
:17
而这两点,也是 h
函数处理这种场景下,最不同的地方。
那么下面我们就跟踪源码,来看一下这次 h
函数的执行逻辑,由测试案例可知,我们一共触发了 4
次 h
函数:
第一次触发
h
函数:jsh('p', 'p1')
进入
_createVNode
方法,此时的参数为:触发
createBaseVNode
时,shapeFlag = 1
- 进入
createBaseVNode
- 触发
normalizeChildren(vnode, children)
- 进入
normalizeChildren
- 进入
else
,执行type = ShapeFlags.TEXT_CHILDREN
,此时type = 8
- 最后执行
vnode.shapeFlag |= type
,得到vnode.shapeFlag = 9
- 进入
- 进入
以上整体流程与 02 小节 看到的完全相同
接下来是 第二次、第三次 触发 h
函数,这两次触发代码流程与第一次相同,我们可以跳过。
进入到 第四次 触发
h
函数:jsh('div', { class: 'test' }, [ h('p', 'p1'), h('p', 'p2'), h('p', 'p3') ])
此时进入到
_createVNode
时的参数为:展开
children
数据为 解析完成之后的vnode
:
代码继续,计算
shapeFlag = 1
触发
createBaseVNode
:进入
createBaseVNode
执行
normalizeChildren(vnode, children)
:进入
normalizeChildren
因为当前
children = Array
,所以代码会进入到else if (isArray(children))
执行
type = ShapeFlags.ARRAY_CHILDREN
,即:type = 16
接下来执行
vnode.shapeFlag |= type
此时
vnode.shapeFlag = 1
,转化为二进制:00000000 00000000 00000000 00000001
此时
type = 16
,转化为二进制:00000000 00000000 00000000 00010000
所以最终
|=
之后的二进制为:00000000 00000000 00000000 00010001
转化为
10进制
为17
代码执行结束。
由以上代码可知,当我们处理 ELEMENT + ARRAY_CHILDREN
场景时:
- 整体的逻辑并没有变得复杂
- 第一次计算
shapeFlag
,依然为Element
- 第二次计算
shapeFlag
,因为children
为Array
,所以会进入else if (array)
判断
05:框架实现:构建 h 函数,处理 ELEMENT + ARRAY_CHILDREN 场景
根据上一小节的源码阅读可知,ELEMENT + ARRAY_CHILDREN
场景下的处理,我们只需要在 packages/runtime-core/src/vnode.ts
中,处理 isArray
场景即可:
在
packages/runtime-core/src/vnode.ts
中,找到normalizeChildren
方法:jselse if (isArray(children)) { + type = ShapeFlags.ARRAY_CHILDREN }
创建测试实例
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 3
的 render
函数,发现可以正常渲染)
局部总结
那么到现在我们可以先做一个局部的总结。
对于 vnode
而言,我们现在已经知道,它存在一个 shapeFlag
属性,该属性表示了当前 VNode
的 “类型” ,这是一个非常关键的属性,在后面的 render
函数中,还会再次看到它。
shapeFlag
分成两部分:
createVNode
:此处计算“DOM”
类型,比如Element
createBaseVNode
:此处计算 “children” 类型,比如Text
||Array
06:源码阅读:h 函数,组件的本质与对应的 VNode
组件是 vue
中非常重要的一个概念,这一小节我们就来看一下 组件 生成 VNode
的情况。
在 vue
中,组件本质上是 一个对象或一个函数(Function Component
)
我们这里 不考虑 组件是函数的情况,因为这个比较少见。
我们可以直接利用 h
函数 +render
函数渲染出一个基本的组件:
创建
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>
在当前代码中共触发了两次
h
函数,我们来查看两次打印的结果:vnode 2
:- 大家重点关注我们标注的属性。
shapeFlag
:这个是当前的类型表示,4
表示为一个 组件type
:是一个 对象,它的值包含了一个render
函数,这个就是component
的 真实渲染 内容__v_isVNode
:VNode
标记
- 大家重点关注我们标注的属性。
vnode1
:与ELEMENT + TEXT_CHILDREN
相同json{ __v_isVNode: true, type: "div", children: "这是一个 component", shapeFlag: 9 }
那么由此可知,对于 组件 而言,它的一个渲染,与之前不同的地方主要有两个:
shapeFlag === 4
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'))
07:框架实现:处理组件的 VNode
根据上一小节的了解,我们知道 组件的 VNode
其实只存在两个不同的地方:
type
shapeFlag
对于 type
而言,它是 h
函数的第一个参数,我们其实不需要单独进行处理,所以我们只需要处理 shapeFlag
即可。
在我们的代码中,处理 shapeFlag
的地方有两个:
createVNode
:第一次处理,表示node
类型(比如:Element
)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')
return vnode1
}
}
const vnode2 = h(component)
console.log(vnode2);
</script>
可以得到相同的打印结果
08:源码阅读:h 函数,跟踪 Text 、 Comment、Fragment 场景
当组件处理完成之后,最后我么来看下 Text 、 Comment、Fragment
这三个场景下的 VNode
。
Text
Text
标记为 文本。即:纯文本的 VNode
创建 packages/vue/examples/imooc/runtime/h-other.html
测试实例,查看 Text
的打印:
const { h, render, Text, Comment, Fragment } = Vue
const vnodeText = h(Text, '这是一个 Text')
console.log(vnodeText);
// 可以通过 render 进行渲染
render(vnodeText, document.querySelector('#app'))
查看打印:
{
__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
的打印:
const { h, render, Text, Comment, Fragment } = Vue
const vnodeComment = h(Comment, '这是一个 Comment')
console.log(vnodeComment);
render(vnodeComment, document.querySelector('#app'))
查看打印:
{
__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
的打印:
const { h, render, Text, Comment, Fragment } = Vue
const vnodeFragment = h(Fragment, '这是一个 Fragment')
console.log(vnodeFragment);
render(vnodeFragment, document.querySelector('#app'))
查看打印(可以看一下 Element
比较 特殊):
{
__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
:
export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text')
export const Comment = Symbol('Comment')
然后导出即可。
创建测试实例 packages/vue/examples/runtime/h-other.html
:
<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 的增强处理
vue
对 class 和 style 做了专门的增强,使其可以支持 Object
和 Array
。
比如说,我们可以写如下测试案例 packages/vue/examples/imooc/runtime/h-element-class.html
:
<script>
const { h, render } = Vue
const vnode = h('div', {
class: {
'red': true
}
}, '增强的 class')
render(vnode, document.querySelector('#app'))
</script>
这样,我们可以得到一个 class: red
的 div
。
这样的 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
即可):
进入
_createVNode
的debugger
(仅关注class
的处理)此时
props
为:props: { class: { 'red': true } }
执行
if (props)
,存在。进入 判断:执行
let { class: klass, style } = props
,得到klass: {red: true}
执行
props.class = normalizeClass(klass)
,这里的normalizeClass
方法就是处理class
增强的关键:进入
normalizeClass
方法:jsimport { 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() }
此时
props
的class
即为red
以上代码可知:
- 对于
class
的增强其实还是比较简单的,只是额外对class
和style
进行了单独的处理。 - 整体的处理方式也比较简单:
- 针对数组:进行迭代循环
- 针对对象:根据
value
拼接name
11:框架实现:完成虚拟节点下的 class 和 style 的增强
那么明确好了增强的原理之后,那么下面我们就可以进行对应的实现了。
创建
packages/shared/src/normalizeProp.ts
:tsimport { 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() }
在
packages/runtime-core/src/vnode.ts
的createVNode
增加判定:jsif (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')
console.log(vnode);
</script>
打印可以获取到正确的 vnode
。
12:总结
本章中,我们完成了:
Element
Component
Text
Comment
Fragment
5 个标签类型。
同时处理了:
Text Children
Array chiLdren
两个子节点类型。
在这里渲染中,其实我们可以发现,整个 Vnode
生成,核心的就是几个属性:
type
children
shapeFlag
__v_isVNode
但是大家也要注意,在 vue 3
的源码中,整个 VNode
的处理是非常复杂的,咱们是因为剔除了所有的边缘情况,所以才会看起来比较简单而已。
另外,我们还完成了 class
的增强逻辑,我们知道对于 class
的增强其实是一个额外的 class
和 array
的处理,把复杂数据类型进行解析即可。
对于 style
的增强逻辑,我们没有专门去看,它的代码是在 packages/shared/src/normalizeProp.ts
中的 normalizeStyle
方法,本身的逻辑也非常简单,大家可以去看一下。咱们就不在课程中专门讲解了。