设计思路
声明式地描述 UI
vue 是一个声明式的 UI 框架
描述页面需要哪些
- DOM 元素
<a/> <div/>
- 属性 class id src
- 事件 click 按键
- 元素层级结构
vue3 的解决方案
- 一致
<a></a> <div></div>
- 一致 还可以使用v-bind 绑定 id="123" :id="demo"
- 使用@ 或者 v-on绑定 @click
- 一致
<div><span></span></div>
可以声明式的描述 UI(模板),当然 vue 也可以使用对象描述
js
const title = {
// 标签名称
tag: 'h1',
// 标签属性
props: {
onClick: handler
},
// 子节点
children: [
{ tag: 'span' }
]
}
对应到模板
vue
<h1 @click="handler"><span></span></h1>
模板 和 对象描述的区别,
- 对象更加灵活
- 模板更加直观
对于 循环 判断等 对象更加方便
vue 使用 h 函数,方便创建 js 对象,使创建虚拟 dom 更加方便
js
import { h } from 'vue'
export default {
render() {
return h('h1', { onClick: handler }) // 虚拟 DOM
}
}
- 渲染函数。一个组件要渲染的内容是通过渲染函数来描述的,也就是上面代码中的 render 函数,Vue.js 会根据组件的 render 函数的返回值拿到虚拟 DOM,然后就可以把组件的内容渲染出来了。
初识渲染器
作用: 就是把虚拟 DOM 渲染为真实 DOM
h("div","hello") => 渲染器 => 真是 dom (<div>hello</div>)
简单实现一个简单的渲染器 先写一个简单的虚拟 dom
js
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
- 首先简单解释一下上面这段代码
- tag 用来描述标签名称,所以 tag: 'div' 描述的就是一个 标签。
- props 是一个对象,用来描述 标签的属性、事件等内容。可以看到,我们希望给 div 绑定一个点击事件。
- children 用来描述标签的子节点。在上面的代码中,children 是一个字符串值,意思是 div 标签有一个文本子节点:click me
实际上,你完全可以自己设计虚拟 DOM 的结构,例如可以使用 tagName 代替 tag,因为它本身就是一个 JavaScript 对象,并没有特殊含义。
下面是一个简单渲染器
js
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 函数接收如下两个参数。
- vnode:虚拟 DOM 对象。
- container:一个真实 DOM 元素,作为挂载点,渲染器会把虚拟 DOM 渲染到该挂载点下。
现在我们回过头来分析渲染器 renderer 的实现思路,总体来说分为三步。
- 创建元素:把 vnode.tag 作为标签名称来创建 DOM 元素。
- 为元素添加属性和事件:遍历 vnode.props 对象,如果 key 以 on 字符开头,说明它是一个事件,把字符 on 截取掉后再调用 toLowerCase 函数将事件名称小写化,最终得到合法的事件名称,例如 onClick 会变成 click,最后调用 addEventListener 绑定事件处理函数。
- 处理 children:如果 children 是一个数组,就递归地调用 renderer 继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果 children 是字符串,则使用 createTextNode 函数创建一个文本节点,并将其添加到新创建的元素内。
组件的本质
组件就是一组 DOM 元素的封装
js
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
修改 renderer
js
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.tag === 'string') {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container)
} else if (typeof vnode.tag === 'function') {
// 说明 vnode 描述的是组件
mountComponent(vnode, container)
}else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach(child => renderer(child, el))
}
// 将元素添加到挂载点下
container.appendChild(el)
}
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)
}
js
function mountComponent(vnode, container) {
// 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
const subtree = vnode.tag()
// 递归地调用 renderer 渲染 subtree
renderer(subtree, container)
}
Vue.js 中的有状态组件就是使用对象结构来表达的。
js
// MyComponent 是一个对象
const MyComponent = {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
}
编译器
编译器的作用其实就是将模板编译为渲染函数
html
<div @click="handler">
click me
</div>
转换为
js
render() {
return h('div', { onClick: handler }, 'click me')
}
对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程
编译器需要知道计算出哪些是变化的 哪些是固定的