前言
终于来到渲染系统啦~
在 vue3
渲染系统学习的第一章,我们先来处理 h
函数的构建,关于 h
函数的介绍我这里就不多讲了,具体可以查询文档 h() 以及 创建VNode
我们知道 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
渲染。
1. 构建 h 函数,处理 ELEMENT + TEXT_CHILDREN
老样子,我们从下面这段代码的调试 开始 vue3
的源码阅读
<script>
const { h } = Vue
const vnode = h(
'div',
{
class: 'test'
},
'hello render'
)
console.log(vnode)
</script>
这段代码很简单,就是使用 h
函数 创建了一个类型为 ELEMENT
子节点为 TEXT
的 vnode
。
1.1 源码阅读
- 我们直接跳到源码
packages/runtime-core/src/h.ts
中的第174
行,为h
函数增加debugger
:
通过源码可知,h 函数接收三个参数:
type
:类型。比如当前的div
就表示Element
类型propsOrChildren
:props
或者children
children
:子节点
而且最终代码将会触发
createVNode
方法,createVNode
方法实际就是调用了_createVnode
方法 我们进入_createVNode
方法:
3、 这里 _createVNode
对 type
做了一些条件判断,我们的 type
为 div
可以先跳过接着调试:
_createVNode
接着对props
做了class
和style
的增强,我们也可以先不管,最终得到shapeFlag
的值为1
,shapeFlag
为当前的 类型标识:shapeFlag
。查看packages/shared/src/shapeFlags.ts
的代码,根据enum ShapeFlags
可知:1
代表为 Element
即当前 shapeFlag = ShapeFlags.Element
,代码继续执行:
- 可以看到
_craeteVNode
最终是调用了createBaseVNode
方法,我们进入到createBaseVNode
方法:
createBaseVnode
方法首先创建了一个vnode
,此时的vnode
为上图右侧所示。我们做些简化,剔除对我们无用的属性之后,得到:
children: "hello render
props: {class: 'test'}
shapeFlag: 1 // 表示为 Element
type: "div"
__v_isVNode: true
在 createBaseVnode
中继续执行代码,会进入到 normalizeChildren
的方法中:
- 在
normalizeChildren
的方法中,会执行最后的else
以及一个 按位或赋值运算 最后得到shapeFlag
的最终值为9
normalizeChildren
方法 结束,craeteBaseVNode
返回vnode
- 至此,整个
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
1.2 代码实现
- 创建
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
函数:
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)
}
}
- 创建
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
}
- 在
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
}
- 在
index
中导出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
对象
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 函数,分别为:
h('p', 'p1')
h('p', 'p2')
h('p', 'p3')
- 以及最外层的
h(...)
前三次触发代码的流程和第一个节中相似,我们直接将代码 debugger
到第四次 h
函数
2.1 源码阅读
- 此时进入到
_createVNode
时的参数为:
- 代码继续,计算
shapeFlag = 1
(与第一节一样) _createVNode
返回一个createBaseVNode
方法, 进入createBaseVNode
createBaseVNode
创建vnode
, 接着执行normalizeChildren(vnode, children)
:
normalizeChildren
我们之前跟踪过得,这次vnode.shapeFlag
计算出来是17
。- 我们最终将不重要的属性剔除,打印出的
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
的过程
- 整体的逻辑并没有变得复杂
- 第一次计算
shapeFlag
,依然为Element
- 第二次计算
shapeFlag
,因为children
为Array
,所以会进入else if (array)
判断
2.2 代码实现
根据上一小节的源码阅读可知,ELEMENT + ARRAY_CHILDREN
场景下的处理,我们只需要在 packages/runtime-core/src/vnode.ts
中,处理 isArray
场景即可:
- 在
packages/runtime-core/src/vnode.ts
中,找到normalizeChildren
方法:
else if (isArray(children)) {
// TODO: array
+ type = ShapeFlags.ARRAY_CHILDREN
}
- 创建测试实例
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
分成两部分:
createVNode
:此处计算“DOM”
类型,比如Element
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 案例分析
- 在当前代码中共触发了两次
h
函数, - 第一次是在
component
对象中的render
函数内,我们可以把component
对象看成一个组件,实际上在vue3
中你打印一个组件对象它的内部就有一个render
函数,下面是我打印的一个App
组件
- 第二次是在将
component
作为参数生成的vnode2
时 - 最后将生成的
vnode2
通过render
渲染函数 渲染到页面上(关于render
函数我们之后在讲) - 最终打印的
vnode2
如下图所示:
shapeFlag
:这个是当前的类型表示,4
表示为一个 组件type
:是一个 对象,它的值包含了一个 render 函数,这个就是 component 的 真实渲染 内容__v_isVNode
:VNode
标记
vnode1
:与ELEMENT + TEXT_CHILDREN
相同
{
__v_isVNode: true,
type: "div",
children: "这是一个 component",
shapeFlag: 9
}
总结:
那么由此可知,对于 组件 而言,它的一个渲染,与之前不同的地方主要有两个:
shapeFlag
=== 4type
:是一个 对象(组件实例),并且包含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
的地方有两个:
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')
console.log(vnode1)
return vnode1
}
}
const vnode2 = h(component)
console.log(vnode2)
</script>
可以得到相同的打印结果:
4. 构建 h 函数,处理 Text / Comment/ Fragment
当组件处理完成之后,最后我们来看下 Text
、 Comment
、Fragment
这三个场景下的 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>
查看打印:
可以看到 Text
、Comment
、Fragment
三个的 type
分别为 Symbol(Text)
、Symbol(Comment)
、Symbol(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, 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
的方法中对 class
和 style
做了专门的增强,使其可以支持 Object
和 Array
。
比如说:
<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
即可)
5.1 源码阅读
- 我们直接来到在第一节阅读源码有讲过的对
prop
进行处理的地方,也就是packages/runtime-core/src/vnode.ts
文件中_createVNode
方法内:
- 执行
props.class = normalizeClass(klass)
,这里的normalizeClass
方法就是处理class
增强的关键,进入normalizeClass
:
总结:
- 对于
class
的增强其实还是比较简单的,只是额外对class
和style
进行了单独的处理。 整体的处理方式也比较简单:
- 针对数组:进行迭代循环
- 针对对象:根据
value
拼接name
5.2 代码实现
- 创建
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()
}
- 在
packages/runtime-core/src/vnode.ts
的createVNode
增加判定:
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. 总结
在本章中,完成了对:
Element
Component
Text
Comment
Fragment
5
个标签类型的处理。
同时处理了:
Text Children
Array chiLdren
两个子节点类型。
在这里渲染中,我们可以发现,整个 Vnode
生成,核心的就是几个属性:
type
children
shapeFlag
__v_isVNode
另外,还完成了 class
的增强逻辑,对于 class
的增强其实是一个额外的 class
和 array
的处理,把复杂数据类型进行解析即可。
对于 style
的增强逻辑本质上和 class
的逻辑是一样的所以没有去实现。
它的源码是在 packages/shared/src/normalizeProp.ts
中的 normalizeStyle
方法,本身的逻辑也非常简单。