三、Vue.js 3的设计思路
3.1 声明式地描述UI
编写前端页面要涉及的内容
1、DOM元素
2、元素的属性
3、元素的事件
4、元素的层级结构
Vue选择了和原生基本一致的描述方式来进行模版的书写,当然我们也可以用js对象的方式来描述
3.1.1模版描述ui
<templete>
<div class="dom" :id="coustomId" @click="handler">
<div />
</div>
</templete>
3.1.2js对象描述ui
const vnode = {
// 标签名称
tag: 'div',
// 标签属性
props: {
class: 'dom',
id: coustomId
onClick: handler
},
// 子节点
children: [
{ tag: 'div' }
]
}
export default {
render() {
return vnode
}
}
利用h函数
import { h } from 'vue'
export default {
render () {
return h('h1', { onClick:handler })
}
}
3.2初始渲染器
Vue先通过渲染函数(.render ) 拿到虚拟DOM,之后再通过渲染器将虚拟DOM转化为真实DOM,如图3-1所示
const vnode = {
// 标签名称
tag: 'div',
// 标签属性
props: {
class: 'dom',
onClick: handler
},
// 子节点
children: 'click me'
}
export default {
render() {
return title
}
}
先解释一下上面的代码:
- tag用来描述标签名称,所以div就是一个标签
- props是一个对象,用来描述标签的属性、事件等内容
- children用来描述标签的子节点,上面代码中是一个字符串,所以代码有一个文本节点
接下来我们实现一个渲染器
function renderer (vnode, container) {
const { tag, props, children} = vnode
// 生成DOM节点
const el = document.createElement(tag)
// 处理props属性
for ( const key in props) {
// 如果是on开头说明是事件
if(/^on/.test(key)) {
el.addEventListener(key.slice(2).toLowerCase(), props[key])
} else {
el.setAttribute(key, props[key])
}
}
// 处理children
if (typeof children === 'string') el.appendChild(document.createTextNode(children))
else if (Array.isArray(children)) children.forEach(child => renderer(child, el))
container.appendChild(el)
}
当然这只是首次渲染,渲染器复杂的地方在于更新阶段,例如文本内容的变更从click me变更为 click again,渲染器应该只更新元素的文本内容,而不需要走一遍完整的创建过程 #### 3.3组件的本质
组件的本质就是DOM元素的封装,有点像js中的fragment切片,所以我们可以定义一个函数,函数的返回值就是这个页面要渲染的内容
const MyComponent =function () {
return {
tag: 'div',
props: {
onClick: () => alert(1)
},
children: 'click me'
}
}
重构渲染器renderer函数的tag处理部分
function renderer (vnode, container) {
// 是dom节点
if (typeof vnode.tag === 'string') mountElement(vnode, container)
// 是组件节点
else if (typeof vnode.tag === 'function') mountComponent(vnode, container)
}
mountElement是之前的renderer函数
function mountElement (vnode, container) {
const { tag, props, children} = vnode
// 生成DOM节点
const el = document.createElement(tag)
// 处理props属性
for ( const key in props) {
// 如果是on开头说明是事件
if(/^on/.test(key)) {
el.addEventListener(key.slice(2).toLowerCase(), props[key])
} else {
el.setAttribute(key, props[key])
}
}
// 处理children
if (typeof children === 'string') el.appendChild(document.createTextNode(children))
else if (Array.isArray(children)) children.forEach(child => renderer(child, el))
container.appendChild(el)
}
mountComponent如下,主要是为了生成虚拟DOM,然后再去调用renderer
function mountComponent (vnode, container) {
// 获取虚拟DOM
const sunTree = vnode.tag()
renderer(sunTree, container)
}
当然我们知道的Vue的组件展示形式是一个对象,所以最终renderer、mountComponent应该是下方这个样子的
renderer如下
function renderer (vnode, container) {
// 是dom节点
if (typeof vnode.tag === 'string') mountElement(vnode, container)
// 是组件节点
else if (typeof vnode.tag === 'object') mountComponent(vnode, container)
}
mountComponent 如下
function mountComponent (vnode, container) {
// 获取虚拟DOM
const sunTree = vnode.tag.render()
renderer(sunTree, container)
}
3.4模版的工作原理
模版的工作原理其实主要是借助于编辑器,我们知道在写vue时会导出一个对象,编译器让模版生成作用的原理就是在导出的对象上生成一个render函数,返回vnode,并在进行分析时,将可能发生变化的数据在vnode中标识出来,方便渲染器进行更新时找到哪个地方可能发生变化
{
render () {
return {
tag: 'div',
props: {
id: 'foo',
class: cls
},
patchFlags: 1 //假设数字1代表class是动态的
}
}
}