Vue.js是一种被广泛使用的JavaScript框架,用于构建用户界面和单页面应用。Vue3是其最新的主要版本,引入了许多新特性并做了一些改进。
一、Vue3 性能提升
1. Object.defineProperty VS Proxy
Vue2 和 Vue3 在数据响应性系统的实现上采用了不同的方式:Vue2 使用 Object.defineProperty,而 Vue3 则选择了 Proxy。
(1) Vue2:Object.defineProperty
在 Vue 2 中,数据响应性系统的核心是通过 Object.defineProperty 来实现的。当我们在Vue组件中定义 data 函数返回的对象时,Vue 会遍历这个对象的每个属性,并使用 Object.defineProperty 将它们转变为 getter/setter。这使得 Vue 可以在访问或修改这些属性时追踪依赖关系和发出更改通知。
然而,Object.defineProperty 有一些限制:
它无法检测到对象属性的添加或删除。这意味着如果在创建Vue组件后动态添加新的根级数据属性,Vue 将无法追踪这个属性的更改。
它无法检测数组的变动,除非使用 Vue.js 指定的方法(如 push,pop 和 Vue.set 等)。
(2) Vue3:Proxy
在 Vue3 中,数据响应性系统转向使用 ES6 中的 Proxy 对象。Proxy 对象能够在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。
Proxy 的优势包括:
Proxy 可以拦截对象的任何操作,这包括属性的添加和删除,这样就解决了 Object.defineProperty 的主要限制。
Proxy 可以直接处理数组的变动,而无需使用特殊方法。
但是,Proxy 的主要问题是它不能在低版本的 JavaScript 环境中被 polyfill。这意味着 Vue3 无法在不支持 Proxy 的旧浏览器(如IE11)中运行,除非使用兼容性构建版本,但那将带来额外的性能和包大小成本。
2. Virtual DOM 重构
(1) 传统 Virtual DOM 的性能瓶颈
虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vDom 树。
传统 vDom 的性能与模板大小正相关,与动态节点的数量无关。在一些组件整个模板内只有少量动态节点的情况下,这些遍历都是性能的浪费。
根本原因在于,JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足。
(2) Vue3 的优化——动静结合
静态树提升:在 Vue3 中,编译器可以检测到模板中的静态内容(即不会改变的内容),并将其提升出虚拟DOM渲染函数,这样在重新渲染时就无需再次处理静态内容。这大大提高了渲染性能。
静态属性提升:类似地,Vue3 编译器还可以将静态的根级别节点属性提升出渲染函数,这样在diffing过程中就无需再次处理这些属性。
块级别的渲染:Vue3 的新编译策略将模板中的代码分割为独立的“块”,每个块都有自己的更新函数。当状态改变时,Vue3 可以更精确地知道哪个块需要更新,从而减少不必要的渲染工作。
更好的事件处理:在 Vue3 中,事件监听器绑定在组件的根元素上,而不是在每个元素上。这样可以减少事件监听器的数量,提高性能。
Fragments:Vue3 支持 Fragments,这意味着组件可以有多个根节点,这在虚拟DOM中提供了更多灵活性。
(3) 新策略的提升
新策略将 vDom 更新性能由与模板整体大小相关提升为与动态内容的数量相关。
官方的测试中,Vue3 的更新效率是 Vue2 同等量级的6倍。
3. 更多编译时的优化
(1) Slot 默认编译为函数
在 Vue2 中,插槽内容是在父组件的上下文中编译的。然而,这种方法有一些限制,比如你不能直接访问子组件的数据和方法,除非子组件将它们作为插槽的 props 提供。
在 Vue3 中,插槽的行为发生了改变。所有的插槽现在默认会被编译为函数。这种新的插槽语法被称为“编译时插槽”,它可以改进性能,因为插槽的内容只在需要的时候才会被求值和渲染。
此外,这种新的插槽语法还解决了 Vue2 中的一些限制。例如,现在我们可以在插槽内容中直接访问子组件的数据和方法,就像它们是在子组件的上下文中被编译的一样。这让我们可以更灵活地定制插槽的内容。
然而,这种新的插槽语法也可能引入一些新的问题,比如作用域混淆。为了解决这个问题,Vue3 推荐在模板中明确地标记插槽的 props,以区分父组件和子组件的作用域。
(2) Monomorphic vnode factory
monomorphic vnode factory 是 Vue 3 在虚拟 DOM 系统中的一种优化策略,它通过生成具有相同形状的虚拟节点来提高性能和内存使用效率。
Vue3 对虚拟节点工厂函数的处理做了优化,通过引入monomorphic vnode factory。简单地说,这种工厂函数生成的虚拟节点始终具有相同的形状(或称为类型)。这使得 JavaScript 引擎可以更有效地优化这些函数,从而提高应用的性能。
此外,这种优化还使得虚拟节点的内存占用更少,因为相同形状的虚拟节点可以共享一些内部数据结构。
(3) Compiler-generated flags for vnode/children types
在 Vue3 中,编译器生成的标志(flags)用于表示虚拟节点或其子节点的类型。这些标志在编译时生成,它们提供了有关虚拟节点结构的信息,使得渲染引擎能够更有效地处理虚拟节点。
二、Compiler 优化细节
1. diff 算法优化
先来介绍一个 Vue3 的模板编译器工具,我们可以在左侧写 Vue3 的模板,会将它实时编译成一个渲染函数,接下来我们要做的就是侦测渲染函数的改变。
来看一个简单的案例,根节点为一个 div,有两个子节点,其中一个子节点的内容为静态的,另一个为动态的。
<div>
<span>hello world</span>
<span>{{ message }}</span>
</div>
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
// Check the console for the AST
来看渲染函数,Vue2 中我们使用 createElement 来返回子节点,现在我们使用 createElementBlock 来返回一个区块。第一个参数代表当前组件的根节点,第二个参数看它有没有属性,没有就设置为 null。第三个参数为一个数组,内容为当前根节点下的子节点。
第一个子节点 span,_createElementVNode("span", null, "hello world"),第一个参数代表标签类型为 span,第二个参数代表没有属性,第三个参数代表内容只有文本节点 "hello world"。
第二个子节点是一个带有动态绑定的 span,_createElementVNode("span", null, _toDisplayString(_ctx.message), 1 /* TEXT */),这里有一个数字1,这个数字1就是一个 flag,编译时生成的标记。标记不是只有1,可以是1~9,这些标记统一称为 patchFlags。
在这个示例中,div 是一个区块,在这个区块中,只有标记了 patchFlag 的 vNode 才会被真正的追踪。后续 message 的值发生了更新,就会直接找到 div 这个区块,跳转到它动态有 flag 标记的 vNode 上。接下来通过 flag 就可以清楚的知道下面要去比较什么内容,什么内容为动态的。这里 flag 倍设为1,我们就知道当前 span 里面的文本内容可能会发生改变,因为文本内容的 flag 标记就为1。在这个 span 中可能还会有一些其他内容和子节点,这些其他内容这里就不管,这里只需要知道被标记为1的这个文本内容可能会改变,就可以了。
接下来我们再添加一些静态节点,如下:
在 Vue2 的 diff 算法中,会对这个组件树逐层逐级比较每一个子节点,会一个一个的比较这些 span 的内容有没有变化。
在 Vue3 中,只需要找到 div 这个区块,然后逐一比较这个区块下的动态节点即可,这样就会节省很多更新消耗的时间。
之前我们很害怕一个动态内容被嵌套的非常深,从根节点开始逐层比对会很消耗时间的。
我们再更改一下代码结构:
这里可以看到,虽然我们在 span 标签外层嵌套了3层父节点,我们也会直接找到根 div 所在的区块,找到它对应的动态节点。
在 Vue3 中,所有的动态节点都和其所在的区块绑定了,当内容发生变更时,会直接找到它对应的区块,然后找到区块中动态改变的节点,而不会像 Vue2 一样也会将很多静态不会变化的节点也遍历一遍。
这就是 Vue3 虚拟 Dom 优化中最关键的优化,此外还有一些对节点属性的优化。
2. 节点属性优化
我们在 span 上定义了一个属性 id,可以看到 patchFlag 并没有变化,这个 id 属性也只在渲染时渲染一次,后续就不会变化了。我们如果把 id 设为动态属性会怎么样呢?
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", { id: _ctx.box }, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, ["id"]),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world")
]))
}
// Check the console for the AST
可以看到,patchFlag 从1变成了9,代表在这个节点上不光文本内容会发生变化,属性也会发生变化,还加了一条注释表示 id 属性会发生变化。
我们在加一个静态的 class 属性 box1:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", {
id: _ctx.box,
class: "box1"
}, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, ["id"]),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world"),
_createElementVNode("span", null, "hello world")
]))
}
// Check the console for the AST
可以看到 class 属性只是加到渲染属性中,并没有加入到后面的注释中,所以在 diff 时只会检查这个 span 的文本和 id 有没有变化。
这套体系就会保证我们在更新时只会关注那些真正会变的内容,这样就跳出了虚拟 Dom 更新时的性能瓶颈。
至于这些功能是如何实现的,我们可以点击 Options,选中 hoistStatic 选项,就会出现如下内容:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_2 = ["id"]
const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createElementVNode("span", null, "hello world", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", {
id: _ctx.box,
class: "box1"
}, _toDisplayString(_ctx.message), 9 /* TEXT, PROPS */, _hoisted_2),
_hoisted_3,
_hoisted_4,
_hoisted_5,
_hoisted_6,
_hoisted_7
]))
}
// Check the console for the AST
可以看到,Vue 将静态内容都提到渲染函数外面,这些静态内容只会在开始时渲染一次。后续改变时div 这个区块下静态内容直接使用外面定义的,只会检查文本和 id 属性这2个动态内容。
三、Composition API - 更好的逻辑复用
1. 组合式 API 简介
Vue3 引入了一个全新的 API,称为 Composition API,即组合式 API。这个 API 的设计目的是解决在大型应用中管理和复用逻辑的问题。
在 Vue2 中,我们通常通过选项式 API(Options API)来组织代码,即在 Vue 组件中定义各种选项,如 data,methods,computed,watch 等。但是这种方式在处理大型复杂组件时会变得困难,因为相关的代码可能会分布在不同的选项中,使得代码难以理解和维护。此外,逻辑复用也有一些限制,尽管 Vue 提供了 mixins 和 scoped slots,但它们都有一些缺点和限制。
组合式 API 提供了一种新的方式来组织和复用代码。我们可以把组件的逻辑写成一个一个的 composition function,然后在需要的地方调用这些函数。这使得我们可以更方便地把逻辑集中在一起,也更方便地在不同的组件中复用逻辑。一个组合式 API 的简单示例如下:
const app = {
setup() {
// data
const count = ref(0);
// computed
const plusOne = computed(() => count.value + 1);
// method
const increname = () => count.value++;
// watch
watch(() => count.value * 2, v => console.log(v));
// lifecircle
onMounted(() => console.log('onMounted));
// 暴露给模板或渲染函数
return {
count
};
}
};
组合式 API 并没有替代原有的选项式 API,而是作为一个可选的补充。你可以根据需要和喜好选择使用选项式 API 或者组合式 API,或者两者结合。
2. 组合式 API 的好处
(1) 更好的 TypeScript 类型推断支持
Vue3 的组合式 API提供了更好的 TypeScript 类型推断支持,主要基于以下几个原因:
- 显式类型声明:在组合式 API中,我们可以使用 ref 和 reactive 函数来创建响应式的引用和对象。这些函数都接受一个参数,该参数的类型将被用来推导出返回值的类型。这让我们能够明确地声明和控制响应式数据的类型。
- 函数和返回值类型:在组合式 API中,我们可以通过返回值来暴露组件的公共 API。这些返回值可以被明确地类型化,让我们有更好的控制权和更好的类型安全性。
- 更直观的类型注解:选项式 API可能导致类型定义在不同的选项中分散,而组合式 API则允许我们将相关的逻辑和类型放在一起,从而让类型注解更直观。
- 简单易用:使用组合式 API 写出的代码,TS 和 JS 的代码基本完全一样的。如上述的代码,既是 JS 代码又是 TS 代码,不需要任何的手动类型声明,它也会完美的进行类型推断和自动补全。
(2) 更灵活的逻辑复用能力
在 Vue2 中,如果你想在不同的组件之间复用逻辑,你可能会使用 mixins、高阶组件 和作用域插槽的方式。但是这些技术在 TypeScript 中的类型推断并不理想,因为它们的行为可能会导致命名冲突、类型冲突、类型丢失、数据来源不清晰的问题。
相反,组合式 API允许我们使用普通的 JavaScript 函数来复用逻辑,这些函数有很好的类型推断支持。
(3) tree shaking 更友好
Tree shaking 是一种在打包时去除无用代码的优化技术。在 JavaScript 模块中,如果某些导出的函数或变量没有被其他模块使用,那么在打包时,这些未使用的代码可以被移除,从而减少最终产物的体积。
Vue3 的组合式 API 更好地支持 tree shaking,主要是因为它的设计方式更接近于标准的JavaScript 模块。在组合式 API 中,我们可以将代码组织为多个函数,每个函数负责一部分独立的逻辑。然后我们就可以按需导入和使用这些函数,而不是使用一个大的 “Vue” 对象。这意味着,如果我们没有使用某个函数,那么它就可以在打包时被移除。
相比之下,Vue2 的选项式 API 更依赖于 Vue 的全局 API 和实例方法。例如,你可能会在组件的methods 或 computed 属性中使用 this.$set 或 this.$emit。这些方法是 Vue 实例的一部分,不能被单独地导入和使用,因此它们也不能被 tree shaking 优化。
还有一点,基于函数的 API 写的代码有更好的压缩性,因为所有函数名包括函数中定义的变量名都能被压缩,而对象中的属性或方法名并不可以。因此,使用组合式 API 打包出来的代码体积会更小。
四、Vue3 新特性与争议
1. 组合式 API 相关
(1) ref & reactive
(2) 新的生命周期函数
(3) computed 和 watch
(4) Hooks
2. 内置组件
(1) Tleport
(2) Suspense
本文不会具体讲解些新特性,会在后面的文章中讲到。
3. 争议
(1) <script setup> 设计
在 Vue3 的单文件组件中有一个新的语法,可以在 script 标签中加上 setup,即 <script setup>,这样做的好处是我们可以通过这种写法自动将所有顶层的变量的声明暴露给模板来进行使用。
<script setup>
// data
const count = ref(0);
// computed
const plusOne = computed(() => count.value + 1);
// method
const increname = () => count.value++;
// watch
watch(() => count.value * 2, v => console.log(v));
// lifecircle
onMounted(() => console.log('onMounted));
</script>
<template>
<Foo>{{ count }}</Foo>
</template>
也就是允许我们以全局变量的形式把这些变量应用到 Vue 的模板中。
(2) ref: 设计
可以使用 "ref:" 的语法使使用 ref 的方式更方便。
如果我们想让一种原始类型的值具有响应性,那就可以将其作为参数传给 ref,就会返回一个包装对象,可以使用这个对象的 value 属性进行取值。
每次使用 .value 取值还是比较麻烦的,我们可以使用 "ref:" 这个语法糖的形式定义变量,类似于 const 和 let 这种声明符。
ref: count = 1;
function inc() {
count++;
}
接下来就可以直接使用 count 变量了,编译之后就是下面的代码。
const count = ref(1);
function inc() {
count.value++;
}
使用 <script setup> 和 可以减少代码的冗余,使用 ref: 可以让使用 ref 更加高效。但从这2个设计出现后,引起了巨大的争议,特别是 ref:。有一些人认为使用 ref: 相当于造了一个方言,会增加学习成本,影响了直觉等。
在 Vue3.2 中只保留了 <script setup>,ref: 语法糖被抛弃了。
五、环境搭建
1. 准备
(1) 安装 nodejs,需要10以上的版本。
(2) 安装 vue 脚手架,需要4.5.13及以上版本。安装好 nodejs 后,使用 npm install -g @vue/cli 可以安装 vue 脚手架。使用 vue --version 可以查看版本。
2. 初始化 Vue3 项目
使用命令 vue create 项目名称,即可创建一个 Vue 项目。
vue create myproject
接下来会问我们一些问题:
(1) 选择创建模板
前2项为 Vue2 和 Vue3 的默认模板,这里我们选择第3项,手动设置。
(2) 选择安装选项
注意这里要选择某个选项,要按空格,按回车会进入下一步。
这里可以选择 TypeScript、Router 和 Vuex。
(3) 选择版本
这里我们直接选择 3.x。
(4) 选择是否使用 class 装饰器
我们选择 N。
(5) 选择是否用 Babel 和 TS 来做代码检测
选择 y。
(6) 选择路由模式是否使用 history 模式
选择 n。
(7) 选择代码格式检查模式
直接选默认的第一项。
(8) 选择在什么时候进行代码检查
选择第一项,保存的时候。
(9) 选择配置文件保存在哪里
选择第一项,保存在一个单独文件中。
(10) 选择要不要保存上述的创建配置
如果需要保存上面的步骤,可以选择 y,下次创建 Vue3 项目可以直接使用。
3. Vue3 项目目录结构
-
项目名称
-
node_modules
-
存放当前项目下所有的依赖包
-
-
public
-
index.html
-
项目的入口文件,浏览器默认访问的文件
-
不要改动
-
会在这个页面中,引用其他的 vue 组件文件
-
-
- src
-
assets
-
静态文件,比如图片,静态的css或者js
-
-
components
-
存放组件文件的位置,存放vue文件
-
一个vue文件对应浏览器上的一个文件
-
一般用于存放放入在其他页面的小组件
-
-
router
-
路由信息
-
-
store
-
vuex状态管理的仓库位置
-
用于存储用户的相关信息,比如用户名
-
-
views
-
存放vue组件
-
一个vue组件就是我们在浏览器上看到的一个页面
-
比如登录 比如列表
-
后台项目中,我们在webapp下创建jsp文件,现在是在views目录下创建vue文件
-
-
App.vue
-
项目的根组件,index.html引用的就是这个组件
-
我们自定义的组件都是嵌入在App.vue组件中
-
-
main.ts
-
全局的 ts 文件
-
可以实现外部资源的引用
-
- shims-vue.d.ts
- TS 的声明文件,告诉 TS 以 .vue 结尾的这些文件需要交给 Vue 模块进行处理
-
-
配置文件
-
package.json
-
启动和打包命令
-
项目的依赖
-
- tsconfig.json
- TS 的配置文件
-
4. 启动项目
控制台输入 npm run serve,出现如下信息即为启动成功。
接下来浏览器访问 http://localhost:8080 即可访问。文章来源:https://www.toymoban.com/news/detail-471712.html
文章来源地址https://www.toymoban.com/news/detail-471712.html
到了这里,关于Vue3-01-Vue3 新特性及环境搭建的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!