Vue3 源码解析 02--Vue3 特性

318 阅读12分钟

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 中采用了新的接口使用方式。这种新的接口方式被称为CompositionAPI\color{#ff3030}{Composition API}(组合式 API),而在 Vue2 中与之相对应的则是OptionsAPI\color{#ff3030}{Options API}(选项式 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 的标记属性仅停留在根级别\color{#ff3030}{根级别}。这也就意味着,当你将一个嵌套的,没有 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。