文中部分图片来源于
Vue Mastery
前言
笔者最近在学习Vue3
,通过本文和各位掘友聊聊Vue3
带来了哪些更新和优化。
本文主要内容包括以下两个部分:
Vue
的简要发展历程Vue3
的更新及优化点
Vue 简要发展历程
我们都知道,与前端页面息息相关的一个东西叫做DOM
或者 Document Object Model
。在没有Vue
这些框架之前,我们开发HTML
页面,会映射到一系列的DOM 节点
,由浏览器解析,绘制,最终显示成网页。
当我们需要改变网页显示的内容,我们会使用
JS
通过浏览器提供的API
进行 DOM
操作。如下图:
但是在实际的网页上可能会存在成千上万的
DOM 节点
,如果需要我们前端开发同学需要通过JS
去操作那么多DOM Node
,显然是一个体力活,而且还会造成很多问题:
- 开发效率低
- 代码复杂,维护成本高
- 代码量大
- ......
Vue1
然后就出现了像 Vue
这样的前端框架帮助我们干这些体力活,我们只需要关心业务数据的变更,框架会自动帮我们更新DOM
,当用户更改了DOM的数据,框架又会自动的更新业务数据,省去了很多重复繁琐的工作量。但是随着网页复杂度增加,尽管由框架帮我们完成大量的DOM 节点
操作,但是还是无法避免网页性能变差的问题(Vue1
)
Vue2 —— 引入Virtual DOM
Vue2
为了减少DOM 节点
操作引入了Virtual DOM
(虚拟DOM),其本质就是使用JavaScript
对象去描述DOM Node
。
Vue
会把我们写的template
编译成render function
,Vue
执行render function
,过程中会进行依赖收集,最终返回Vnode
(虚拟DOM
节点)。然后由Vue
将虚拟的节点创建为真实的DOM
并挂载。
当
render
函数依赖的状态变更,render
函数重新执行得到一个新的Virtual DOM
,此时新旧Virtual DOM
对比进行差异化DOM
修补更新。
框架引入Virtual DOM
操作DOM
比原生DOM
操作快吗?
我们看下尤大的回答:
Virtual DOM
的好处
- 让组件的渲染逻辑完全从真实的DOM解耦
- 在其他的环境中重用框架的运行时(允许开发人员自定义渲染解决方案,比如IOS、Android等,不局限于浏览器)
- 可以在返回实际渲染引擎之前使用
JS
以编程的方式构造、检查、克隆、操作所需要的DOM Node
就是因为这些优点,Vue3
依然使用Virtual DOM
。接下来,看看vue3
做了哪些优化。
Vue3 及其优化点
Vue3
中,有三个比较核心的模块:
Reactivity Module
—— 数据响应性处理Compiler Module
—— 将单文件组件的template
编译成render
函数Render Module
—— 负责将Virtual DOM
生成DOM Node
并挂载网页;patch
新旧Virtual DOM
等。Render Module
主要在三个阶段工作:render
阶段 —— 执行render
返回VNode
mount
阶段 —— 通过VNode
创建DOM Node
并挂载patch
阶段 —— 对比新旧VNode
并更新DOM
三个模块协同工作,一个动画看下整个流程:
接下来,列举一下优化点。
采用 TS 开发
加入了类型后,更有利于阅读源码和维护。
采用 monorepo 管理项目
采用 monorepo 管理项目的好处请参考文章为什么越来越多的项目选择 Monorepo?
Vue3 渲染函数的优化
属性对象参数的写法优化
先来看一张图,图中是Vue3
与 Vue2
渲染函数的写法。
可以看到
Vue3
与 Vue2
渲染函数的参数个数与各个参数的作用差不多,区别最大的就是属性对象参数,也就是第二个参数的写法。
- 在
Vue2
中:需要用户区分事件、DOM
属性、组件属性、attribute
等,要将对应的属性写在对应的类别中。 - 在
Vue3
中:无需用户区分,扁平结构,直接写即可,Vue
会帮我们智能的进行绑定👍🏻
render函数的第一个参数不再是渲染函数
// 在 vue2 中,如果我们将 render 的逻辑进行拆分
// 那么在调用各个拆分出来的函数时,要手动把渲染函数h传递进去
// 写 jsx 的时候不需要
export default {
data() {
return {}
},
methods: {
renderCondition(h) {
//...
},
defaultRender(h) {
//...
}
},
render(h) {
if (condition) {
return this.renderCondition(h)
} else {
return this.defaultRender(h)
}
}
}
在vue3
中:
import { h } from 'vue'
// 直接使用即可
插槽变成了一个函数,而不是虚拟节点
比如:
<template>
<Stack>
<div>1</div>
<div>2</div>
<Stack>
<div>3</div>
<div>5</div>
<Stack>
<div>6</div>
<div>7</div>
</Stack>
</Stack>
</Stack>
</template>
我们想要这个Stack渲染成以下的样子,并可以无限嵌套使用:
- 1
- 2
-
3
-
5
- 6
- 7
-
使用渲染函数实现:
<script>
import { h } from 'vue'
export default {
render() {
return h('div',
{
class: 'margin-l-12'
},
// 在Vue2, this.$slots.default就是VNode,不需要执行
this.$slots.default && this.$slots.default() || [])
}
}
</script>
<style>
.margin-l-12 {
margin-left: 12px;
}
</style>
效果:
template
编译优化
静态DOM
节点Hoisted
我们的
template
中有两个静态的DOM节点,意味着这俩节点跟我们的状态没有关系,所以编译时会被提升,这样的好处是啥?
patch
的时候无需关注这些静态节点,提升patch
的速度。在Vue2
的时候,我们会去对比新旧虚拟DOM
,不论这个节点是不是包含动态的内容都会去遍历对比,显然静态的节点是非常没有必要对比的(它不会消失,也不会变化)。- 静态节点只会被创建一次,后面都是重用,减少了重复创建、回收的开销。
动态节点及动态属性识别
compiler
在将template
转化渲染函数时会生成一些辅助性的标识用于提升组件更新的速度,比如上图中圈出的地方,文本为dym
的节点 class
是动态的,所以编译过后生成了一个 /*CLASS*/
,当组件更新处理该节点时,无需关注该节点的其他属性(innerText/style/...),只需要关心class
。一个节点我们可能觉得这点提升微不足道,但是往往我们的应用页面会由很多节点构成,在Vue2
中没有这些辅助的标识帮助我们识别哪些属性是可以跳过不处理的,所以会对整个节点的所有属性都去检查是否变更,当节点数量多了以后无疑会造成阻塞或者卡顿,Vue3
做到了,只检查可能会变化的那些属性,极大的提升了性能。
Block Optimization
译为”块优化“,我们注意到编译生成的render
函数中有openBlock
,其作用是用来收集某个block
内的动态子节点,最终这个block
上会生成一个存着该block
内动态节点的扁平数组。这样就更进一步的缩小了组件更新时的新旧VNode
的对比范围,也进一步提升了性能。
Reactivity
什么是响应式? —— 自动更新,状态自动保持同步
Vue3
是如何实现的?
Vue3
使用Proxy
来实现状态变更的拦截,相比Vue2
使用Object.defineProperty
实现,优点如下:
Object.defineProperty
是通过给对象新增属性/修改现有属性 来实现getter/setter
的拦截。需要遍历对象的每一个key
去实现,当遇到很大的对象或者嵌套层级很深的对象,性能问题会很明显。而Proxy
则是通过在对象的访问前架设拦截,是完完全全的代理模式,性能远远优于Object.defineProperty
这种方式。Object.defineProperty
这种方式无法拦截到给对象新增属性这种操作,因为组件初始化不能预知会新增哪些属性,也就没法设置getter/setter
,所以我们不得不使用Vue2
提供的$set
api,再去Object.defineProperty
给新增的属性加上getter/setter
。而使用Proxy
无需关心这个问题。Proxy
天然支持Array
,无需再去拦截会改变原数组的那些原型方法(shift、unshift、push、pop、sort、splice、reverse
),甚至你支持修改数组的length
也会被拦截。Proxy
不支持IE
,啥年代了,这应该也是个优点。
Composition API & setup hook
Composition API
组合式 API (Composition API
) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue
组件。它包含了这些API:
- 响应式API ——
ref
、reactive
、computed
、watch
...... - 生命周期钩子 ——
onMounted
、onUnmounted
...... - 依赖注入 ——
provide
、inject
......
Composition Api
是为了更好的 代码逻辑组织 以及更好的代码复用能力设计的。
setup hook
Vue3
还新增了setup hook
,其特点如下:
setup
这个hook
是最先执行的。setup
内不能访问this
,因为就是故意这么设计的。setup
返回一个对象,对象内的所有属性都可在其他option
以及template
中使用。setup
返回的对象中属性值为ref
的,在template
中无需使用.value
。- 完全支持与
options api
同时使用,加一个setup
选项即可。
更好的代码逻辑组织
在Vue2
中,我们为了实现一个功能,往往需要在多个option
选项中进行开发 —— data
中写一点,watch
中写一点,methods
中写一点......,这种情况在复杂的组件中尤为常见,同一个功能代码也尤其分散,造成了不好的开发体验以及后续维护上的困难。因此 Vue3
设计了 Composition Api
,我们现在能够将一个功能的代码实现在一起,更加聚合,更加容易维护。再次放上一张官网的图:
更好的代码复用能力
- 在
Vue2
,我们往往通过mixin
来实现代码逻辑复用,它存在以下几个问题:- 变量来源不清,当我们没有注意到当前组件的
mixin
选项,我们可能看到一些当前组件没有定义的属性、方法,很可能会懵。当存在多个mixin
,变量和方法的来源变得更加不清 - 变量和方法命名冲突,当存在多个
mixin
,变量和方法冲突变得极其容易
- 变量来源不清,当我们没有注意到当前组件的
- 在
setup
中,我们可以像编写普通的JavaScript
那样来编写组件代码,我们的逻辑都是一个一个的变量或者函数,所以我们在写Composition Api
的代码时可运用上所有JavaScript
代码组织的最佳实践,很轻松的对这些函数拆分,提取以达到复用的目的。
笔者对此也写的有一篇文章,感兴趣的掘友可以移步看下4种方案带你探索 Vue.js 代码复用的前世今生
更好的开发体验
Composition Api
主要利用基本的变量和函数,它们本身就是类型友好的。用Composition Api
重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript
书写的组合式 API
代码和用 JavaScript
写都差不太多!让许多纯 JavaScript
用户也能从 IDE
中享受到部分类型推导功能。
更小的生产代码体积
- 一方面
<script setup>
形式书写的组件模板被编译为了一个内联函数,和<script setup>
中的代码位于同一作用域。不像options api
需要依赖this
上下文对象访问属性,被编译的模板可以直接访问<script setup>
中定义的变量,无需从实例中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。 - 另一方面
Vue3
本身对tree shaking
的支持就比较好。
watchEffect
Vue3
新增了watchEffect
API,笔者简要描述一下watch
和 watchEffect
的区别:
先看一下他们的用法:
watch(() => state.count, (val, oldVal) => {
// 与vue2的watch几乎完全一样,除了第一个参数,要不是个ref, 要不是个函数返回一个值
// 回调是懒执行的,只有当监听的值变更才会执行
// 回调函数内访问其他响应式值,这些响应式值的变更不会触发回调(与watchEffect的区别)
// 回调可以获取到当前值 和 旧值
}, {
immediate: true, // 立即执行
deep: true // 如果监听一个对象,可以设置深度监听
})
watchEffect(() => {
// 会马上执行这个函数,并且函数内的所有响应式值的dep会添加这个effect
// 当函数内任意一个响应式值变更,都会重新执行这个effect
})
在大多数情况下,你可能使用watchEffect
要多一些,比如一个初始化加载数据的例子:
使用watch
:
import { watch, reactive, ref, watchEffect, computed, mounted } from 'vue'
export default {
props: ['id'],
data() {
return {
busiData: null
}
},
created() {
this.fetchData(this.id)
},
methods: {
fetchData(id) {
fetch(url + id).then(res => res.json()).then(data => this.busiData = data)
}
},
setup() {
watch(() => this.id, (val, oldVal) => {
this.fetchData(val)
})
return {}
}
使用watchEffect
:
export default {
props: ['id'],
setup(props) {
const busiData = reactive(null)
watchEffect(() => {
fetch(url + props.id).then(res => res.json).then(data => busiData.value = data)
})
return {}
}
}
可以很明显的看到,使用watchEffect
实现这个功能,代码量更少,更简洁!
Vue3
还有很多小优化和更新,笔者不在此一一列举了,一些主要的点都在上面了。
总结
本文主要内容如下:
- 由浅入深,简要描述了
Vue
的发展历程 Virtual DOM
相关问题Vue3
更新点及优化点TS
开发monorepo
管理项目- 渲染函数优化
- 模版编译优化
- 响应式优化
Composition API
&setup
我们能够明显感受到,Vue3
在性能、逻辑复用、逻辑组织、开发体验等很多方面的提升是巨大的!👍🏻
结语
如果文中有些的不对的地方,还请指正一下,谢谢!
如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【赞
】都是我创作的最大动力 ^_^