大家都知道,vue2的响应性存在以下限制:
- 当为 对象 新增一个没有在 data 中声明的属性时,新增的属性 不是响应性 的
- 当为 数组 通过下标的形式新增一个元素时,新增的元素 不是响应性 的 那Vue3做了些什么,不再有以上限制了呢?我们一起来看看吧!
今天先探索reactive的响应性。
一、构建reactive函数,获取proxy实例
整个 reactive 函数,本质上是返回了一个 proxy 实例,那么我们先去实现这个 reactive 函数,得到 proxy 实例。
创建packages/reactivity/src/reactive.ts 模块:
import { mutableHandlers } from "./baseHandlers";
/**
* 响应性 Map缓存对象
* key target
* val proxy
*/
export const reactiveMap = new WeakMap<object, any>()
/**
* 为复杂数据类型,创建响应性对象
* @param target 被代理对象
* @returns 代理对象
*/
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>) {
// 如果该实例已经被代理,则直接返回
const existingProxy = proxyMap.get(target)
if(existingProxy) {
return existingProxy
}
// 未被代理则生成proxy实例
const proxy = new Proxy(target, baseHandlers)
// 缓存代理对象
proxyMap.set(target, proxy)
return proxy
}
此时,在vue/examples/reactivity/reactive.html中建一个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>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { reactive } = Vue
const obj = reactive({
name: 'zhangsan'
})
console.log(obj)
</script>
</html>
可看到打印结果:
已经可以正确返回一个proxy
二、handler部分的实现
创建packages/reactivity/src/mutableHandlers.ts 模块:
里边的get方法和set方法就是整个handler的核心部分
/**
* getter回调方法
*/
const get = createGetter()
/**
* 创建getter回调方法
*/
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object ) {
// 利用 Reflect 得到返回值
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
}
}
const set = createSetter()
function createSetter() {
return function set(target: object, key: string | symbol, value: any, receiver: object) {
// 利用 Reflect 设置新值
const res = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key, value)
return res
}
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set
}
以上我们知道了get方法中收集依赖,set方法触发依赖。
三、触发effect
1、创建packages/reactivity/src/effect.ts
在创建好了reactive实例之后,接下来我们需要触发effect.
demo中:
effect(() => {
document.querySelector('#app').innerHTML = obj.name
})
effect的实现,是生成一个ReactiveEffect,在ReactiveEffect里面,执行run函数,在run函数中,首先标记当前触发的activeEffect,然后执行fn,至此完成了回调函数的触发。
export function track(target: object, key: unknown){}
export function trigger(target: object, key: unknown, newValue: unknown){}
export let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
activeEffect = this
return this.fn()
}
}
export function effect<T = any>(fn: () => T) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
在demo中引入effect函数,可以在浏览器看到name已经渲染到div中了
2、实现track
我们知道,getter 时要收集当前的 fn 函数,以便在setter 的时候可以执行对应的 fn 函数 但是对于收集而言,如果仅仅是把 fn 存起来还是不够的,我们还需要知道,当前的这个 fn 是哪个响应式数据对象的哪个属性对应的,只有这样,我们才可以在 该属性 触发 setter 的时候,准确的执行响应性。
所以我们的实现思路是这样的:创建一个weakMap: WeakMap:
- key:响应性对象
- value: Map对象: -key: 响应性对象的指定属性 -value: 指定对象的指定属性的执行函数
type KeyToDepMap = Map<any, ReactiveEffect>
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, key: unknown){
// 如果当前不存在执行函数,则直接返回
if(!activeEffect) return
// 尝试从targetMap中,根据target获取map
let depsMap = targetMap.get(target)
// 如果获取到的 map 不存在,则生成新的 map 对象,并把该对象赋值给对应的value
if(!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 为指定map 指定key设置回调函数
depsMap.set(key, activeEffect)
console.log(targetMap)
}
打印targetMap,已经得到相应的weakMap:
3、实现trigger
以上可知,所有的依赖关系都保存到了targetMap,所以说依赖触发也就是从targetMap中读取对应的effect,然后执行对应的函数就可以了。
/**
* 触发依赖的方法
* @param target WeakMap 的 key
* @param key 代理对象的key,当依赖被触发时, 需要根据该key获取
*/
export function trigger(target: object, key: unknown){
// 依据 target 获取存储的map实例
const depsMap = targetMap.get(target)
if(!depsMap) return
// 依据 key, 从depsMap中取出value,该value是一个ReactiveEffect类型的数据
const effect = depsMap.get(key) as ReactiveEffect
if(!effect) {
return
}
effect.fn()
}
四、构建Dep模块,处理一对多的依赖关系
创建以下测试实例,就会发现,只有第二个div的值是改变的。
<body>
<div id="app1"></div>
<div id="app2"></div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: 'zhangsan'
})
effect(() => {
document.querySelector('#app1').innerHTML = obj.name
})
effect(() => {
document.querySelector('#app2').innerHTML = obj.name
})
console.log(obj)
setTimeout(() => {
obj.name = 'lisi'
}, 2000);
</script>
原因是在做依赖收集时,指定object的每个key只对应了一个value值,也就是说只能完成一个key对应一个有效effect函数的对应关系,所以对于上边的demo,一个name属性对应了两个effect,目前的代码是不支持的。想要一个key对应多个effect也很简单,只需要让原先Map里的一个ReactiveEffect变成数组即可。
创建packages/reactivity/src/dep.ts
用set来存储多个effect
import { ReactiveEffect } from "./effect";
export type Dep = Set<ReactiveEffect>
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
return dep
}
接下来修改track函数中的代码,将之前代码depsMap.set(key, activeEffect)处进行修改:
const dep = depsMap.get(key)
if(!dep) {
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
export function trackEffects(dep: Dep) {
dep.add(activeEffect!)
}
trigger同样也需要进行修改,将原来的const effect = depsMap.get(key) as ReactiveEffect处进行修改:
// 依据 key, 从depsMap中取出value,该value是一个ReactiveEffect类型的数据
const dep: Dep | undefined = depsMap.get(key)
if(!dep) {
return
}
triggerEffects(dep)
/**
*
* @param 依次触发dep中保存的依赖
*/
export function triggerEffects(dep: Dep) {
const effects = Array.isArray(dep) ? dep : Array.from(dep)
// 依次触发依赖
for(const effect of effects) {
triggerEffect(effect)
}
}
export function triggerEffect(effect: ReactiveEffect) {
effect.run()
}
至此就完成了多个effect的依赖收集和多个effect的依赖触发,本质上就是把Map里的value值由原来的ReactiveEffect变成了对应的Set数组。
五、reactive函数的局限性
1、reactive只能对复杂数据类型进行使用
由以上分析可知,reactive内部使用的是proxy,而proxy只能接收复杂类型,不能接收简单类型,所以简单数据类型的响应性不能通过reactive函数来实现
2、reactive的响应性数据,不可以进行解构
当使用解构赋值时,创建的是一个普通对象,而不是通过Proxy包装的对象,因此失去了响应性。
六、总结
通过以上的学习,我们在构建了reactive响应性函数,我们知道了它是通过是通过 proxy 的 setter 和 getter,来实现的数据监听,需要配合 effect 函数进行使用,基于 WeakMap 完成的依赖收集和处理,可以存在一对多的依赖关系。同时也了解了reactive函数的不足。
因为reactive的不足,所以vue3又为我们提供了ref函数构建响应性,那么ref 函数的内容是如何进行实现的呢?ref 可以构建简单数据类型的响应性吗?为什么 ref 类型的数据,必须要通过 .value 访问值呢?让我们下一篇文章见吧。
本文表述若有不准确的地方,欢迎大佬指正,互相学习,共同成长~