3. Vue.js 3 的设计思路
3.1 声明式地描述 UI
在 Vue.js 中,哪怕是事件,都有与之对应的描述方式。用户不需要手写任何命令式代码,这就是所谓的声明式地描述 UI。
js
// 模版写法
<h1 @click="handler"><span></span></h1>
// 渲染函数写法
import { h } from 'vue'
export default {
render() {
return h('h1', { onClick: handler },{ h('span')}) // h函数的返回值就是虚拟 DOM
}
}
// 虚拟DOM
const title = {
// 标签名称
tag: 'h1',
// 标签属性
props: {
onClick: handler
},
// 子节点
children: [
{ tag: 'span' }
]
}
虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观
3.2 初识渲染器
渲染器的作用就是把虚拟 DOM 渲染为真实 DOM
js
function mountElement(vnode, container) {
// 使用 vnode.tag 作为标签名称创建 DOM 元素
const el = document.createElement(vnode.tag);
// 遍历 vnode.props,将属性、事件添加到 DOM 元素
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以字符串 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
vnode.props[key] // 事件处理函数
);
}
}
// 处理 children
if (typeof vnode.children === "string") {
// 如果 children 是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children));
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach((child) => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}
3.3 组件的本质
- 组件就是一组 DOM 元素的封装
- 组件的返回值也是虚拟 DOM,它代表组件要渲染的内容
- 虚拟 DOM 其实就是用来描述真实 DOM 的普通 JavaScript 对象
js
// 创建组件
const MyComponent = function () {
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
};
// 创建虚拟DOM
const vnode = {
tag: MyComponent,
};
// 渲染组件
function mountComponent(vnode, container) {
// 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
const subtree = vnode.tag();
// 递归地调用 renderer 渲染 subtree
renderer(subtree, container);
}
// 渲染函数
function renderer(vnode, container) {
if (typeof vnode.tag === "string") {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container);
} else if (typeof vnode.tag === "function") {
// 说明 vnode 描述的是组件
mountComponent(vnode, container);
}
}
3.4 模板的工作原理
模板内容会通过编译器编译成渲染函数并添加到 <script>
标签块的组件对象上
vue
<template>
<div @click="handler">click me</div>
</template>
<script>
export default {
data() {
/* ... */
},
methods: {
handler: () => {
/* ... */
},
},
};
</script>
vue
// 编译后的文件
<script>
export default {
data() {/* ... */},
methods: {
handler: () => {/* ... */}
}
render() {
return h('h1', { onClick: handler }, 'click me' ) // 虚拟 DOM
}
}
</script>
无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理
3.5 Vue.js 是各个模块组成的有机整体
渲染器的作用之一就是寻找并且只更新变化的内容,当变量
cls
的值发生变化时,渲染器会自行寻找变更点编译器能识别出哪些是静态属性,哪些是动态属性,在生成代码的时候附带这些信息
编译器和渲染器之间是存在信息交流的,它们互相配合使得性能进一步提升,而它们之间交流的媒介就是虚拟 DOM 对象
vue
<div id="foo" :class="cls"></div>
js
render() {
// 为了效果更加直观,这里没有使用 h 函数,而是直接采用了虚拟 DOM 对象
// 下面的代码等价于:
// return h('div', { id: 'foo', class: cls })
return {
tag: 'div',
props: {
id: 'foo',
class: cls
}
patchFlags: 1 // 假设数字 1 代表 class 是动态的
}
}