【学习笔记】《玩转Vue3全家桶》--深入理解Vue3的响应式机制

前言:这篇文章,我们来认识一下Vue3的响应式机制吧。

一、什么是响应式

    在JavaScript里面的变量,是没有响应式这个概念的。在学习JavaScript的时候,我们了解到代码时自上而下执行的。
    如果,我们能让getDouble函数自动执行,也就是如下图所示,我们使用JavaScript的某种机制,把count包裹一层,每当对count进行修改时,就去同步更新double的值,那么就有一种double自动跟着count的变化而变化的感觉,这就算是响应式的雏形了。
在这里插入图片描述

二、响应式原理

    响应式原理是什么呢?Vue中用过三种响应式解决的方案,分别是defineProperty、Proxy和value setter。首先,我们看下Vue2的defineProperty API吧,回顾一下:
-----------------------------------------------------------------分割线-----------------------------------------------------------------------------------------
     Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
    语法:

Object.defineProperty(obj, prop, descriptor)

    参数:
obj :要定义属性的对象;
prop:要定定义或修改的属性的名称或symbol;
descriptor: 要定义或修改的属性描述符。
    返回值:
被传递给函数的对象。
    描述:
get: 属性的getter函数,如果没有getter,则为undefined。当访问该属性的时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为undefined。
set: 属性的setter函数,如果没有setter,则为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。默认为undefined。
-----------------------------------------------------------------分割线-----------------------------------------------------------------------------------------
    结合例子来说明一下,在下面的代码中,我们定义一个对象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作为Vue2实现响应式的原理,它的语法中也有一些缺陷。比如在下面代码中,我们删除obj.count属性,set函数就不会执行,double还是之前的数值。
这也是为什么在Vue2中,我们需要$delete 一个专门的函数去删除数据。

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

    Vue 3的响应式机制是基于Proxy实现的。Proxy的重要意义在于它解决了Vue2响应式的缺陷。看下面的代码,在其中我们通过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实现的功能和Vue2的definePropery类似,他们都能够在用户修改数据的时候触发set函数,从而实现自动更新double的功能。而且,Proxy还完善了几个defineProperty的缺陷,比如说可以监听到属性的删除。
    Proxy是针对对象来监听,而不是针对某个属性,所以不仅可以代理那些定义时不存在的属性,还可以代理更丰富的数据结构,比如Map、Set等,并且我们也能通过deleteProperty实现对删除操作的代理。
    比如下面代码中,Vue3的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后,响应式机制就比较完备了。但是在Vue3中还有另一个响应式实现的逻辑,就是利用对象的get和set函数来进行监听,这种响应式的实现方式,只能拦截某一个属性的修改,这也是Vue3中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这个函数让我们在数据变化之后可以执行指定的函数。
    使用 < script setup>重构之后的todolist的代码,这段代码使用watchEffect,数据变化之后,会把数据同步到localStorage之上,这样我们就实现了todolist和本地存储的同步。


import { ref, watchEffect, computed } from "vue";

let title = ref("");
let todos = ref(JSON.parse(localStorage.getItem('todos')||'[]'));
watchEffect(()=>{
    localStorage.setItem('todos',JSON.stringify(todos.value))
})
function addTodo() {
  todos.value.push({
    title: title.value,
    done: false,
  });
  title.value = "";
}

    更进一步,我们可以抽离一个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
}

    现在,你应该已经学会了在 Vue 内部进阶地使用响应式机制,去封装独立的函数。社区也有非常优秀的 Vueuse 工具库,包含了大量类似 useStorage 的工具函数库。
    如下图所示,我们可以把日常开发中用到的数据,无论是浏览器的本地存储,还是网络数据,都封装成响应式数据,统一使用响应式数据开发的模式。这样,我们开发项目的时候,只需要修改对应的数据就可以了。
在这里插入图片描述
    基于响应式的开发模式,我们还可以按照类似的原理,把我们需要修改的数据,都变成响应式。比如,我们可以在loading状态下,去修改游览器的小图标favicon。和本地存储类似,修改favicon时,我们需要找到head中有icon属性的标签。
    在下面的代码中,我们把对图标的对应修改的操作封装成了useFavicon函数,并且通过ref和watch的包裹,我们还把小图标变成了响应式数据。


import {ref,watch} from 'vue'
export default function useFavicon( newIcon ) {
    const favicon = ref(newIcon)

    const updateIcon = (icon) => {
      document.head
        .querySelectorAll(`link[rel*="icon"]`)
        .forEach(el => el.href = `${icon}`)
    }
    const reset = ()=>favicon.value = '/favicon.ico'

    watch( favicon,
      (i) => {
        updateIcon(i)
      }
    )
    return {favicon,reset}
  } 

    这样在组件中,我们就可以通过响应式的方式去修改和使用小图标,通过对 faivcon.value 的修改就可以随时更换网站小图标。下面的代码,就实现了在点击按钮之后,修改了网页的图标为 geek.png 的操作。

 <script setup>
 import useFavicon from './utils/favicon'
 let {favicon}  = useFavicon()
 function loading(){
   favicon.value = '/geek.png'
 }
</script>

<template>
  <button @click="loading">123</button>
</template>

四、Vueuse工具包

    我们自己封装的 useStorage,算是把 localStorage 简单地变成了响应式对象,实现数据的更新和 localStorage 的同步。同理,我们还可以封装更多的类似 useStorage 函数的其他 use 类型的函数,把实际开发中你用到的任何数据或者浏览器属性,都封装成响应式数据,这样就可以极大地提高我们的开发效率。
    Vue 社区中其实已经有一个类似的工具集合,也就是 VueUse,它把开发中常见的属性都封装成为响应式函数。
    VueUse 趁着这一波 Vue 3 的更新,跟上了响应式 API 的潮流。VueUse 的官方的介绍说这是一个 Composition API 的工具集合,适用于 Vue 2.x 或者 Vue 3.x,用起来和 React Hooks 还挺像的。
    在项目目录下打开命令行,输入如下命令,来进行VueUse插件的安装:

npm install @vueuse/core

    然后,我们就先来使用一下 VueUse。在下面这段代码中,我们使用 useFullscreen 来返回全屏的状态和切换全屏的函数。这样,我们就不需要考虑浏览器全屏的 API,而是直接使用 VueUse 响应式数据和函数就可以很轻松地在项目中实现全屏功能。

<template>
  <h1 @click="toggle">click</h1>
</template>
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen()
</script>

    useFullscreen的封装逻辑和useStorage类似,都是屏蔽游览器的操作,把所有我们需要用到的状态和数据都用响应式的方式统一管理,VueUse中包含了很多我们常用的工具函数,我们可以把网络状态、异步请求的数据、动画和事件等功能,都看成是响应式的数据去管理。

总结

    首先,介绍了响应式的概念以及我们为什么需要响应式。
    然后,通过对 useStorage 的封装,讲解了响应式机制的进阶用法,那就是可以把一切项目中的状态和数据都封装成响应式的接口,屏蔽了浏览器的 API,对外暴露的就是普通的数据,可以极大地提高我们的开发效率。
    接着,了解了 VueUse 这个工具包,这也是 Vue 官方团队成员的作品。VueUse 提供了一大批工具函数,包括全屏、网络请求、动画等,都可以使用响应式风格的接口去使用,并且同时兼容 Vue 2 和 Vue 3,开箱即用。推荐去 GitHub 关注 VueUse 的动态和功能。

参考博客

MDN的Object.defineProperty() https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
《玩转Vue3全家桶》–大圣

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值