Vue3设计思路
声明式地描述UI
vue3是一个声明式框架
模板描述UI
前端页面涉及的内容:
- DOM元素,例如div标签a标签
- 属性:标签的属性,如id等
- 事件:click事件等
- 元素层级结构:DOM树的层级结构
vue3声明式表述上述内容:
- 使用HTML标签一致的方式描述DOM元素
- 使用HTML标签一致的方式描述属性
- 使用
:或v-bind动态绑定属性,如<div :id="testId"></div> - 使用
@或v-on描述事件,如<div @click="handler"></div> - 使用HTML标签一致的方式描述层级结构
手写虚拟DOM描述UI
可以使用JS对象来描述:
const title = {
tag: 'h1',
props: {
onClick: handler
},
children: [
{tag: 'span'}
]
}
//解析完:<h1 @click="handler"><span></span></div>
模板和手写虚拟DOM的不同
使用JS对象描述更加灵活:
let level = 3
cosnt title = {
tag: `h${level}`
}
使用声明式,需要穷举:
<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>
vue的选择
vue中主要使用模板描述UI,但是也支持使用虚拟DOM描述UI(也就是使用JS对象)
vue中提供的写法,手写渲染函数,vue会通过render函数的返回值拿到虚拟DOM在渲染:
import {h} from 'vue'
export default {
render(){
//这里的h函数返回了一个JS对象,目的是让我们省去写tag等属性名的代码
return h('h1', {onCilck: handle}) //虚拟DOM
}
}
渲染器
将虚拟DOM(JS对象描述真实的DOM结构)转化为真实DOM渲染到浏览器页面中
渲染器实现
其中vnode是虚拟DOM对象,container是一个真实的DOM,作为挂载点
function render(vnode, container){
// 根据tag创建标签
const el = document.createElement(vnode.tag)
// 遍历props 将属性、事件添加到DOM上
for(const key in vnode.props){
// 匹配事件
if(/^on/.test(key)){
el.addEventListener(
key.substring(2).toLowerCase(),
vnode.props[key]
)
}else{
el.setAttribute(key, vnode.props[key])
}
}
let children = vnode.children
// 处理children
if(typeof children === 'string'){
// 判断字符串的时候 创建文本节点
const textNode = document.createTextNode(children)
el.appendChild(textNode)
}else if( Array.isArray(children) ){
// 子节点 递归渲染
children.forEach( item=> render(item, el) )
}
container.appendChild(el)
}
渲染器实现思路
- 创建元素
- 为元素添加属性和事件
- 处理
children
渲染器精髓
渲染器的精髓并不在创建DOM元素,而是在更新节点的阶段
如果只是对vnode做小小的修改,并不用重走一遍创建元素的过程,只需要更改更新元素的地方
组件的本质
组件的本质就是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容
使用函数描述组件
函数返回值就是组件要渲染的内容:
const MyComponent = function(){
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
构造虚拟DOM对象:
const vnode = {
tag: MyComponent
}
使用渲染函数:
function render(vnode, container){
//说明vnode描述的是标签元素
if(typeof vnode.tag === 'string'){
mountElement(vnode, container) //实现与上述的render函数一样
}else if(typeof vnode.tag === 'function'){
//说明vnode描述的是组件
mountComponent(vnode, container) //将vnode.tag()的结果传递给render而已
}
}
使用JS对象描述组件
JS对象表达组件:
const MyComponent = function(){
render(){
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
}
其余步骤同理,略
模板的工作原理
模板的工作主要用到了另一个重要部分:编译器
编译器和渲染器一样,只是一段程序,不过工作内容不同
编辑器的作用是将模板编译成渲染函数
编译器工作
给出模板:
<div @click="handler">click me</div>
对编译器来说,模板就是字符串,转化成渲染函数:
render(){
return h('div', {onClick: handler}, 'click me')
}
vue文件的转换
将<template>标签中的内容就是模板内容,会将其编译成渲染函数并添加到<script>中
所以无论使用模板还是渲染函数,对应vue组件来说,渲染的内容最终都是通过渲染函数生成的,然后渲染器再将渲染函数返回的虚拟DOM渲染为真实DOM,这就是Vue渲染页面的流程
Vue是各个模块组成的有机整体
组件的实现依赖于渲染器,模板的编译依赖于编译器,生成后的代码都是根据渲染器和虚拟DOM设计决定的,因此各个模块相互关联
两者配合工作使性能提升:
给出模板:
<div :class="cls"></div>
编译成渲染函数:
render(){
return h('div', {class: cls})
}
现在问题:
cls是一个变量,渲染器的作用是寻找并且只更新变化的内容
所以cls变化时,渲染器需要自行寻找变更点,这样会消耗性能
解决:
编译器有能力分析动态内容,因为一眼就能看出:class="cls"是可能变化的
生成代码时可以附带这些信息:
render(){
return {
tag: 'div',
props: {
class: cls
},
patchFlags: 1 //假设数字1代表class是动态的
}
}
总结:这样能省去渲染器寻找变更点的工作量,性能自然也就提升了,而虚拟DOM对象中也会包含多种数据字段表示一定的含义