Vue3 源码解析 02--Vue3 特性
前言
Vue3 框架对于 Vue2 来说是一个巨大的进步,它不仅兼容了 Vue2 的语法还提供了大量的新特性,下面我们就一起学习一下 Vue3 的新特性。
Vue2 存在的问题
不可否认,Vue 作为一个拥有百万级用户的前端框架来说,已经是一个很成熟的框架了,但是当面对各种各样的需求的时候,还是暴露了一些缺陷,这也是为什么作者要重写 Vue2. 下面我们通过一段简单的代码来直观的感受一下:
<template>
<div>
<form action="">
<input type="text" v-model="stu.id">
<input type="text" v-model="stu.name">
<input type="submit" @click='addStu'>
</form>
<ul>
<li v-for='(stu,index) in stus' :key='index'@click="deleteStu(index)">{{stu.name}}</li>
</ul>
</div>
</template>
<script>
export default {
name:'App',
data:()=>{
return {
stus:[
{id:1,name:'小明',age:12},
{id:2,name:'小红',age:13},
],
stu:{
id:'',
name:'',
age:''
}
}
},
methods:{
//删除信息
deleteStu(index){
this.stus = this.stus.filter((item,i)=>i!==index)
},
//添加信息
addStu(e){
e.preventDefault()//屏蔽默认事件
const stu = Object.assign({},this.stu)
this.stus.push(stu)
this.stu.id = ''
this.stu.name=''
this.stu.age=''
}
}
}
</script>
这里,我们实现了最简单的一个学生信息展示、添加、删除功能。 可以看到,如果我们想实现一个新增的功能,需要在 data 里面新增一个 stu 元素,然后在 methods 里面新增一个 addStu 的方法。如果需求稍微复杂一点,还有可能用到 computed 和 watch,需要分别在里面添加相应的逻辑。 在简单项目下,这样做完全没有问题,但是随着项目复杂程度的加大耦合性就会变得严重。针对这一问题,Vue 提出了 Mixin 的概念,Mixin 的出现虽然解决了部分耦合性的问题,但是伴随的确是调试难度的加大,使问题来源变得模糊。 所以针对这一问题,Vue3 提出了 CompositionAPI 的概念。
CompositionAPI
上面提到了,Vue3 中采用了新的接口使用方式。这种新的接口方式被称为(组合式 API),而在 Vue2 中与之相对应的则是(选项式 API)。 关于 OptionsAPI 我们上面已经介绍了,那么相对于 OptionsAPI 的缺点,CompositionAPI 又是如何解决这个问题的。还是上面提到的那些功能,那么我们看一下 CompositionAPI 是如何实现的:
<template>
<form action="">
<input type="text" v-model="state.stu.id">
<input type="text" v-model="state.stu.name">
<input type="text" v-model="state.stu.age">
<input type="submit" @click='addStu'>
</form>
<ul>
<li @click='rmStu(index)' v-for='(item,index) in state.stus' :key='index'>{{item.name}}---{{item.age}}</li>
</ul>
</template>
import {ref,reactive} from 'vue'
export default {
name:"App",
setup(){
const {rmStu,state} = useRemoveStudent()
const {addStu,state2} = useAddStudent(state)
return { rmStu,state, addStu}
}
}
// 抽离出来的新增学生功能
function useAddStudent(state){
let state2=reactive({
stu:{id:'',name:'',age:''}
})
//添加信息
function addStu(e){
e.preventDefault()//屏蔽默认事件
const stu = Object.assign({},state.stu)
state.stus.push(stu)
state.stu.id = ''
state.stu.name=''
state.stu.age=''
}
return {
addStu
}
}
//删除学生功能
function useRemoveStudent(){
let state = reactive({
stus:[
{id:1,name:'小明',age:12},
{id:2,name:'小红',age:13},
],
stu:{
id:'',
name:'',
age:''
}
})
function rmStu(index){
state.stus = state.stus.filter((item,i)=>i!==index)
}
return {
state,rmStu
}
}
CompositionAPI 入口:setup
上面的代码,通过 CompositionAPI 实现了和 OptionAPI 同样的功能,但是我们可以看到,没有 Vue2 中的 data 和 methods 这些,取而代之的是setup函数。
setup 函数是 CompositionAPI 的入口函数。通过return来暴露我们需要使用的数据。
通过上面的代码我们可以看到。我们通过useAddStudent,useRemoveStudent两个函数将相应的功能抽离出来。随着我们业务逻辑复杂度的上升,界面数量的增加。我们可以把相同功能的业务逻辑封装到单独的模块中。实现了组件的解偶,同时相对于 Mixin,避免了调试过程中出现的坑。
这里提一下 setup 需要注意的事项:
- setup 钩子函数在生命周期 beforeCreate 之前执行,所以 setup 函数里面不能调用 data 和 methods 等
- 由于我们不能在 setup 中调用 data 和 methods,所以 Vue 为了避免我们的错误使用,它直接将 setup 函数中的 this 修改成了 undefined
- setup 函数只能是同步不能是异步的
Vue3 响应式:ref 和 reactive
在 Vue2 中,data 里面的数据自动实现了响应式.但是在 Vue3 的 CompositionAPI 中实现响应式的方式是通过 ref 和 reactive。 下面我们来看一段代码:
import {ref,reactive} from 'vue'
setup(){
let count =ref(0)
let state = reactive(){
list:[{id:1,name:'小明',age:12}]
}
return {
count,state
}
}
上面的代码分别通过 ref 和 reactive 实现了简单数据和复杂类型数据的响应式。
reactive 和 ref 都是 Vue3 提供的实现响应式数据的方法。在 Vue2 中响应式数据的底层是通过 defineProperty 来实现的,而在 Vue3 中响应式数据的底层是通过 ES6 的 Proxy 来实现的。
ref 和 reactive 的注意点
reactive 的注意点:
- reactive 的参数必须是对象(如果传入的不是一个响应式,那么响应式无法实现)
- 如果给 reactive 传递了替他对象(不是自定义对象,例如 Date)
- 默认情况下修改对象,界面不会自动更新
- 如果想更新,可以通过重新赋值的方式
例如:
setup(){
let state = reactive({
time:new Date()
})
function changeTimeFail(){
//直接修改数据,界面不会自动更新
state.stu.setDate(state.stu.getDate()+1)
}
function changeTimeSuc(){
//重新赋值,界面自动更新
let time = new Date(state.time.getTime())
time.setDate(state.time.getDate()+1)
state.time = time
}
return{
state,
changeTimeFail,
changeTimeSuc
}
}
运行上面的代码后我们知道,当我们使用 reactive 时,传入的不是一个自定义的对象,那么默认情况下修改对象是不会触发界面的自动更新的,如果想触发自动更新,只有重新赋值。
ref 注意点:
- 在 template 中使用 ref 的值不需要通过 value 获取
- 在 Js 中使用 ref 的值需要通过 value 获取
<template>
<!-- 不需要通过.value的形式就可以获取count数据 -->
<div @click='printCount'>Count:{{count}}</div>
</template>
setup(){
let count = ref(0)
function printCount(){
//需要通过.value的形式获取count中的数据
console.log(count.value)
}
return {
count,
printCount
}
}
造成上面的情况的本质是,在我们使用 ref 创建响应式数据的时候,Vue 自动在我们的数据外面封装了一层。当 Vue 解析 template 的过程中,会判断响应式数据的类型,如果是 ref 类型的数据,则自动为数据添加.value
ref 和 reactive 的异同
相同点:
- ref 和 reactive 都是 Vue3 实现数据响应式的方式
- ref 和 reactive 的响应式原理都是 ES6 的 Proxy
不同点:
- reactive 的参数是复杂类型的数据(Json/Arr),ref 的参数是简单类型的数据
- 虽然 ref 和 reactive 的最底层都是 Proxy,那是因为 ref 的底层本质是 reactive.当我们使用 ref 的时候,系统会自动给我们转换成:ref(xx)=>reactive({value:xx})
- 如果在 template 中使用ref类型的数据,那么Vue 会自动帮我们添加.value。 如果在 template 中使用的是reactive类型的数据,那么Vue 不会自动帮我们添加.value
这里简单说一下 Vue 是如何判断数据类型的:是通过当前数据的__v_ref 私有属性来判断的,如果有该属性,且取值为 true 则表示当前数据是 ref 类型,需要添加value属性名
Vue3 的递归监听和非递归监听
在 Vue 中默认情况下,不论是通过 ref 还是 reactive 都是递归监听。但是递归监听存在一个问题,就是数据量比较大的时候,递归监听是非常消耗性能的。 所以针对这种情况,Vue3 中提出了非递归监听的解决办法:shallowReactive 、shallowRef
这里解释一下递归监听为什么非常消耗性能,因为为了达到每层数据的响应式,Vue 将每层数据的外层都包装了一层 Proxy。
这里顺便说一下,我们使用 ref 的时候也是可以传递复杂类型数据的,所以这也就是为什么我们上面说的ref 和 reactive 都可以实现递归监听的原因
贴一段 ref 递归监听的代码
setup(){
let state = ref({
demo:{
a:'a',
aClass:{
b:'b',
bClass:{
c:'c',
cClass:{
d:'d'
}
}
}
}
})
function changeData(){
state.value.demo.a='1'
state.value.demo.aClass.b='2'
state.value.demo.aClass.bClass.c='3'
state.value.demo.aClass.bClass.cClass.d='4'
//这里的state,是将我们传入的数据最外面包了一层Proxy,我们传入的数据作为其中的value属性存在的
console.log(state)
}
return {
state,
changeData
}
}
shallowReactive 和 shallowRef
非递归监听,顾名思义就是只监听第一层数据的变化,当第一层数据改变之后界面就会更新,如果第一层数据没有发生改变,而其子层级的数据发生变化,那么界面是不会更新的。 注意点 如果是通过 shallowRef 创建的数据,因为 ref 数据类型为简单类型,所以 Vue 监听的就是数据中 value 属性的变化,并不是第一层数据的变化。
用 shallowReactive 举个例子
import {shallowReactive} from 'vue'
setup(){
let state = shallowReactive({
demo:{
a:'a',
aClass:{
b:'b',
bClass:{
c:'c',
cClass:{
d:'d'
}
}
}
}
})
function changeData(){
state.demo.aClass.b='2'
state.demo.aClass.bClass.c='3'
state.demo.aClass.bClass.cClass.d='4'
console.log(state)//Proxy{demo:{...}}
console.log(state.demo)//{a:'a',aClass:{...}}
console.log(state.demo.aClass)//{b:'2',bClass:{...}}
console.log(state.demo.aClass.bClass)//{c:'2',cClass:{...}}
}
return {
state,
changeData
}
}
通过上面的输出结果,我们可以看出来,shallowReactive 处理后的数据只有最外层的数据会包装成 Proxy,其他的层级数据不做处理。从这点我们可以解释:shallowReactive 只是监听了第一层数据的变化。
如果想单独更新某一层的数据,那么需要使用triggerRef去处理我们的 state 数据,这样界面就会触发更新。(Vue3 中只提供了 triggerRef 方法,没有提供类似 triggerReactive 方法。所以,如果是 reactive 类型的数据,是没有办法直接触发的)
这里贴一段与上面相似的代码:
setup(){
let state = shallowRef({
demo:{
a:'a',
aClass:{
b:'b',
bClass:{
c:'c',
cClass:{
d:'d'
}
}
}
}
})
function changeData(){
state.value.demo.aClass.b='2'
state.value.demo.aClass.bClass.c='3'
state.value.demo.aClass.bClass.cClass.d='4'
//触发界面强制更新,刷新界面数据
triggerRef(state)
}
return {
state,
changeData
}
}
PS:一般情况下我们使用 ref 或者 reactive 即可,只有在需要监听的数据量比较大的时候,我们才使用 shallowRef/shallowReactive
**shallowRef 本质:**shallowRef 的底层就是 shallowReactive,如果是通过 shallowRef 创建的数据,那么他们监听的是 value 属性的变化,因为本质上 value 才是第一层。
获取响应式数据的原始数据:toRaw
我们知道在 Vue 中 ref 和 reactive 都是响应式的数据。响应式数据的特点就是每次修改都会更新 UI 界面。这样虽然自动实现了双向数据绑定,那么在某些不需要更新 UI 界面的情况下,这无疑是个消耗性能的点。针对这种情况,Vue3 提供了 toRaw 方法。
照样贴一段代码:
<template>
<div>
<div>{{state.name}}</div>
<button @click="changeObj">按钮</button>
</div>
</template>
setup(){
let obj = {name:'小明'}
let state = reactive(obj)
function changeObj(){
//修改obj内容,UI界面不会触发更新
obj.name='小红'
console.log(obj===state)//false
console.log('obj',obj)//{name:'小红'}
console.log('sate',state)//Proxy:{name:'小红'}
}
return {
state,
changeObj
}
}
通过上面的输出内容,可以看出来 state 和 obj 是引用的关系。当我们修改 obj 的内容的时候,会同步修改 state 的内容,但是不会触发 UI 界面的更新。 利用这个原理,我们可以通过 toRaw 获取 ref/reactive 的原始数据,对原始数据进行修改,这样不会触发 UI 界面的更新,从而提升了性能。 还是上面的代码:
setup(){
let obj = {name:'小明'}
let state = reactive(obj)
let obj2 = toRaw(state)
function changeObj(){
obj2.name='小红'
console.log(obj2===obj)//true
console.log(obj)//{name:'小红'}
console.log(state)//Proxy:{name:'小红'}
}
return {
state,
changeObj
}
}
上面的代码,可以看出来 toRaw 可以获取 reactive 的原始数据。这样我们可以在任意位置获取 reactive 的原始数据进行更改,同时不会触发 UI 界面的更新,提升了性能。
我们上面提到过 ref 的底层也是 reactive,那么同样的原理我们可以利用 toRaw 获取 ref 的原始数据:
setup(){
let obj = {name:'小明'}
//ref处理数据响应式
let state = ref(obj)
//唯一和之前不同的地方就是我们传入的参数就是state.value
let obj2 = toRaw(state.value)
function changeObj(){
obj2.name='小红'
}
return {
state,
changeObj
}
}
注意点:如果想通过 toRaw 获取 ref 类型的原始数据,那么应该使用的方法是:roRaw(state.value),即告诉 toRaw 方法,要获取的是 value 属性。 因为经过 Vue 处理之后,.value 中保存的数据才是我们当初创建 ref 数据传入的那个参数
固化数据:markRaw
固化这个词是我自己编的 😂。试想这么一个场景:渲染的数据量很大,但是不会发生改变,这个时候我们可以使用 markRaw 标记这个数据,告诉 Vue 该数据的修改不会触发 UI 更新。 同时,如果数据被 markRaw 标记了,就算该数据作为响应式数据中的属性,它也依然不是响应式的
const obj1 = markRaw({...})
console.log(isReactive(reactive(obj1)))//false
//被markRaw标记后,就算是作为属性,也不会触发响应式
const obj2 = reactive({obj1})
console.log(isReactive(obj2.obj1))//false
注意:这里的 markRaw 是高级 API 的用法。markRaw 的标记属性仅停留在。这也就意味着,当你将一个嵌套的,没有 markRaw 标记的对象设置为 reactive 对象的属性,那么在重新访问时,你将会得到一个包装后的 Proxy 对象。
例如:
const father = markRaw({
son:{}
})
const obj= reactive({
//尽管 father 被markRaw被标记了,但是father.son并没有
son :father.son
})
console.log(father.son===obj.son)
为 reactive 对象的属性创建 ref:toRef/toRefs
当我们想为一个 reactive 对象的某些属性创建 ref 的时候,可以通过 toRef 或者 toRefs 来完成。 例如:
const state= reactive({
name:'小明',
age:17
})
//将state的age属性变为ref数据
const objRef = toRef(state,'age')
objRef.value = 18
console.log(state.age)//18
state.age=19
console.log(objRef.value)//19
注意:如果我们利用 ref 将某一属性变成响应式,那么我们修改响应式数据的时候不会影响到原始数据,因为他的本质是将传入的数据包装成一个 Proxy。但是我们利用 toRef 将某一个对象中的属性变成 ref 的时候,那么我们修改响应式的数据是会影响到原始数据的。
toRefs 的原理和 toRef 是相同的,不同的地方在于 toRef 是将某个单一的属性变成 ref 数据,toRefs 是将多个属性变成 ref 数据。
const state = reactive({
name:'小明',
age:18
})
const {name,age} = toRefs(state)
toRef 和 ref 的区别
- ref 是复制,修改数据不会影响之前的数据。
- toRef 是引用,修改数据会影响之前的数据。
自定义 ref:customRef
customRef 允许我们自定义一个 ref,可以现实的控制依赖追踪和触发响应。
import { customRef } from 'vue'
function myRef(value) {
return customRef((track, trigger) => {
return {
get() {
track() //告诉Vue这个数据需要追踪变化
return value
},
set(newValue) {
value = newValue
trigger() //告诉Vue触发界面更新
},
}
})
}
总结
至此,Vue3 的 Composition API 常规内容基本上介绍完毕了。 Vue3 作为一个渐进式的框架在新增了 CompositionAPI 的同时也完全兼容 Vue2 的 OptionsAPI,这对我们开发者来说无疑是一个好消息,我们可以缓慢的从 Vue2 过渡到 Vue3。