Vue 3 内部原理讲解,深入理解 Vue 3,创建你自己的 Vue 3。
Deep Dive into Vue 3. Build Your Own Vue 3 From Scratch.
本文完整内容可见,从零开始创建你自己的Vue 3
第1章 Vue 3总览
你能学到什么
- 了解 Vue 3 核心模块的功能
- 了解 Vue 3 整体的运行过程
核心模块
Vue 3有三个核心模块,分别是:
- 响应式(
reactivity
)模块 - 编译器(
compiler
)模块 - 渲染器(
renderer
)模块
响应式模块
reactivity
模块用来创建响应式对象,我们可以监听这些对象的修改,当执使用了这些响应式对象的代码执行时,他们就会被跟踪,当响应式对象的值发生了变化时,这些被追踪的代码会重新执行。
编译器模块
compiler
模块是用来处理模板的,它把模板编译成 render
函数,它可以发生在浏览器的运行阶段,但更多的是在Vue项目构建时进行编译。
渲染器模块
renderer
模块处理 VNode
,把组件渲染到 web 页上。它包含三个阶段:
-
Render
阶段,调用render
函数并返回一个VNode
; -
Mount
阶段,render
接收VNode
,然后进行 JavaScript DOM 操作来创建 web 页; -
Patch
阶段,render
接收新旧两个VNode
,比较二者的不同,然后进行页面的局部更新。
运行过程
-
compiler
把 HTML 编译成render
函数; -
reactivity
模块初始化reactive
对象; -
renderer
模块的Render
阶段,调用引用了reactive
对象的render
函数,这样就监听了响应式对象,render
函数返回VNode
; -
renderer
模块的Monut
阶段,用VNode
生成真实的 DOM 并渲染到页面上; - 如果
reactive
对象发生了变化,将再次调用render
函数创建新的VNode
,这时将进入renderer
模块的Patch
阶段,更新页面的变化。
第2章 渲染机制
你能学到什么
- 了解 Virtual DOM 存在的意义
- 了解
render
函数存在的必要性 - 了解
tempalte
和render
的使用场景
Virtual DOM
Virtual DOM 是什么
Virtual DOM 就是用 JavaScript 对象来描述真实的 DOM 节点。
例如,有这样一段 HTML:
<div id="div">
<button @click="click">click</button>
</div>
用 Virtual DOM 来表示可以是这样的(为什么说可以是这样,因为这完全取决于你的设计):
const vDom = {
tag: 'div',
id: 'div',
children: [{
tag: 'button',
onClick: this.click
}]
}
为什么要用 Virtual DOM
-
可跨平台,Virtual DOM 使组件的渲染逻辑和真实 DOM 彻底解耦,因此你可以很方便的在不同环境使用它,例如,当你开发的不是面向浏览器的,而是 IOS 或 Android 或小程序,你可以利用编写自己的
render
函数,把 Virtual DOM 渲染成自己想要的东西,而不仅仅是 DOM。 - 可程序式修改,Virtual DOM 提供了一种可以通过编程的方式修改、检查、克隆 DOM 结构的能力,你可以在把 DOM 返回给渲染引擎之前,先利用基本的 JavaScript 来处理好。
- 提升性能,当页面中有大量的 DOM 节点操作时,如果涉及到了浏览器的回流和重绘,性能是十分糟糕的,就像第二条说的,在 DOM 返回给渲染引擎之前,我们可以先用 JavaScript 处理 Virtual DOM,最终才返回真实 DOM,极大减少回流和重绘次数。
render 函数
render 函数是什么
首先我们知道,当你在编写 Vue 组件或页面时,一般会提供一个 template
选项来写 HTML 内容。根据Vue 3 总览这一部分内容的介绍,Vue 会先走编译阶段,把 template
编译成 render
函数,所以说最终的 DOM 一定是从 render
函数输出的。因此 render
函数可以用来代替 template
,它返回的内容就是 VNode
。直接使用 render
反而可以省去 complier
过程。
为什么要提供 render 函数
Vue 中提供的 render
函数是非常有用的,因为有些情况用 template
来表达业务逻辑会一定程度受到限制,这种情况你需要一种比较灵活的编程方式来表达底层的逻辑。
例如,当你有一个需求是大量的文本输入框,这中需求你要写的标签并不多,但是却揉了大量的交互逻辑,你需要在模板上添加大量的逻辑代码(比如控制关联标签的显示),然而,你的 JavaScript 代码中也有大量的逻辑代码。
render
函数的存在可以让你在一个地方写业务逻辑,这时你就不用太多的去考虑标签的问题了。
render 函数使用方法
有一段 template
如下:
template: '<div id="foo" @click="onClick">hello</div>'
Vue 3 中用 render
函数实现如下:
import { h } from 'vue'
render() {
return h('div', {
id: 'foo',
onClick: this.onClick
}, 'hello')
}
由于这是纯粹的 JavaScript,所以如果你需要实现 template
中类似 v-if
、v-for
这样的功能,直接通过三元表达式做到。
import { h } from 'vue'
render() {
let nodeToReturn
// v-if="ok"
if(this.ok) {
nodeToReturn = h('div', {
id: 'foo',
onClick: this.onClick
}, 'ok')
} else {
// v-for="item in list"
const children = this.list.map(item => {
return h('p', {
key: item.id
}, item.text)
})
nodeToReturn = h('div', {}, children)
}
return nodeToReturn
}
这就是 render
基本使用用法,就是 JavaScript 代码而已。
render 函数使用场景
一般来说我们用 tempate
可以满足大多数场景来,但是你一定了解过 slot
这个东西,如果只使用 tempate
你将无法操作 slot
中的内容,如果你需要程序式地修改传进来的 slot
内容,你就必须用到 render
函数了(这也是大多数使用 render
函数的场景)。
下面我们用一个例子来说明。
比如我们要实现这样一个组件:实现层级缩进效果,即类似 HTML 中嵌套的 UL 标签,看起来就像这样:
level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2
我们的模板是这样写的,实际上 Stack
组件就是给每一个 slot
都增加一个左边距:
<Stack size="10">
<div>level 1</div>
<Stack size="10">
<div>level 1-1</div>
<div>level 1-2</div>
<Stack size="10">
<div>level 1-2-1</div>
<div>level 1-2-2</div>
</Stack>
</Stack>
</Stack>
现在我们只用 template
是无法实现这种效果的,众所周知,template
只能把默认的 slot
渲染出来,它不能程序式处理 slot
的值。
我们先用 template
来实现这个组件,stack.html:
const Stack = {
props: {
size: [String, Number]
},
template: `
<div class="stack">
<slot></slot>
</div>
`
}
这样由于不能处理 slot
内容,那么它的表现效果如下,并没有层级缩进:
level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2
我们现在尝试用 render
函数实现 Stack
组件:
const { h } = Vue
const Stack = {
props: {
size: [String, Number]
},
render() {
const slot = this.$slots.default
? this.$slots.default()
: []
return h('div', { class: 'stack' },
// 这里给每一项 slot 增加一个缩进 class
slot.map(child => {
return h('div', { class: `ml${this.$props.size}` }, [ child ])
}))
}
}
render
函数中我们可以通过 this.$slots
拿到插槽内容,通过 JavaScript 把它处理成任何我们想要的东西,这里我们给每一项 slot
添加了一个 margin-left: 10px
缩进,看下效果:
level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2
完美,我们实现了一个用 template
几乎实现不了的功能。
原则:
- 一般来说开发一些公共组件时才会用到
render
- 当你发现用 JavaScript 才能更好的表达你的逻辑时,那么就用
render
函数 - 日常开发的功能性组件使用
template
,这样更高效,且template
更容易被complier
优化
结束语
你可能会想,为什么不直接编译成 VNode
,而要在中间加一层 render
呢?
这是因为 VNode
本身包含的信息比较多,手写太麻烦,也许你写着写着不自觉就封装成了一个 helper
函数,h
函数就是这样的,它把公用、灵活、复杂的逻辑封装成函数,并交给运行时,使用这样的函数将大大降低你的编写成本。
知道了为什么要有 render
后,才需要去设计实现它,其实主要是实现 h
函数。
第3章 渲染器原理及其实现
你能学到什么
- 了解 Vue 3 中的
VNode
- 了解
render
的具体渲染原理 - 了解
diff
算法的作用 - 实现 Vue 3 中渲染器功能
编译器和渲染器 API初探
Complier 和 Renderer
在Vue 3总览章节中,我们已经初步认识了编译器(complier
)和渲染器( renderer
)的作用。
- 编译器是用来处理模板的,它把模板编译成
render
函数 - 渲染器处理
VNode
,把组件渲染到 web 页上
我们有这样一段 HTML:
<div id="div">
<button @click="click">click</button>
</div>
编译器会先把它处理成 render
函数,类似下面的代码:
import { h } from 'vue'
render() {
return h('div', {
id: 'div',
}, [
h('button', {
onClick: this.click
}, 'click')
])
}
渲染器通过 render
函数获取对应的 VNode
,类似这样:
const vDom = {
tag: 'div',
id: 'div',
children: [{
tag: 'button',
onClick: this.click,
text: 'click'
}]
}
编译器(Complier)真实场景
上面是一个很简单的例子,实际上,Vue 3中的编译器做了很多的优化工作,比如判断你的节点是静态的还是动态的、缓存事件的绑定等等。所以如果你的组件用 template
实现的话,反而会被 Vue 优化。
我们通过 Vue 3在线模板编译系统 生成一段真实代码:
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "div" }, [
_createVNode("button", { onClick: _ctx.click }, "click", 8 /* PROPS */, ["onClick"])
]))
}
// Check the console for the AST
可以看到和我们手写的 render
函数还是有比较大的差异。
设计 VNode
render
函数返回结果就是 h
函数执行的结果,因此 h
函数的输出为 VNode
。
所以需要先设计一下我们的 VNode
。
用 VNode 描述 HTML
一个 html
标签有它的标签名、属性、事件、样式、子节点等诸多信息,这些内容都需要在 VNode
中体现。
<div id="div">
div text
<p>p text</p>
</div>
const elementVNode = {
tag: 'div',
props: {
id: 'div'
},
text: 'div text',
children: [{
tag: 'p',
props: null,
text: 'p text'
}]
}
上面的代码显示了 DOM 变成 VNode
的表现形式,VNode
各属性解释:
-
tag
:表示 DOM 元素的标签名,如div
、span
等 -
props
:表示 DOM 元素上的属性,如id
、class
等 -
children
:表示 DOM 元素的子节点 -
text
:表示 DOM 元素的文本节点
这样设计 VNode
完全没有问题(实际上 Vue 2 就是这样设计的),但是 Vue 3 设计的 VNode
并不包含 text
属性,而是直接用 children
代替,因为 text
本质也是 DOM 的子节点。
在保证语义讲得通的情况下尽可能复用属性,可以使 VNode
对象更加轻量。
基于此我们把刚才的 VNode
修改成如下形式:
const elementVNode = {
tag: 'div',
props: {
id: 'div'
},
children: [{
tag: null,
props: null,
children: 'div text'
}, {
tag: 'p',
props: null,
children: 'p text'
}]
}
用 VNode 描述抽象内容
什么是抽象内容呢?组件就属于抽象内容,比如下面这一段模板内容:
<div>
<MyComponent></MyComponent>
</div>
MyComponent
是一个组件,我们预期渲染出 MyComponent
组件所有的内容,而不是一个 MyComponent
标签,这用 VNode
如何表示呢?
上一段内容我们其实已经通过 tag
是否为 null
来区分元素节点和文本节点了,那这里我们可以通过 tag
是否是字符串判断是标签还是组件呢?
const elementVNode = {
tag: 'div',
props: null,
children: [{
tag: MyComponent,
props: null
}]
}
理论上是可以的,Vue 2 中就是通过 tag
来判断的,具体过程如下,可以在这里看源码:
-
VNode.tag
如果不是字符串,则创建组件类型的VNode
-
VNode.tag
是字符串- 若是内置的
html
或svg
标签,则创建正常的VNode
- 若是属于某个组件的 id,则创建组件类型的
VNode
- 未知或没有命名空间的组件,直接创建
VNode
- 若是内置的
以上这些判断都是在挂载(或 patch
)阶段进行的,换句话说,一个 VNode
表示的内容需要在代码运行阶段才知道。这就带来了两个难题:无法从 AOT
的层面优化、开发者无法手动优化。
如果可以提前知道 VNode
类型,那么就可以对其进行优化,所以这里我们可以定义好一套用来判断 VNode
类型的规则,随便是用 FLAG = 1
这样的数字表示还是其它方法。
区分 VNode 类型
这里我们给 VNode
增加一个字段 shapeFlag
(这是为了和 Vue 3 保持一致),它是一个枚举类型变量,具体如下:
export const enum ShapeFlags {
// html 或 svg 标签
ELEMENT = 1,
// 函数式组件
FUNCTIONAL_COMPONENT = 1 << 1,
// 普通有状态组件
STATEFUL_COMPONENT = 1 << 2,
// 子节点是纯文本
TEXT_CHILDREN = 1 << 3,
// 子节点是数组
ARRAY_CHILDREN = 1 << 4,
// 子节点是 slots
SLOTS_CHILDREN = 1 << 5,
// Portal
PORTAL = 1 << 6,
// Suspense
SUSPENSE = 1 << 7,
// 需要被keepAlive的有状态组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
// 已经被keepAlive的有状态组件
COMPONENT_KEPT_ALIVE = 1 << 9,
// 有状态组件和函数式组件都是“组件”,用 COMPONENT 表示
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
现在我们可以修改我们的 VNode
如下:
const elementVNode = {
shapeFlag: ShapeFlags.ELEMENT,
tag: 'div',
props: null,
children: [{
shapeFlag: ShapeFlags.COMPONENT,
tag: MyComponent,
props: null
}]
}
shapeFlag
如何用来判断 VNode
类型呢?按位运算即可。
const isComponent = vnode.shapeFlag & ShapeFlags.COMPONENT
熟悉一下按位运算。
-
a & b
:对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。 -
a | b
:对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。
我们把 ShapeFlags
对应的值列出来,如下:
ShapeFlags | 操作 | bitmap |
---|---|---|
ELEMENT | 0000000001
|
|
FUNCTIONAL_COMPONENT | 1 << 1 | 000000001 0 |
STATEFUL_COMPONENT | 1 << 2 | 00000001 00 |
TEXT_CHILDREN | 1 << 3 | 0000001 000 |
ARRAY_CHILDREN | 1 << 4 | 000001 0000 |
SLOTS_CHILDREN | 1 << 5 | 00001 00000 |
PORTAL | 1 << 6 | 0001 000000 |
SUSPENSE | 1 << 7 | 001 0000000 |
COMPONENT_SHOULD_KEEP_ALIVE | 1 << 8 | 01 00000000 |
COMPONENT_KEPT_ALIVE | 1 << 9 |
1 000000000 |
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
根据上表展示的基本 flags
值可以很容易地得出下表:
ShapeFlags | bitmap |
---|---|
COMPONENT | 00000001 1 0 |
区分 children 的类型
上面我们已经看到了 children
可以是数组或纯文本,但真实场景可能是:
null
- 纯文本
- 数组
这里我们可以增加一个 ChildrenShapeFlags
的变量表示 children
的类型,但是基于之前的设计原则,我们完全可以用 ShapeFlags
来表示,那么同一个 ShapeFlags
如何既用来表示 VNode
的类型,又用来表示其 children
的类型呢?
仍然是按位运算,我们通过 JavaScript 代码判断 children
类型,然后和当前 VNode
进行按位或运算即可。
我们增加如下函数用来专门处理子节点类型,这和 Vue 3 中的处理一致:
function normalizeChildren(vnode, children) {
let type = 0
if (children == null) {
children = null
} else if (Array.isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'string') {
children = String(children)
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type
}
这样我们就可以直接通过 shapeFlag
同时判断 VNode
及其 children
类型了。
为什么children
也需要标识呢?原因只有一个:为了patch
过程的优化。
定义 VNode
至此,我们可以定义 VNode
结构如下:
export interface VNodeProps {
[key: string]: any
}
export interface VNode {
// _isVNode 是 VNode 对象
_isVNode: true
// el VNode 对应的真实 DOM
el: Element | null
shapeFlag: ShapeFlags.ELEMENT,
tag: | string | Component | null,
props: VNodeProps | null,
children: string | Array<VNode>
}
实际上,Vue 3 中对 VNode
的定义要复杂的多,这里就不去细看了。
生成 VNode 的 h 函数
基本的 h 函数
首先我们实现一个最简单的 h
函数,可以是这样的,接收三个参数:
-
tag
标签名 -
props
DOM 上的属性 -
children
子节点
我们新建一个文件 h.ts
,内容如下:
function h(tag, props, children){
return {
tag,
props,
children
}
}
我们用如下的 VNode
来表示 <div class="red"><span>hello</span></div>
:
import { h } from './h'
const vdom = h('div', {
class: 'red'
}, [
h('span', null, 'hello')
])
看一下实际输出内容:
const vdom = {
"tag": "div",
"props": {
"class": "red"
},
"children": [
{
"tag": "span",
"props": null,
"children": "hello"
}
]
}
基本符合预期,但是这里有同学可能又要问了:“这个 vdom
和写的 h
函数没什么不同,为什么不直接写 VNode
?”
这是因为我们现在的 h
函数所做的仅仅就是返回传入的参数,实际上根据我们对 VNode
的定义,还缺少一些字段,不过你也可以直接写 VNode
,但这样会增加大量的额外工作。
完整的 h 函数
现在我们补全 h
函数,添加 _isVNode
、el
和 shapeFlag
字段。
function h(tag, props = null, children = null) {
return {
_isVNode: true,
el: null,
shapeFlag: null,
tag,
props,
children
}
}
这里的 _isVNode
永远为 true
,el
不是在创建 VNode
的时候赋值,所以不用处理,我们主要处理 shapeFlag
,实际上 shapeFlag
有 10 种类型,我们这里只实现一个最简单的判断:
function h(tag, props = null, children = null) {
let shapeFlag = null
// 这里为了简化,直接这样判断
if (typeof tag === 'string') {
shapeFlag = ShapeFlags.ELEMENT
} else if(typeof tag === 'object'){
shapeFlag = ShapeFlags.STATEFUL_COMPONENT
} else if(typeof tag === 'function'){
shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
}
return {
_isVNode: true,
el: null,
shapeFlag,
tag,
props,
children
}
}
现在我们需要处理一下 children
的类型了,VNode 章节中我们讲过其判断逻辑,那么 h
函数现在完整逻辑如下:
function h(tag, props = null, children = null) {
let shapeFlag = null
// 这里为了简化,直接这样判断
if (typeof tag === 'string') {
shapeFlag = ShapeFlags.ELEMENT
} else if(typeof tag === 'object'){
shapeFlag = ShapeFlags.STATEFUL_COMPONENT
} else if(typeof tag === 'function'){
shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
}
const vnode = {
_isVNode: true,
el: null,
shapeFlag,
tag,
props,
children
}
normalizeChildren(vnode, vnode.children)
return vnode
}
function normalizeChildren(vnode, children) {
let type = 0
if (children == null) {
children = null
} else if (Array.isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') {
type = ShapeFlags.SLOTS_CHILDREN
} else if (typeof children === 'string') {
children = String(children)
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type
}
现在我们重新写一个测试代码看一下 h
函数输入结果:
import { h } from './h'
const MyComponent = {
render() {}
}
const vdom = h('div', {
class: 'red'
}, [
h('p', null, 'hello'),
h('p', null, null),
h(MyComponent)
])
console.log(vdom);
// vdom:
// {
// _isVNode: true,
// el: null,
// shapeFlag: 17,
// tag: 'div',
// props: { class: 'red' },
// children: [
// {
// _isVNode: true,
// el: null,
// shapeFlag: 9,
// tag: 'p',
// props: null,
// children: 'hello'
// },
// {
// _isVNode: true,
// el: null,
// shapeFlag: 1,
// tag: 'p',
// props: null,
// children: null
// },
// {
// _isVNode: true,
// el: null,
// shapeFlag: 4,
// tag: [Object],
// props: null,
// children: null
// }
// ]
// }
至此已经完成了 h
函数的基本设计,可以得到想要的 VNode
了,下一步就是把 VNode
渲染到页面上。
渲染 VNode 的 mount 函数
得到 VNode
之后,我们需要把它渲染到页面上,这就是渲染器的 Mount
阶段。
mount 函数基本原理
首先,新建一个 render.ts
文件,用来处理挂载相关代码。
mount
函数应该是这样,接收一个 VNode
作为参数,并把生成的 DOM 放进指定的容器 container
中,实现如下:
function mount(vnode, container) {
const el = document.createElement(vnode.tag)
contianer.appendChild(el);
}
这就是挂载所做的核心事情,不过这里我们还缺少具体要实现的内容:
- 根据不同
shapeFlag
生成不同 DOM - 设置 DOM 的属性
- DOM 子节点的处理
- 生成 DOM 后需将其赋值给
vnode.el
解决 VNode
的类型问题
这里我们需要先了解一下普通有状态组件和函数式组件分别是什么,以下仅做理解用。
- 普通有状态组件
const MyComponent = {
render() {
return h('div', null, 'stateful component')
}
}
- 函数式组件
function MyFunctionalComponent() {
return h('div', null, 'function component')
}
我们根据 vnode.shapeFlag
的值来对各种类型 VNode
进行渲染操作。
function mount(vnode, container) {
if (vnode.tag === null) {
mountTextElement(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
mountElement(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
mountStatefulComponent(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
mountFunctionalComponent(vnode, container)
}
}
function mountTextElement(vnode, container) {
...
}
function mountElement(vnode, container) {
...
}
function mountStatefulComponent(vnode, container) {
...
}
function mountFunctionalComponent(vnode, container) {
...
}
渲染文本节点
function mountTextElement(vnode, container) {
const el = document.createTextNode(vnode.children)
container.appendChild(el)
}
渲染标签节点
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
container.appendChild(el)
}
渲染普通有状态组件
普通有状态组件就是一个对象,通过 render
返回其 VNode
, 因此其渲染方法如下:
function mountStatefulComponent(vnode, container) {
const instance = vnode.tag
instance.$vnode = instance.render()
mount(instance.$vnode, container)
instance.$el = vnode.el = instance.$vnode.el
}
渲染函数式组件
函数式组件的 tag
为一个函数,返回值为 VNode
,因此其渲染方法如下:
function mountFunctionalComponent(vnode, container){
const $vnode = vnode.tag()
mount($vnode, container)
vnode.el = $vnode.el
}
设置 DOM 属性
这里为了简化,这里我们假设 props
的每一项都是 DOM 的 attribute
,所以我们可以这样做:
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
if(vnode.props){
for(const key in vnode.props){
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
container.appendChild(el);
}
实际上,Vue 3 中 props
是一个扁平化的结构,它同时包含了 property
、attribute
、event listener
等,每一项都需要单独处理,如下:
props: {
id: 'div',
class: 'red',
key: 'key1',
onClick: this.onClick
}
简单解释 property
、attribute
的区别就是:attribute
是 DOM 自带的属性,如:id
、class
;property
是自定义的属性名,如:key
、data-xxx
。
渲染子节点
我们知道 children
可以是字符串或数组,因此实现方法如下:
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
// props
if(vnode.props){
for(const key in vnode.props){
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
// children
if(vnode.children){
if(typeof vnode.children === 'string'){
el.textContent = vnode.children
}else{
vnode.children.forEach(child => {
mount(child, el)
})
}
}
container.appendChild(el);
}
关联 VNode
及其 DOM
这个只需要增加一行代码即可,其它函数类似:
function mountTextElement(vnode, container) {
const el = document.createTextNode(vnode.children)
vnode.el = el // (*)
container.appendChild(el)
}
完整实现
现在我们实现了渲染器 mount
所有的功能,完整代码如下:
function mount(vnode, container) {
if (vnode.tag === null) {
mountTextElement(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
mountElement(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
mountStatefulComponent(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
mountFunctionalComponent(vnode, container)
}
}
function mountTextElement(vnode, container) {
const el = document.createTextNode(vnode.children)
vnode.el = el
container.appendChild(el)
}
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
// props
if(vnode.props){
for(const key in vnode.props){
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
// children
if(vnode.children){
if(typeof vnode.children === 'string'){
el.textContent = vnode.children
}else{
vnode.children.forEach(child => {
mount(child, el)
})
}
}
vnode.el = el
container.appendChild(el)
}
function mountStatefulComponent(vnode, container) {
const instance = vnode.tag
instance.$vnode = instance.render()
mount(instance.$vnode, container)
instance.$el = vnode.el = instance.$vnode.el
}
function mountFunctionalComponent(vnode, container){
const $vnode = vnode.tag()
mount($vnode, container)
vnode.el = $vnode.el
}
完整示例
现在我们可以检验一下写的是否正确,新建 vdom.html,添加如下代码,并在浏览器中打开:
import { h } from './h'
import { mount } from './render'
const MyComponent = {
render() {
return h('div', null, 'stateful component')
}
}
function MyFunctionalComponent() {
return h('div', null, 'function component')
}
const vdom = h('div', {
class: 'red'
}, [
h('p', null, 'text children'),
h('p', null, null),
h(MyComponent),
h(MyFunctionalComponent)
])
console.log(vdom);
mount(vdom, document.querySelector("#app"))
浏览器渲染结果,所有内容均正常显示。
至此,我们已经了解了 Vue 3 的基本渲染原理,并实现了一个简易版本的渲染器。
未完待续~(由于内容太多,后续将不在本文继续增加)
第4章 Vue 3响应式原理及实现
你能学到什么
- 了解
reactive
设计理念 - 开发独立的响应式库
本文完整内容可见,build-your-own-vue-next
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。