什么是响应式
js 本身没有响应式,思考这个结果
ini
let count = 1
let double = count * 2
console.log(double)
count = 2
console.log(double)
即使 count 有变化 第二次打印还是 2
Vue 响应式 流程
css
┌───────────────────────────────┐
│ 组件模板 (Template) │
│ <span>{{ count }}</span> │
│ <button @click="count++"/> │
└──────────────┬────────────────┘
│ 依赖追踪
▼
┌───────────────────────────────┐
│ 响应式系统 (Reactive Core) │
│ ref() / reactive() / effect()│
│ └─ 依赖收集 + 派发更新 │
└──────────────┬────────────────┘
│ 通知变化
▼
┌───────────────────────────────┐
│ 虚拟DOM (VNode) │
│ 计算出新的 UI 树 │
│ 与旧树进行 diff 比较 │
└──────────────┬────────────────┘
│
▼
┌───────────────────────────────┐
│ 真正DOM更新 │
│ innerText / class / style │
└───────────────────────────────┘
Vue 2 defineProperty
定义个一个对象 obj,使用defineProperty 代理了 count 属性。这样我们就对 obj 对象的 value 属性实现了拦截,读取 count 属性的时候执行 get 函数,修改 count 属性的时候执行 set 函数,并在 set函数内部重新计算了 double
javascript
let getDouble = (n) => n * 3
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) // 打印3
obj.count = 2
console.log(double) // 打印6 count 变化打印结果也会变
但会存在问题,如果此时删除 obj.count 属性,set 函数就不会执行,double 还是之前的数值。这也是为什么在 Vue 2 中,我们需要 $delete 一个专门的函数去删除数据。
arduino
delete obj.count
console.log(double) //4 结果还是之前的值
Vue 3 基于 Proxy
用户修改数据的时候触发 set 函数,从而实现自动更新 double 的功能。而且 Proxy 还完善了几个 definePropery 的缺陷,比如说可以监听到属性的删除。
Proxy 是针对对象来监听,而不是针对某个具体属性,所以不仅可以代理那些定义时不存在的属性,还可以代理更丰富的数据结构,比如 Map、Set 等,并且我们也能通过deleteProperty 实现对删除操作的代理。
ini
let getDouble = (n) => n * 2
let count = 1
let obj = {
count: 1,
}
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
}
},
})
console.log(obj.count, double) // 1,2
proxy.count = 2
console.log(obj.count, double) // 2,4
delete proxy.count
// 删除属性后, undefined NaN 此处删除产生了效果
console.log(obj.count, double)
Vue 3 的 reactive 函数可以把一个对象变成响应式数据,而 reactive 就是基于Proxy 实现的。我们还可以通过 watchEffect,在 obj.count 修改之后,执行数据的打印
javascript
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)
})
Vue3 另一种方式 利用对象的 get 和 set 函数来进行监听,这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现。在下面的代码中,拦截了count 的 value 属性,并且拦截了 set 操作,也能实现类似的功能。
csharp
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) // 1,2
count.value = 2
console.log(count.value, double) // 2,4
响应数据进阶
将前面说的todolist 数据存在本地,解决刷新后数据丢失问题
javascript
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 函数,在响应式的基础之上,把任意数据响应式的变化同步到本地存储
javascript
function useStorage(name, value = []) {
let data = ref(JSON.parse(localStorage.getItem(name) || '[]'))
watchEffect(() => {
localStorage.setItem(name, JSON.stringify(data.value))
})
return data
}
把 ref 变成 useStorage,这也是 Composition API最大的优点,也就是可以任意拆分出独立的功能。
csharp
let todos = useStorage('todos', [])
function addTodo() {
// writ your code
}
甚至可以把想要的数据都变成响应式的形式
javascript
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 }
}
// 使用时
import useFavicon from './utils/favicon'
let { favicon } = useFavicon()
function loading() {
favicon.value = '/geek.png'
}
Vueuse
Vue 社区中其实已经有一个类似的工具集合,也就是 VueUse,它把开发中常见的属性都封装成为响应式函数。避免了重复造轮子,可以直接用。
比如这些常用的
javascript
// 状态与存储
import { useStorage, useToggle } from '@vueuse/core'
const todos = useStorage('todos', [])
const [visible, toggleVisible] = useToggle(true)
javascript
// DOM 与交互
import { useMouse } from '@vueuse/core'
const { x, y } = useMouse() // 响应式坐标
go
// 网络与异步
import { useFetch } from '@vueuse/core'
const { data, isFetching, error } = useFetch('/api/todos').json()
javascript
// 时间与动画
import { useNow, useIntervalFn } from '@vueuse/core'
const now = useNow()
useIntervalFn(() => console.log('每秒触发'), 1000)
arduino
// 剪贴板、窗口与系统
import { useClipboard, useWindowSize } from '@vueuse/core'
const { copy } = useClipboard()
const { width, height } = useWindowSize()
javascript
// 组合应用类
import { useDebounceFn } from '@vueuse/core'
const search = useDebounceFn(() => console.log('搜索触发'), 500)