Vue3 响应式机制
响应式
响应式一直都是 Vue 的特色功能之一。但是在 JavaScript 里面的变量,是没有响应式这个概念的
let count = 1
let double = count * 2
console.log(double) // 2
count = 2
console.log(double) // 2double 的值是根据 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)使用 JavaScript 的某种机制把 count 包裹一层,每当对 count 进行修改时,就去同步更新 double 的值,这就算是响应式的雏形
响应式原理
Vue 中用过三种响应式解决方案,分别是 defineProperty、Proxy 和 value setter
defineProperty
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还是4Proxy
Vue3 响应式机制是基于 Proxy 实现的,Proxy 的重要意义在于它解决了 Vue 2 响应式的缺陷
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 还完善了几个 definePropery 的缺陷,比如说可以监听到属性的删除。
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)
})对象的 get 和 set 函数
在 Vue 3 中还有另一个响应式实现的逻辑,就是利用对象的 get 和 set 函数来进行监听。这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现
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)三种实现原理的对比
| 实现原理 | defineProperty | Proxy | value setter |
|---|---|---|---|
| 实际场景 | Vue 2 响应式 | Vue3 reactive | Vue3 ref |
| 优势 | 兼容性 | 基于 Proxy 实现真正的拦截 | 实现简单 |
| 劣势 | 数组和属性删除等拦截不了 | 兼容不了 IE11 | 只拦截 value 属性 |
| 实际应用 | Vue 2 | Vue3 复杂数据结构 | Vue3 简单属性 |
定制响应式数据
使用 <script setup> 重构之后的 todolist 的代码,使用 watchEffect 数据变化之后会把数据同步到 localStorage 之上,这样就实现todolist 和本地存储的同步
<template>
<div>
<input type="text" v-model="title" @keydown.enter="addTodo"/>
<button v-if="active < all" @click="clear">清理</button>
<ul v-if="todos.length">
<li v-for="todo in todos">
<input type="checkbox" v-model="todo.done"/>
<span :class="{ done: todo.done }"> {{ todo.title }}</span>
</li>
</ul>
<div v-else>暂无数据</div>
<div>
全选<input type="checkbox" v-model="allDone"/>
<span> {{ active }} / {{ all }} </span>
</div>
</div>
</template>
<script setup>
import {ref, computed} from "vue";
import {useStorage} from '../use/useStorage'
let todos = useStorage('todos', [])
function addTodo() {
todos.value.push({
title: title.value,
done: false,
});
title.value = "";
}
let title = ref("");
function clear() {
todos.value = todos.value.filter((v) => !v.done);
}
let active = computed(() => {
return todos.value.filter((v) => !v.done).length;
});
let all = computed(() => todos.value.length);
let allDone = computed({
get: function () {
return active.value === 0;
},
set: function (value) {
todos.value.forEach((todo) => {
todo.done = value;
});
},
});
</script>useStorage 函数
useStorage 函数在响应式的基础之上,把任意数据响应式的变化同步到本地存储。ref 从本地存储中获取数据,封装成响应式并且返回,watchEffect 中做本地存储的同步,useStorage 这个函数可以抽离成一个文件,放在工具函数文件夹中
import {ref, watchEffect, computed} from "vue";
export function useStorage(name, value = []) {
let data = [];
if (localStorage.getItem(name)!="undefined"){
data = ref(JSON.parse(localStorage.getItem(name)))
}else{
data=ref(value)
}
watchEffect(() => {
localStorage.setItem(name, JSON.stringify(data.value))
})
return data
}社区也有非常优秀的 Vueuse 工具库,包含了大量类似 useStorage 的工具函数库
可以把日常开发中用到的数据,无论是浏览器的本地存储,还是网络数据,都封装成响应式数据,统一使用响应式数据开发的模式。基于响应式的开发模式,还可以按照类似的原理,把需要修改的数据,都变成响应式。比如可以在 loading 状态下,去修改浏览器的小图标 favicon。和本地存储类似,修改 favicon 时需要找到 head 中有 icon 属性的标签
useFavicon 函数
对图标的对应修改的操作封装成了 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 的修改就可以随时更换网站小图标。实现了在点击按钮之后,修改了网页的图标的操作
<script setup>
import useFavicon from './utils/favicon'
let {favicon} = useFavicon()
function loading(){
favicon.value = '/demo.png'
}
</script>
<template>
<button @click="loading">123</button>
</template>Vueuse 工具包
VueUse 官方的介绍说这是一个 Composition API 的工具集合,适用于 Vue 2.x 或者 Vue 3.x,用起来和 React Hooks 还挺像的
npm install @vueuse/core使用 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 类似,都是屏蔽了浏览器的操作,把所有需要用到的状态和数据都用响应式的方式统一管理