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

注:本文是大圣老师课程的笔记,原课程地址:07 | 巧妙的响应式:深入理解Vue 3的响应式机制

一,什么是响应式

看下面的代码,double依赖于count,但是当我们修改了count,double却没有发生对应的变化。

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

想要实现count变化后,double自动更新。就需要一个东西去实时监听count,当发现count变化了,就再次调用double = count * 2更新下。如下:

创建一个监听的对象object
let count = 1   //object开始监听count
let double = count * 2
console.log(double)//2
count = 2  //object监听count到count发生变化了。就再次调用double = count * 2,更新double
console.log(double)//2,实际上我们想要的是它变成4,这个操作应该是监听者object再次调用double = count * 2实现

image-20211122233737247

二,响应式的原理

Vue 中用过三种响应式解决方案,分别是 definePropertyProxyvalue setter

我们首先来看 Vue 2 的 defineProperty API,这个函数详细的 API 介绍你可以直接访问MDN 介绍文档Object.defineProperty() - JavaScript | MDN (mozilla.org)来了解。

在下面的代码中,我们定义个一个对象 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 属性(删除的是堆空间中的obj.count),set 函数就不会执行,double 还是之前的数值(double还是全局变量)。这也是为什么在 Vue 2 中,我们需要 $delete 一个专门的函数去删除数据。

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

而Vue 3 的响应式机制是基于 Proxy 实现的,Proxy 的重要意义在于它解决了 Vue 2 响应式的缺陷。

Proxy和Math,Date一样,是js的内置对象。

我们看下面的代码,在其中我们通过 new Proxy 代理了 obj 这个对象,然后通过 get、set 和 deleteProperty 函数代理了对象的读取、修改和删除操作,从而实现了响应式的功能。

let obj = {
    
    }
let count = 1
let getDouble = n=>n*2
let double = getDouble(count)
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
	  }
  }
})
proxy.count=count
console.log(obj.count,double)//1 2
proxy.count = 2
console.log(obj.count,double) //2 4
delete proxy.count
// 删除属性后,我们打印log时,输出的结果就会是 undefined NaN
console.log(obj.count,double) //undefined NaN

我们从这里可以看出 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, watchEffect, computed } from "vue";
let obj = reactive({
    
    
  count: 1
});
let double = computed(() => obj.count * 2);
setTimeout(() => {
    
    
  obj.count = 2;
}, 3000);
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)

三种实现原理的对比表格如下:

image-20211123003224387

三,定制响应式数据

setup重构之后的 todolist 的代码。这段代码使用 watchEffect,数据变化之后会把数据同步到 localStorage 之上,这样我们就实现了 todolist 和本地存储的同步。

function useStorage(name, value=[]){
    
    
    let data = ref(JSON.parse(localStorage.getItem(name)|| value))
    watchEffect(()=>{
    
    
        localStorage.setItem(name,JSON.stringify(data.value))
    })
    return data
}

更进一步,我们可以直接抽离一个 useStorage 函数,在响应式的基础之上,把任意数据响应式的变化同步到本地存储。我们先看下面的这段代码,ref 从本地存储中获取数据,封装成响应式并且返回,watchEffect 中做本地存储的同步,useStorage 这个函数可以抽离成一个文件,放在工具函数文件夹中。

function useStorage(name, value=[]){
    
    
    let data = ref(JSON.parse(localStorage.getItem(name)|| value))
    watchEffect(()=>{
    
    
        localStorage.setItem(name,JSON.stringify(data.value))
    })
    return data
}

在项目中我们使用下面代码的写法,把 ref 变成 useStorage,这也是 Composition API 最大的优点,也就是可以任意拆分出独立的功能。

let todos = useStorage('todos',[])

function addTodo() {
    
    
  ...code
}

我们可以把日常开发中用到的数据,无论是浏览器的本地存储,还是网络数据,都封装成响应式数据,统一使用响应式数据开发的模式。这样,我们开发项目的时候,只需要修改对应的数据就可以了。

四,Vueuse 工具包

我们自己封装的 useStorage,算是把 localStorage 简单地变成了响应式对象,实现数据的更新和 localStorage 的同步。同理,我们还可以封装更多的类似 useStorage 函数的其他 use 类型的函数,把实际开发中你用到的任何数据或者浏览器属性,都封装成响应式数据,这样就可以极大地提高我们的开发效率。

Vue 社区中其实已经有一个类似的工具集合,也就是 VueUse,它把开发中常见的属性都封装成为响应式函数。

vueuse的安装:

npm install @vueuse/core

简单使用:

import {
    
     useMouse } from "@vueuse/core";
// "x" and "y" are refs
const {
    
     x, y } = useMouse();
console.log( x.value, y.value);

Guess you like

Origin blog.csdn.net/weixin_42349568/article/details/121506134