《Vue.js 设计与实现》—— 03 Vue.js 3 的设计思路
1. 声明式地描述 UI
Vue.js 3 是一个声明式的 UI 框架,即用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。
编写前端页面涉及的内容如下:
- DOM 元素:例如是
div标签还是a标签 - 属性:如
a标签的href属性,再如id、class等通用属性 - 事件:如 click、keydown 等
- 元素的层级结构:DOM 树的层级结构,既有子节点,又有父节点
那么,如何声明式地描述上述内容呢?拿 Vue.js 3 来说,相应的解决方案是:
- 使用与 HTML 标签一致的方式来描述 DOM 元素,例如描述一个
div标签时可以使用<div></div> - 使用与 HTML 标签一致的方式来描述属性,例如
<div id="app"></div> - 使用
:或v-bind来描述动态绑定的属性,例如<div :id="dynamicId"></div> - 使用
@或v-on来描述事件,例如点击事件<div @click="handler"></div> - 使用与 HTML 标签一致的方式来描述层级结构,例如一个具有
span子节点的div标签<div><span></span></div>。
可以看到,在 Vue.js 中,哪怕是事件,都有与之对应的描述方式。用户不需要手写任何命令式代码,这就是所谓的声明式地描述 UI。
除上述使用模板来声明式地描述 UI 外,还可以用 JavaScript 对象来描述,如:
const title = {// 标签名称tag: 'h1',// 标签属性props: {onClick: handler},// 子节点children: [{ tag: 'span' }]
}
对应到 Vue.js 模板,就是:
<h1 @click="handler"><span></span></h1>
相比模板,使用 JavaScript 对象描述 UI 更加灵活。例如,假如要表示一个标题,根据标题级别的不同,会分别采用 h1 - h6 这几个标签,如果用 JavaScript 对象来描述,只需要使用一个变量来代表 h 标签即可:
let level = 3
const title = {tag: `h${level}`, // h3 标签
}
如果使用模板来描述,就不得不穷举:
<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>
使用 JavaScript 对象来描述 UI 的方式,其实就是所谓的虚拟 DOM。
正是因为虚拟 DOM 的这种灵活性,Vue.js 3 除了支持使用模板描述 UI 外,还支持使用虚拟 DOM 描述 UI。事实上,我们在 Vue.js 组件中手写的渲染函数就是使用虚拟 DOM 来描述 UI 的,如:
import { h } from 'vue'export default {render() {return h('h1', { onClick: handler }) // 虚拟 DOM}
}
render 函数看似返回的是一个 h 函数,其实 h 函数的返回值就是一个对象,其作用是让用户编写虚拟 DOM 变得更加轻松。如果把上面 h 函数调用的代码改成 JavaScript 对象,就需要写更多内容:
export default {render() {return {tag: 'h1',props: { onClick: handler }}}
}
如果还有子节点,那么需要编写的内容就更多了,所以 h 函数就是一个辅助创建虚拟 DOM 的工具函数,仅此而已。
一个组件要渲染的内容是通过渲染函数来描述的,即上面的 render 函数,Vue.js 会根据组件的 render 函数的返回值拿到虚拟DOM,然后就可以把组件的内容渲染出来了。
2. 初识渲染器
虚拟 DOM 其实就是用 JavaScript 对象来描述真实的 DOM 结构。那么,虚拟 DOM 是如何变成真实 DOM 并渲染到浏览器页面中的呢?答案是渲染器。渲染器的作用就是把虚拟 DOM 渲染为真实 DOM,我们平时编写的 Vue.js 组件都是依赖渲染器来工作的。
假设有如下虚拟 DOM:
const vnode = {tag: 'div', // 标签名称props: { // 标签对应的属性和事件onClick: () => alert('hello')},children: 'click me' // 标签的子节点
}
接下来,需要编写一个渲染器,把上面这段虚拟 DOM 渲染为真实 DOM:
// vnode -- 虚拟 DOM 对象
// container -- 真实的 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 -> clickvnode.props[key] // 事件处理函数)}}// 处理 childrenif (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 函数:
renderer(vnode, document.body) // body 作为挂载点
创建节点只是“前菜”,渲染器的精髓都在更新节点的阶段。假设对 vnode 做一些小小的修改:
const vnode = {tag: 'div',props: {onClick: () => alert('hello')},children: 'click again' // 从 click me 改成 click again
}
对于渲染器来说,它需要精确地找到 vnode 对象的变更点并且只更新变更的内容。就上例来说,渲染器应该只更新元素的文本内容,而不需要再走一遍完整的创建元素的流程(这些内容将在后面讲解)。事实上,渲染器的工作原理其实很简单,归根结底都是使用一些熟悉的 DOM 操作 API 来完成渲染工作。
3. 组件的本质
关于组件,有三个问题:
- 什么是组件?
- 组件和虚拟 DOM 有什么关系?
- 渲染器如何渲染组件?
其实虚拟 DOM 除了能够描述真实 DOM 之外,还能够描述组件。例如使用 { tag: 'div' } 来描述 <div> 标签,但是组件并不是真实的 DOM 元素,如何使用虚拟 DOM 来描述呢?
组件的本质是一组 DOM 元素的封装,这组 DOM 元素就是组件要渲染的内容,因此可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容:
const MyComponent = function () {return {tag: 'div',props: {onClick: () => alert('hello')},children: 'click me'}
}
可以看到,组件的返回值也是虚拟 DOM,它代表组件要渲染的内容。可以让虚拟 DOM 对象中的 tag 属性来存储组件函数:
const vnode = {tag: MyComponent
}
就像 tag: 'div' 用来描述 <div> 标签一样,tag: MyComponent 用来描述组件,只不过此时的 tag 属性不是标签名称,而是组件函数。为了能够渲染组件,需要渲染器的支持。
前面封装的 renderer 函数仅能够渲染非组件(标签)元素,因此可以改名为 mountElement,内容不变,然后修改 renderer 函数为:
function renderer(vnode, container) {if (typeof vnode.tag === 'string') {// 说明 vnode 描述的是标签元素mountElement(vnode, container)} else if (typeof vnode.tag === 'function') {// 说明 vnode 描述的是组件mountComponent(vnode, container)}
}
mountComponent 函数实现如下:
function mountComponent(vnode, container) {// 调用组件函数,获取组件要渲染的内容(虚拟 DOM)const subtree = vnode.tag()// 递归地调用 renderer 渲染 subtreerenderer(subtree, container)
}
组件一定非得是函数吗?其实它完全可以是一个 JavaScript 对象,例如:
// MyComponent 是一个对象
const MyComponent = {render() { // render 函数的返回对象表示组件要渲染的内容return {tag: 'div',props: {onClick: () => alert('hello')},children: 'click me'}}
}
为了完成组件的渲染,需要修改 renderer 渲染器以及 mountComponent 函数。
首先,修改渲染器的判断条件:
function renderer(vnode, container) {if (typeof vnode.tag === 'string') {mountElement(vnode, container)} else if (typeof vnode.tag === 'object') { // 如果是对象,说明 vnode 描述的是组件mountComponent(vnode, container)}
}
接着,修改 mountComponent 函数:
function mountComponent(vnode, container) {// vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟 DOM)const subtree = vnode.tag.render()// 递归地调用 renderer 渲染 subtreerenderer(subtree, container)
}
4. 模板的工作原理
无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明式地描述 UI,并且 Vue.js 同时支持这两种描述 UI 的方式。那么模板是如何工作的呢?答案是编译器。编译器和渲染器一样,只是一段程序而已,不过它们的工作内容不同。编译器的作用是将模板编译为渲染函数,例如:
<div @click="handler">click me
</div>
对于编译器来说,模板就是一个普通的字符串,它会分析该字符串并生成一个功能与之相同的渲染函数:
render() {return h('div', { onClick: handler }, 'click me')
}
以 .vue 文件为例,一个 .vue 文件就是一个组件,如:
<template><div @click="handler">click me</div>
</template><script>
export default {data() {/* ... */},methods: {handler: () => {/* ... */}}
}
</script>
其中 <template> 标签里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到 <script> 标签块的组件对象上,所以最终运行的代码是:
export default {data() {/* ... */},methods: {handler: () => {/* ... */}},render() {return h('div', { onClick: handler }, 'click me')}
}
所以,无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。
5. Vue.js 是各个模块组成的有机整体
前面说到,组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟 DOM 的设计决定的,因此 Vue.js 的各个模块之间是互相关联、互相制约的,共同构成一个有机整体。
下面以编译器和渲染器这两个非常关键的模块为例,看看它们是如何配合工作,并实现性能提升的。假设有如下模板:
<div id="foo" :class="cls"></div>
编译器会把这段代码编译成渲染函数:
render() {// 下面的代码等价于:return h('div', { id: 'foo', class: cls })return {tag: 'div',props: {id: 'foo',class: cls // cls 是一个变量,可能会发生变化}}
}
渲染器的作用之一就是寻找并且只更新变化的内容,所以当变量 cls 的值发生变化时,渲染器会自行寻找变更点。对于渲染器来说,这个“寻找”的过程需要花费一些力气。
从编译器的视角来看,它能否知道哪些内容会发生变化呢?如果编译器有能力分析动态内容,并在编译阶段把这些信息提取出来,然后直接交给渲染器,这样渲染器不就不需要花费大力气去寻找变更点了吗?这是个好想法并且能够实现。
Vue.js 的模板是有特点的,以上面的代码为例,我们可以清楚的知道 id="foo" 是永远不会变化的,而 :class="cls" 是一个 v-bind 绑定,是可能发生变化的。所以编译器能识别出哪些是静态属性,哪些是动态属性,在生成代码的时候完全可以附带这些信息:
render() {return {tag: 'div',props: {id: 'foo',class: cls},patchFlags: 1 // 假设数字 1 代表 class 是动态的}
}
渲染器直接通过 patchFlags 判断变更,不需要自己“寻找”,性能自然就提升了。
因此,编译器和渲染器之间是存在信息交流的,它们互相配合使得性能进一步提升,而它们之间交流的媒介就是虚拟 DOM 对象。
更多文章可关注:GopherBlog、GopherBlog副站
相关文章:
《Vue.js 设计与实现》—— 03 Vue.js 3 的设计思路
1. 声明式地描述 UI Vue.js 3 是一个声明式的 UI 框架,即用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。 编写前端页面涉及的内容如下: DOM 元素:例如是 div 标签还是 a 标签属性:如 a 标签的 href 属性,再…...
2023年湖北省建设厅特种作业操作证报名条件是什么?
建筑施工特种作业人员是指在房屋建筑和市政工程施工活动中,从事可能对本人、他人及周围设备设施的安全造成重大危害作业的人员。建筑施工特种作业人员必须经建设主管部门考核合格,取得建筑施工特种作业人员操作资格证书(以下简称“资格证书”…...
Redis 进阶
🥲 🥸 🤌 🫀 🫁 🥷 🐻❄️🦤 🪶 🦭 🪲 🪳 🪰 🪱 🪴 🫐 🫒 🫑…...
伙伴匹配系统笔记---02
Java 8特性 1. stream / parallelStream 流失处理 2. Optional 可选类 一. 前端整合路由 1. 路由:vue 路由组件库地址:安装 | Vue Router (vuejs.org) 安装:yarn add vue-router@4 2. 整合路由: // 1. 定义路由组件. // 也可以从其他文件导入 const Home = { templ…...
Redis学习——单机版安装
目录 1.解压 2.安装gcc 3.执行make命令 4.复制redis的配置文件到默认安装目录下 5.修改redis.conf文件 6.启动redis服务与客户端 7.查看redis进行是否启动 8.关闭redis服务 9.redis性能测试 注意:安装redis前要安装jdk。 1.解压 [rootlxm148 install]# t…...
第三十一章 React中路由组件和一般组件
在React中,组件是应用程序的构建块。它们是可重用的,可以用于创建复杂的UI。React中有两种类型的组件:路由组件和一般组件。 一般组件 一般组件是React应用程序的基本构建块。它们是可重用的,可以用于创建复杂的UI。它们不知道U…...
怎么把pdf中的某一页分出来?
怎么把pdf中的某一页分出来?PDF格式的文档在日常生活中是非常常见的,相信大家都对其有所了解,并且经常使用。它的主要特点是不允许用户随意编辑其中的内容,当我们仅需要阅读时,PDF文档无疑是十分方便的,尤其…...
MongoDB 聚合操作Map-Reduce
这此之前已经对MongoDB中的一些聚合操作进行了详细的介绍,主要介绍了聚合方法和聚合管道;如果您想对聚合方法和聚合管道进行了解,可以参考: MongoDB 数据库操作汇总https://blog.csdn.net/m1729339749/article/details/130086022…...
shiro CVE-2016-4437 漏洞复现
shiro Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序漏洞原理 在Apache shiro的框架中,执行身份验证时提供了…...
Seqkit-2.2.0 移植指南(openEuler 20.03 LTS SP3)
1.软件介绍 seqkit是一种跨平台的、极快的,全面的fasta/q处理工具。seqkit为所有的主流操作系统提供了一种可执行的双元文件,包括Windows,Linux,MacOS X,并且不依赖于任何的配置或预先配置就可以直接使用。 关于seqk…...
Java版本企业电子招投标采购系统源码——功能模块功能描述+数字化采购管理 采购招投标
功能模块: 待办消息,招标公告,中标公告,信息发布 描述: 全过程数字化采购管理,打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力,为外部供…...
二十三种设计模式第五篇--原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建…...
阿里云镜像区别公共镜像、自定义、共享、云市场和社区镜像介绍
阿里云服务器镜像根据来源不同分为公共镜像、自定义镜像、共享镜像、云市场镜像和社区镜像,一般没有特殊情况选择公共镜像,公共镜像是阿里云官网提供的正版授权操作系统,云市场镜像是在纯净版操作系统的基础上预装了相关软件及运行环境&#…...
非线性方程二分法
非线性方程二分法 优点:算法直观、简单、总能保证收敛;局限:收敛速度慢、一般不单独用它求根,仅为了获取根的粗略近似 文章目录 非线性方程二分法[toc]1 二分法基本思想2 二分法实现 1 二分法基本思想 设 f ( x ) f(x) f(x)在 [ …...
H3C防火墙单机旁路部署(网关在防火墙)
防火墙旁路部署在核心交换机上,内网有三个网段vlan 10:172.16.10.1/24、vlan 20:172.16.20.1/24、vlan30:172.16.30.1。要求内网网关在防火墙设备上,由防火墙作为DHCP服务器给终端下发地址,同时由防火墙来控…...
基于密度的无线传感器网络聚类算法的博弈分析(Matlab代码实现)
目录 💥1 概述 📚2 运行结果 🎉3 参考文献 👨💻4 Matlab代码 💥1 概述 提高能源效率是无线传感器网络面临的关键挑战之一,无线传感器网络日益普遍。由于节点(传感器ÿ…...
宕机了?!DolphinScheduler 高可用和 Failover 机制关键时刻保命
高可用性是 Apache DolphinScheduler 的特性之一。它通过冗余来避免单点问题,所有组件天然支持横向扩容;但仅仅保证了冗余还不够,当系统中有节点宕机时,还需要有故障转移机制能够自动将宕机节点正在处理的工作转移到新节点上执行&…...
try(){}用法try-with-resources、try-catch-finally
属于Java7的新特性。 经常会用try-catch来捕获有可能抛出异常的代码。如果其中还涉及到资源的使用的话,最后在finally块中显示的释放掉有可能被占用的资源。 但是如果资源类已经实现了AutoCloseable这个接口的话,可以在try()括号中可以写操作资源的语句(…...
常见Http错误码学习
常见 http 错误码 服务器巡检时比较常见的 http 错误码 400 Bad Request408 Request Timeout499 client has closed connection502 Bad Gateway504 Gateway Timeout 这些错误码反映了服务器什么样的状态,仅看字面意思还不太容易理解,就动手做个试验…...
qemu-基础篇——ARM 链接过程分析(六)
文章目录 ARM 链接过程分析源文件global_bss_file.cglobal_data_fle.cglobal_function_file.cglobal_rodata_file.cmain.c 链接文件 link.lds编译命令及反汇编命令解析 .o 文件global_bss_file.oglobal_data_fle.oglobal_function_file.oglobal_rodata_file.omain.o 链接观察链…...
别再死记硬背了!用Multisim仿真+图解,5分钟搞懂三极管共射放大电路工作原理
用Multisim仿真图解5分钟掌握三极管共射放大电路三极管共射放大电路是电子技术中最基础也最关键的电路之一,但传统教材中复杂的公式推导和静态图解往往让初学者望而生畏。本文将带你用Multisim仿真软件,通过可视化的方式直观理解电路工作原理,…...
蓝牙抓包不求人:从HCI日志里‘挖’出Link Key的两种实用方法(附安卓路径)
蓝牙安全逆向实战:从HCI日志中提取Link Key的深度解析在蓝牙协议安全研究领域,Link Key作为设备配对认证的核心凭证,其获取方式一直是逆向工程师关注的焦点。许多安全审计场景下,我们往往只能获得加密后的HCI通信日志,…...
力扣HOT100(30)两两交换链表中的节点
链表的交换要注意 “链表不断链”。前驱和后继都要连着迭代法(必学死磕!O (n) 时间,O (1) 空间)1. 为什么必须用虚拟头节点?因为交换后链表的头节点会变! 比如示例 1 中,原来的头是 1࿰…...
别急着扔!12年老ThinkPad X230升级SSD和内存后,Win10流畅得像新电脑
12年老ThinkPad X230重生指南:极简升级打造流畅办公利器每次打开抽屉看到那台积灰的ThinkPad X230,总有种说不出的情感。这款2012年问世的经典商务本,曾陪伴无数人度过加班到凌晨的夜晚。如今性能确实有些力不从心,但直接丢弃又觉…...
告别元素变动导致的报错:探索自动化测试脚本的 AI“自愈”能力
前言:一个所有测试人都经历过的噩梦 周三晚上十一点,CI/CD流水线再次亮起红灯。 你打开日志,满屏的NoSuchElementException扑面而来。仔细一看——前端团队在昨天的版本中重构了登录页面的DOM结构,原本的#login-btn变成了#signin-button-v2,30个测试用例因此全军覆没。 …...
全球无障碍宣传日:iOS 26 辅助功能大升级,这些实用小功能你用过吗?
辅助功能发展与升级很多人对辅助功能的印象还停留在 "小白点",但随着 iPhone 进入全面屏时代,它逐渐变得陌生。实际上,Apple 每年都会为其增添功能,方便身体有障人士使用 iPhone。而且,这些功能不仅惠及有障…...
别再手动维护接口文档了!用Spring Boot 3和Swagger 3实现代码与文档的自动同步
Spring Boot 3与Swagger 3:构建零维护成本的API文档工作流 每次接口变更都要手动更新文档?团队成员总是抱怨文档与实际接口不一致?在敏捷开发时代,传统文档维护方式已成为拖累工程效率的典型痛点。本文将揭示如何通过Spring Boot …...
Sora 2 GIF导出速度提升300%?20年多媒体架构师亲授GPU加速转码链路(CUDA 12.4 + cuVID硬编实测)
更多请点击: https://kaifayun.com 第一章:Sora 2 GIF导出方法概览 Sora 2 并非 OpenAI 官方发布的模型,当前(截至2024年)并无名为“Sora 2”的公开产品。因此,所谓“Sora 2 GIF导出”实为社区对视频生成工…...
UE5 Cesium项目里,如何把默认的飞行Pawn换成建筑漫游Pawn?保姆级迁移教程
UE5 Cesium项目建筑漫游Pawn迁移实战:从飞行模式到精细化浏览的完整指南当你在UE5中结合Cesium插件构建数字孪生场景时,DynamicPawn提供的全球飞行体验令人印象深刻。但当视角聚焦到单体建筑或室内空间时,那种仿佛操控无人机般的操作方式就显…...
终极指南:三步搞定Windows系统安卓APK文件安装,告别模拟器时代
终极指南:三步搞定Windows系统安卓APK文件安装,告别模拟器时代 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 还在为电脑无法直接运行手机应用…...
