Vue 3 响应式回顾
- Proxy 对象实现属性监听
- 多层属性嵌套,在访问过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和 length 属性
- 可以作为单独的模块使用
Proxy 和 Reflect
Proxy 可以对目标对象的读取和修改等操作进行拦截,然后进行处理。创建 Proxy 对象需要两个参数,一个是目标对象,第二个是包含 get、set、deleteProperty 函数的拦截器对象,目标对象的 getter/setter,deleteProperty就设置成这三个函数。
在 Vue 3 中,reactive 的实现就是通过 Proxy 配合 Reflect 实现。
Reflect 也是 ES6 中新增的 API,可以用于获取目标对象的行为,它与 Object 类似,但是更易读,为操作对象提供了一种更优雅的方式。它的方法与 Proxy 是对应的,例如 get、set、deleteProperty。
在严格模式下,Proxy 中传入的 set 和 deleteProperty 需要返回布尔类型的值,如果返回 false 或者不返回的话会抛出 Type Error 的异常。
'use strict'
const target = {
foo: 'xxx',
bar: 'yyy'
}
// Reflect.getPrototypeOf()
// Object.getPrototypeOf()
const proxy = new Proxy(target, {
get (target, key, receiver) {
// return target[key]
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
// target[key] = value
return Reflect.set(target, key, value, receiver)
},
deleteProperty (target, key) {
// delete target[key]
return Reflect.deleteProperty(target, key)
}
})
Proxy 和 Reflect 中使用的 receiver 有区别
Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
'use strict'
const obj = {
get foo() {
console.log(this)
return this.bar
}
}
const proxy = new Proxy(obj, {
get (target, key, receiver) {
if (key === 'bar') {
return 'value - bar'
}
// return Reflect.get(target, key) // this 指向 obj
return Reflect.get(target, key, receiver) // this 指向 receiver,在这里就是 proxy 对象
}
})
console.log(proxy.foo) // value-bar
reactive 的手写实现
- 接收一个参数,判断这参数是否对象。reactive 只能处理对象,这和 Vue 2 不同
- 创建拦截器对象 handler,设置 get/set/deleteProperty
- 返回 Proxy 对象 ```javascript const isObject = val => val !== null && typeof val === ‘object’ const convert = target => isObject(target) ? reactive(target) : target
export function reactive(target) { if (!isObject(target)) return
const handler = { get (target, key, receiver) { // 收集依赖 console.log(‘get’, key)
const result = Reflect.get(target, key, receiver)
return convert(result) // 判断当前属性是否对象,是还要递归处理
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 触发更新
console.log('set', key, value)
}
return result
},
deleteProperty (target, key) {
let result = true
if (Reflect.has(target, key)) {
result = Reflect.deleteProperty(target, key)
console.log('delete', key);
}
return result
}
}
return new Proxy(target, handler) }
demo:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive } from './reactivity/index.js'
const obj = reactive({
name: '张三',
age: 18
})
obj.name = 'lisa'
delete obj.age
console.log(obj)
</script>
</body>
</html>
收集依赖
当访问响应式对象属性时,getter 里要收集依赖。修改对象属性时,set 和 deleteProperty 里要触发更新。
- 新建一个 WeakMap,在里面存储键值对,键是被访问的响应式目标对象,值是一个 Map 对象。
- Map 也是一个存储键值对的对象,在这里的作用是存储依赖于目标对象的属性的集合。所以 Map 对象的键是属性名,值是一个 Set 集合,里面存储了对属性进行操作的函数。
- get 中收集依赖的过程大概如下:
- 判断调用 get 的函数是否为空,如果为空直接返回
- 从 WeakMap 中获取以当前对象为键的键值对的值,也就是 Map,如果没有,调用 WeakMap 的 set 方法创建出来。
- 从 Map 中获取以当前属性的名称为键的键值对的值,也就是 Set,如果没有,调用 Map 的 set 方法创建出来。
- 在 Set 中添加函数。
- 触发更新的过程大概如下:
- 从 WeakMap 中获取以当前对象为键的键值对的值,也就是 Map,如果没有,直接返回
- 从 Map 中获取以当前属性的名称为键的键值对的值,也就是 Set,如果有,从里面获取每一个函数并执行。
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
export function reactive(target) {
if (!isObject(target)) return
const handler = {
get (target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
return convert(result) // 判断当前属性是否对象,是还要递归处理
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty (target, key) {
let result = true
if (Reflect.has(target, key)) {
result = Reflect.deleteProperty(target, key)
result && trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
let activeEffect = null
export function effect (callback) {
activeEffect = callback
callback() // 访问响应式对象属性,去收集依赖
activeEffect = null
}
let targetMap = new WeakMap()
/**
* 对象的属性收集依赖
* @param {object} target 跟踪对象
* @param {string} key 属性名
*/
export function track (target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
/**
* 触发更新
* @param {*} target
* @param {*} key
*/
export function trigger (target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
dep.forEach(effect => {
effect()
})
}
demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive, effect } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = 0
effect(() => {
total = product.price * product.count
})
console.log(total)
product.price = 4000
console.log(total)
product.count = 1
console.log(total)
</script>
</body>
</html>
ref 的手写实现
ref 把基本数据类型转换成响应式的,如果传入的是对象,调用 reactive。
export function ref (raw) {
// 判断 raw 是否是 ref 的对象,如果是的话,直接返回
if (isObject(raw) && raw.__v_isRef) return raw
let value = convert(raw)
const r = {
__v_isRef: true,
get value () {
track(r, 'value')
return value
},
set value (newValue) {
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
}
}
return r
}
ref vs reactive
- ref 可以把基本数据类型数据,转成响应式对象
- ref 返回的对象,重新赋值成对象也是响应式的
- reactive 返回的对象,重新赋值丢失响应式
- reactive 返回的对象不可以解构
toRefs
toRefs 接收一个 reactive 返回的 Proxy 对象,如果不是 Proxy 对象,直接返回。
toRefs 会把基本数据类型的属性,处理成像 ref 那种格式,把转换后的属性挂到一个新的对象,返回。
export function toRefs (proxy) {
const ret = proxy instanceof Array ? new Array[proxy.length] : {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret
}
function toProxyRef (proxy, key) {
const r = {
__v_isRef: true,
get value () {
return proxy[key]
},
set value (newValue) {
proxy[key] = newValue
}
}
return r
}
computed
computed 接收一个 getter 函数,需要在里面收集依赖。
export function computed (getter) {
const result = ref()
effect(() => (result.value = getter()))
return result
}