You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

07 | 巧妙的响应式深入理解Vue 3的响应式机制

你好我是大圣。在上一讲中我给你介绍了Composition API相比于Option API 的优点,以及<script setup>的语法,这些内容能够给我们后面的开发打下了坚实的基础。

今天我带你深入了解一下Vue 3的响应式机制相信学完今天的内容你会对响应式机制有更深地体会。我还会结合代码示例帮你掌握响应式机制的进阶用法让我们正式开始学习吧

什么是响应式

响应式一直都是Vue的特色功能之一。与之相比JavaScript里面的变量是没有响应式这个概念的。你在学习JavaScript的时候首先被灌输的概念就是代码是自上而下执行的。我们看下面的代码代码在执行后打印输出的两次double的结果也都是2。即使我们修改了代码中的count的值后double的值也不会有任何改变。

let count = 1
let double = count * 2
console.log(double)
count = 2
console.log(double)

double的值是根据count的值乘以二计算而得到的如果现在我们想让doube能够跟着count的变化而变化那么我们就需要在每次count的值修改后重新计算double。

比如在下面的代码我们先把计算doube的逻辑封装成函数然后在修改完count之后再执行一遍你就会得到最新的double值。

let count = 1
// 计算过程封装成函数
let getDouble = n=>n*2 //箭头函数
let double = getDouble(count)
console.log(double)

count = 2
// 重新计算double这里我们不能自动执行对double的计算
double = getDouble(count)
console.log(double)

实际开发中的计算逻辑会比计算doube复杂的多但是都可以封装成一个函数去执行。下一步我们要考虑的是如何让double的值得到自动计算。

如果我们能让getDouble函数自动执行也就是如下图所示我们使用JavaScript的某种机制把count包裹一层每当对count进行修改时就去同步更新double的值那么就有一种double自动跟着count的变化而变化的感觉这就算是响应式的雏形了。

响应式原理

响应式原理是什么呢Vue中用过三种响应式解决方案分别是defineProperty、Proxy和value setter。我们首先来看Vue 2的defineProperty API这个函数详细的API介绍你可以直接访问MDN介绍文档来了解。

这里我结合一个例子来说明在下面的代码中我们定义个一个对象obj使用defineProperty代理了count属性。这样我们就对obj对象的value属性实现了拦截读取count属性的时候执行get函数修改count属性的时候执行set函数并在set函数内部重新计算了double。

let getDouble = n=>n*2
let obj = {}
let count = 1
let double = getDouble(count)

Object.defineProperty(obj,'count',{
    get(){
        return count
    },
    set(val){
        count = val
        double = getDouble(val)
    }
})
console.log(double)  // 打印2
obj.count = 2
console.log(double) // 打印4  有种自动变化的感觉

这样我们就实现了简易的响应式功能,在课程的第四部分,我还会带着你写一个更完善的响应式系统。

但defineProperty API作为Vue 2实现响应式的原理它的语法中也有一些缺陷。比如在下面代码中我们删除obj.count 属性set函数就不会执行double还是之前的数值。这也是为什么在Vue 2中我们需要$delete一个专门的函数去删除数据。

delete obj.count
console.log(double) // doube还是4

Vue 3 的响应式机制是基于Proxy实现的。就Proxy这个名字来说你也能看出来这是代理的意思Proxy的重要意义在于它解决了Vue 2响应式的缺陷。我们看下面的代码在其中我们通过new Proxy代理了obj这个对象然后通过get、set和deleteProperty函数代理了对象的读取、修改和删除操作从而实现了响应式的功能。

let proxy = new Proxy(obj,{
    get : function (target,prop) {
        return target[prop]
    },
    set : function (target,prop,value) {
        target[prop] = value;
        if(prop==='count'){
            double = getDouble(value)
        }
    },
    deleteProperty(target,prop){
        delete target[prop]
        if(prop==='count'){
            double = NaN
        }
    }
})
console.log(obj.count,double)
proxy.count = 2
console.log(obj.count,double) 
delete proxy.count
// 删除属性后我们打印log时输出的结果就会是 undefined NaN
console.log(obj.count,double) 

我们从这里可以看出Proxy实现的功能和Vue 2 的definePropery类似它们都能够在用户修改数据的时候触发set函数从而实现自动更新double的功能。而且Proxy还完善了几个definePropery的缺陷比如说可以监听到属性的删除。

Proxy是针对对象来监听而不是针对某个具体属性所以不仅可以代理那些定义时不存在的属性还可以代理更丰富的数据结构比如Map、Set等并且我们也能通过deleteProperty实现对删除操作的代理。

当然为了帮助你理解Proxy我们还可以把double相关的代码都写在set和deleteProperty函数里进行实现在课程的后半程我会带你做好更完善的封装。比如下面代码中Vue 3 的reactive函数可以把一个对象变成响应式数据而reactive就是基于Proxy实现的。我们还可以通过watchEffect在obj.count修改之后执行数据的打印。

import {reactive,computed,watchEffect} from 'vue'

let obj = reactive({
    count:1
})
let double = computed(()=>obj.count*2)
obj.count = 2

watchEffect(()=>{
    console.log('数据被修改了',obj.count,double.value)
})

有了Proxy后响应式机制就比较完备了。但是在Vue 3中还有另一个响应式实现的逻辑就是利用对象的get和set函数来进行监听这种响应式的实现方式只能拦截某一个属性的修改这也是Vue 3中ref这个API的实现。在下面的代码中我们拦截了count的value属性并且拦截了set操作也能实现类似的功能。

let getDouble = n => n * 2
let _value = 1
double = getDouble(_value)

let count = {
  get value() {
    return _value
  },
  set value(val) {
    _value = val
    double = getDouble(_value)

  }
}
console.log(count.value,double)
count.value = 2
console.log(count.value,double)

三种实现原理的对比表格如下,帮助你理解三种响应式的区别。

定制响应式数据

简单入门响应式的原理后,接下来我们学习一下响应式数据在使用的时候的进阶方式。在前面第二讲做清单应用的时候,我给你留过一个思考题,就是让你想办法解决所有的操作状态在刷新后就都没了这个问题。

解决这个问题所需要的就是让todolist和本地存储能够同步。我们首先可以选择的就是在代码中显式地声明同步的逻辑而watchEffect这个函数让我们在数据变化之后可以执行指定的函数。

我们看下使用