Vue.js 3 是一个声明式的 UI 框架,意思是说用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。如何设计一个声明式框架呢?

一、声明式描述 UI

vue.js 3 是一个声明式的 UI 框架,如何设计 UI 框架,需要明确前端页面涉及的内容:

1、DOM 元素:例如标签;
2、属性:例如 class、id、style、src 等;
3、事件:例如 click、mouseover、keydown 等;
4、元素层级结构:DOM 树的层级结构,既有子节点,又有父节点。

如何声明式的描述上面前端页面涉及的内容呢?vue.js 3 解决方案:
1、使用 HTML 标签一致的方式来描述 dom 元素,例如:<div></div>;
2、使用 HTML 标签一致的方式来描述属性,例如:<div class='app'></div>;v-bind 来描述动态绑定的属性,例如:<div :id="wrapper"></div>;
3、使用 v-on 来描述事件,例如:<div @click="handleClick"></div>;
4、使用 HTML 标签一致的方式来描述元素层级结构,例如:<div><span></span></div>;

js 对象描述 UI 页面:

1
2
3
4
5
6
7
8
9
10
const title = {
// 标签名称
tag: 'h1',
// 标签属性
props: {
onClick: handler
},
// 子节点
children: [{ tag: 'span' }]
}

对应的 vue.js 模板就是:

1
<h1 @click="handler"><span></span></h1>

js 对象来描述 UI 更具灵活性

1
2
3
4
5
//模板表示: js对象描述h1~h6标签,标签对应的名字会随着level值改变而变化
let level = 3
const title = {
tag: `h${level}`
}

vue.js 使用 JS 对象来描述 UI 的方式,其实就是所谓的虚拟 DOM。其实我们在 vue.js 组件中手写渲染函数就是使用虚拟 DOM 来描述 UI 的。

1
2
3
4
5
6
import { h } from 'vue'
export default {
render() {
return h('div', { onClick: handler })
}
}

上面的 h 函数返回值就是一个对象,其作用是让我们编写虚拟 DOM 变的更加轻松。

如果把上面的 h 函数调用嗲买改成 js 对象,就需要写更多内容了:

1
2
3
4
5
6
7
8
9
10
export default {
render() {
return {
tag: 'h1',
props: {
onClick: handler
}
}
}
}

如果还有子节点,写的内容就更多了,所以 h 函数就是一个辅助创建虚拟 dom 的工具函数,仅此而已。

什么是组件的渲染函数?

一个组件要渲染的内容是通过渲染函数来描述的,就是上面的 render 函数,vue.js 会根据组件的 render 函数的返回值拿到虚拟 dom,然后就可以把组件的内容渲染出来了。

二、渲染器

我们知道了虚拟 DOM 其实就是用 js 对象来描述真实的 dom 结构,那么 vue.js 是如何把虚拟 DOM 渲染成真实的 DOM 呢?

渲染器的作用就是把虚拟 DOM 渲染为真实 DOM。

编写一个渲染器,实现点击 div 标签,alert hello 功能

1
2
3
4
5
6
7
8
// 假设一个虚拟DOM
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me' // 用来描述标签的子节点
}

渲染器将上面虚拟 DOM 渲染为真实 DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function renderer(vnode, container) {
// 使用vnode.tag作为标签名称创建DOM元素
const el = document.createElement(vnode.tag)

// 遍历vnode.props,把props中的属性、事件添加到el中
for (const key in vnode.props) {
// 如果key以on开头,说明是事件
if (/^on/.test(key)) {
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称
vnode.props[key] // 事件处理函数
)
}
}
// 处理children
// 如果vnode.children是字符串,说明是文本子节点
if (typeof vnode.children === 'string') {
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归调用renderer函数渲染子节点,使用当前元素el作为挂载节点
vnode.children.forEach((child) => renderer(child, el))
}
// 将元素添加到挂载点下
container.appendChild(el)
}

renderer(vnode, document.body)

renderer 函数的 2 个参数说明:

  • vnode:虚拟 DOM 对象
  • container:真实 DOM 元素,作为挂载点,渲染器会把虚拟 DOM 渲染到该挂载点下。

renderer 渲染器的实现思路:

创建元素:把 vnode.tag 作为标签名称来创建 DOM 元素。

为元素添加属性和事件:遍历 vnode.props 对象,如果 key 以 on 字符开头,说明它是一个事件,把字符 on 截取掉后再调用 toLowerCase 函数将事件名称小写化,最终得到合法的事件名称,例如 onClick 会变成 click,最后调用 addEventListener 绑定事件处理函数。

处理 children:如果 children 是一个数组,就递归地调用 renderer 继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果 children 是字符串,则使用
createTextNode 函数创建一个文本节点,并将其添加到新创建的元素内。

三、组件的本质

1. 组件的定义

组件就是一组 DOM 元素的封装,这组 DOM 元素就是组件要渲染的内容,因此
我们可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容: