一、前言
我们从全局视角了解Vue.js 3 的设计思路、工作机制及其重要的组成部分。我们可以把这些组成部分当作独立的功能模块,看看它们之间是如何相互配合的。
- UI的两种形式:模板字符串和虚拟DOM
- Vue.js框架的两个重要组成部分:编译器和渲染器
二、本章内容
2.1 声明式地描述UI
Vue.js 3 是一个声明式的UI框架。设计一个这样的框架,我们需要了解编写前端页面都涉及哪些内容?具体如下:
- DOM元素:是
div标签还是a标签。 - 属性:如
a标签的href属性,再如id、class等通用属性 - 事件:如
click、keydown等。 - 元素的层级结构:DOM树的层级结构,既有子节点,又有父节点。
那么,如何声明式地描述上述内容呢? 在Vue.js 3 中的解决方案是:
- 使用与HTML标签一致的方式来描述DOM元素、属性与层级结构等,例如
<div id="app"><span>趋动科技</span></div>; - 使用:或v-bind来描述动态绑定的属性,使用@或v-on来描述事件例如
<div :name="name" @click="handleClick"></div>; 这样,用户不需要手写任何命令式代码,就可以实现声明式地描述UI。当然除了使用这种模板形式描述UI外,还可以用JS对象来描述:
const title = {
tag: 'div',
props: {
onClick: handleClick
},
children: [
{tag: 'span'}
]
}
对应到Vue.js模板是:
<div @click="handleClick"><span></span></div>
使用JS对象来描述比模板来说的优势是更加灵活。假如我们根据变量level的取值分别渲染h1~h6标签:
//模板
<h1 v-if="level === 1"></h1>
<h2 v-else-if="level === 2"></h2>
<h3 v-else-if="level === 3"></h3>
<h4 v-else-if="level === 4"></h4>
<h5 v-else-if="level === 5"></h5>
<h6 v-else-if="level === 6"></h6>
//JS对象描述,只需要一个变量来代表h标签即可
let level = 1; //h标签的级别
const title = {
tag:`h${level}` //h1标签
}
我们在Vue.js组件中通过手写渲染函数就是使用虚拟DOM来描述UI, h函数的返回值就是一个对象,让我们编写虚拟DOM变动更加轻松:
import { h } from 'vue'
export default {
render(){
return h('h1',{ onClick:handler }) //虚拟DOM
}
}
组件的渲染函数:一个组件要渲染的内容是通过渲染函数来描述的,也就是代码中的render函数,Vue.js会根据组件的render函数的返回值拿到虚拟DOM,然后将组件的内容渲染出来。
2.2 初识渲染器
我们可以使用JS对象来描述真实的DOM结构。那么虚拟DOM又是如何通过渲染函数转为真实DOM后,渲染到页面中的呢? 渲染器的作用就是把虚拟DOM渲染为真实DOM
假如我们有如下虚拟DOM:
const vNode = {
tag:'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
接下来,我们需要编写一个渲染器,把上面这段虚拟DOM渲染为真实DOM,renderer函数接收两个参数:vnode虚拟DOM对象与container真实DOM挂载点,渲染器会把虚拟DOM渲染到该挂载点下。
function renderer(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)
}
渲染器renderer的实现思路:
- 创建元素
- 为元素添加属性和事件
- 递归遍历children创建节点
2.3 组件的本质
组件就是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容,因此可以定义函数来描述组件本身的内容。
const MyComponent = function(){
return {
tag:'div',
props:{
onClick:() => alert('hello')
},
children:'click me'
}
}
这时候的虚拟DOM就是这样的:
const vnode = {
tag: MyComponent
}
对于处理函数和字符串的时候,render 函数也会有所不同:
// 字符串 渲染函数
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)
}
//函数渲染函数
function mountComponent(vnode, container) {
// 调用组件函数,获取组件要渲染的内容(虚拟DOM)
const subtree = vnode.tag()
// 递归调用 renderer 渲染 subtree
renderer(subtree, container)
}
//主 render 函数
function renderer(vnode, container) {
if (typeof vnode.tag === 'string') {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container)
} else if (typeof vnode.tag === 'function') {
// 说明 vnode 描述的是组件
mountComponent(vnode, container)
}
}
2.4 模板的工作原理
无论是手写虚拟 DOM还是使用模板,都是属于声明式 UI,上文中讲过,需要将虚拟 DOM 转换为真实 DOM,这一过程需要的就是编译器。 编译器和渲染器一样,就是一个程序,编译器的作用就是将模板编译成渲染函数,如下模板:
<div @click="handler">click me</div>
最终通过编译器编译,会将其转化为渲染函数:
export default {
render() {
return h('div', { onClick: handler }, 'click me')
}
}
对于一个组件来说,最终都是通过渲染函数产生的,然后渲染器把渲染函数返回的虚拟 DOM,渲染为真实 DOM,这就是模板的工作原理,也是Vue.js 渲染页面的流程。
2.5 Vue.js是各个模块组成的有机整体
在Vue.js框架设计中,组件的实现依赖于渲染器和编译器,渲染器和编译器之间是互相关联、互相制约的,它们共同构成一个有机整体,不同模块之间互相配合,进一步提升框架性能。