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.

299 lines
14 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 07 | 巧妙的响应式深入理解Vue 3的响应式机制
你好我是大圣。在上一讲中我给你介绍了Composition API相比于Option API 的优点,以及<script setup>的语法,这些内容能够给我们后面的开发打下了坚实的基础。
今天我带你深入了解一下Vue 3的响应式机制相信学完今天的内容你会对响应式机制有更深地体会。我还会结合代码示例帮你掌握响应式机制的进阶用法让我们正式开始学习吧
## 什么是响应式
响应式一直都是Vue的特色功能之一。与之相比JavaScript里面的变量是没有响应式这个概念的。你在学习JavaScript的时候首先被灌输的概念就是代码是自上而下执行的。我们看下面的代码代码在执行后打印输出的两次double的结果也都是2。即使我们修改了代码中的count的值后double的值也不会有任何改变。
```xml
let count = 1
let double = count * 2
console.log(double)
count = 2
console.log(double)
```
double的值是根据count的值乘以二计算而得到的如果现在我们想让doube能够跟着count的变化而变化那么我们就需要在每次count的值修改后重新计算double。
比如在下面的代码我们先把计算doube的逻辑封装成函数然后在修改完count之后再执行一遍你就会得到最新的double值。
```xml
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的变化而变化的感觉这就算是响应式的雏形了。
![](https://static001.geekbang.org/resource/image/5c/97/5c9a7aa3468f19b7edf067b7b252ea97.jpg?wh=1090x970)
## 响应式原理
响应式原理是什么呢Vue中用过三种响应式解决方案分别是defineProperty、Proxy和value setter。我们首先来看Vue 2的defineProperty API这个函数详细的API介绍你可以直接访问[MDN介绍文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)来了解。
这里我结合一个例子来说明在下面的代码中我们定义个一个对象obj使用defineProperty代理了count属性。这样我们就对obj对象的value属性实现了拦截读取count属性的时候执行get函数修改count属性的时候执行set函数并在set函数内部重新计算了double。
```xml
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一个专门的函数去删除数据。
```xml
delete obj.count
console.log(double) // doube还是4
```
Vue 3 的响应式机制是基于Proxy实现的。就Proxy这个名字来说你也能看出来这是代理的意思Proxy的重要意义在于它解决了Vue 2响应式的缺陷。我们看下面的代码在其中我们通过new Proxy代理了obj这个对象然后通过get、set和deleteProperty函数代理了对象的读取、修改和删除操作从而实现了响应式的功能。
```xml
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修改之后执行数据的打印。
```xml
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操作也能实现类似的功能。
```xml
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)
```
三种实现原理的对比表格如下,帮助你理解三种响应式的区别。
![](https://static001.geekbang.org/resource/image/b5/11/b5344de85923a2ba8bea60283b491711.png?wh=1336x650)
## 定制响应式数据
简单入门响应式的原理后,接下来我们学习一下响应式数据在使用的时候的进阶方式。在前面第二讲做清单应用的时候,我给你留过一个思考题,就是让你想办法解决所有的操作状态在刷新后就都没了这个问题。
解决这个问题所需要的就是让todolist和本地存储能够同步。我们首先可以选择的就是在代码中显式地声明同步的逻辑而watchEffect这个函数让我们在数据变化之后可以执行指定的函数。
我们看下使用 <script setup>重构之后的todolist的代码。这段代码使用watchEffect数据变化之后会把数据同步到localStorage之上这样我们就实现了todolist和本地存储的同步。
```xml
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这个函数可以抽离成一个文件放在工具函数文件夹中。
```xml
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 最大的优点,也就是可以任意拆分出独立的功能。
```xml
let todos = useStorage('todos',[])
function addTodo() {
...code
}
```
现在你应该已经学会了在Vue内部进阶地使用响应式机制去封装独立的函数。社区也有非常优秀的Vueuse工具库包含了大量类似useStorage的工具函数库。在后续的实战应用中我们也会经常对通用功能进行封装。
如下图所示,我们可以把日常开发中用到的数据,无论是浏览器的本地存储,还是网络数据,都封装成响应式数据,统一使用响应式数据开发的模式。这样,我们开发项目的时候,只需要修改对应的数据就可以了。
![](https://static001.geekbang.org/resource/image/5a/0e/5a5yy5dc6f6b25f1c1ff8f3a434cd10e.jpg?wh=2316x1829)
基于响应式的开发模式我们还可以按照类似的原理把我们需要修改的数据都变成响应式。比如我们可以在loading状态下去修改浏览器的小图标favicon。和本地存储类似修改favicon时我们需要找到head中有icon属性的标签。
在下面的代码中我们把对图标的对应修改的操作封装成了useFavicon函数并且通过ref和watch的包裹我们还把小图标变成了响应式数据。
```xml
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的操作。
```xml
<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插件的安装
```xml
npm install @vueuse/core
```
然后我们就先来使用一下VueUse。在下面这段代码中我们使用useFullscreen来返回全屏的状态和切换全屏的函数。这样我们就不需要考虑浏览器全屏的API而是直接使用VueUse响应式数据和函数就可以很轻松地在项目中实现全屏功能。
```xml
<template>
<h1 @click="toggle">click</h1>
</template>
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen()
</script>
```
useFullscreen的封装逻辑和useStorage类似都是屏蔽了浏览器的操作把所有我们需要用到的状态和数据都用响应式的方式统一管理VueUse中包含了很多我们常用的工具函数我们可以把网络状态、异步请求的数据、动画和事件等功能都看成是响应式的数据去管理。
## 总结
我们来总结一下今天学到的内容首先我给你介绍了响应式的概念以及我们为什么需要响应式具体Vue 3的响应式源码我会在课程第四部分带你手写一个。
然后通过对useStorage的封装我为你讲解了响应式机制的进阶用法那就是可以把一切项目中的状态和数据都封装成响应式的接口屏蔽了浏览器的API对外暴露的就是普通的数据可以极大地提高我们的开发效率。
接着我带你了解了VueUse这个工具包这也是Vue官方团队成员的作品。VueUse提供了一大批工具函数包括全屏、网络请求、动画等都可以使用响应式风格的接口去使用并且同时兼容 Vue 2 和 Vue 3开箱即用。这门课程剩下的项目中会用到很多VueUse的函数也推荐你去GitHub 关注 VueUse的动态和功能。
## 思考题
最后,留给你一道思考题:你的项目中有哪些数据可以封装成响应式数据呢?
欢迎在评论区留言我会跟你一起探究Vue 3响应式的其他用法也欢迎你把这篇文章分享给其他人我们下一讲见