🌬🌬🌬 前言:在Vue3中我们知道可以通过reactive、ref实现响应式数据,本质上来说,这两种方式都是通过依赖收集、依赖触发实现的数据相应,那这两种方式会有什么差异性吗,下面将会从应用场景、实现方式等方面进行解析,借鉴Vue源码并通过ts实现简化版的reactive、ref,春节已经过完啦,新的一年一起加油吧~💪🏻💪🏻💪🏻
一、构建基础列表
大致目录结构
diff
---| packages
---|---| reactivity // 响应性模块
---|---|---| src
---|---|---|---| index.ts 出口文件
---|---|---|---| ref.ts
---|---|---|---| reactive.ts
---|---|---|---| effect.ts
---|---|---|---| dep.ts
---|---|---|---| baseHandlers.ts
---|---| shared // 公共方法模块
---|---|---| src
---|---|---|---| index.ts 出口文件
---|---|---|---| shapeFlags.ts
---|---| vue // 打包、测试实例、项目整体入口模块
---|---|---| dist
---|---|---| examples
---|---|---| src
---|---|---|---| index.ts 出口文件
二、开整
reactive 目标:构建 reactive 函数,获取 proxy 实例
1. 创建 packages/reactivity/src/reactive.ts
模块
typescript
import { mutableHandlers } from './baseHandlers'
/**
* 响应性 Map 对象 🤔🤔🤔思考一下:这里为什么用new WeakMap进行缓存呢?
* key: target
* val: proxy
*/
export const reactiveMap = new WeakMap<object, any>()
/**
* 返回响应性对象(复杂数据类型)
* @param target 被代理对象
* @returns 代理对象
*/
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
/**
* 实现创建响应性对象
* @param target 被代理对象
* @param baseHandlers handler
* @returns proxy
*/
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
}
2. 创建 packages/reactivity/src/baseHandlers.ts
模块
arduino
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
3. 👀 解答时间 WeakMap 👀
通过上面文档对好兄弟 WeakMap 和 Map 的介绍,我们大致可以获取到他们都是 {key, value} 的结构对象,那有什么不一样的地方呢,下面来看一段对于WeakMap的key的描述:
我们可以提取几个关键字,键(即key)必须是对象
,不会创建对它的键(即key)的强引用
, 不会阻止该对象被垃圾回收
,总结一下:1. key
必须是对象,2. key
是弱引用的,下面是搬运的前辈们的文章,希望可以帮助大家理解强引用,弱引用,以及垃圾回收,简单描述的话:
- 弱引用:不会影响垃圾回收机制。即:WeakMap 的 key 不再存在任何引用时,会被直接回收。
- 强引用:会影响垃圾回收机制。存在强应用的对象永远 不会 被回收。
WeakMap 数据格式,👣注:后续会用到👣
key
: 响应性对象value
: Map 对象key
:响应性对象的指定属性value
:指定对象的指定属性的 执行函数
4. baseHandlers.ts
模块 完善
对于 Proxy 来说,它的 handler
可以监听 代理对象
的 getter
和 setter
,下面我们来完善一下 mutableHandlers
csharp
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {
get,
set
}
/**
* 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
}
}
/**
* setter 回调方法
*/
const set = createSetter()
/**
* 创建 setter 回调方法
*/
function createSetter() {
return function set(target: object, key: string | symbol, value: unknown, receiver: object) {
// 通过 Reflect.set 设置新值
const result = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key, value)
return result
}
}
5. effect.ts
创建 track && trigger,构建 effect 函数,生成 ReactiveEffect 实例
typescript
/**
* 收集依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function track(target: object, key: unknown) {
}
/**
* 触发依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
* @param newValue 指定 key 的最新值
* @param oldValue 指定 key 的旧值
*/
export function trigger(target: object, key?: unknown, newValue?: unknown) {
}
/**
* effect 函数
* @param fn 执行方法
* @returns 以 ReactiveEffect 实例为 this 的执行函数
*/
export function effect<T = any>(fn: () => T) {
// 生成 ReactiveEffect 实例
const _effect = new ReactiveEffect(fn)
// 执行 run 函数
_effect.run()
}
/**
* 当前的 effect, 单例模式:只会创建且仅创建一次对象的设计模式
*/
export let activeEffect: ReactiveEffect | undefined
/**
* 触发依赖的执行类
*/
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
// 为 activeEffect 赋值
activeEffect = this
// 执行 fn 函数
return this.fn()
}
}
5. effect.ts
完善 track && trigge
从上面我们写的内容来看,接下来我们需要在 getter
收集当前的 fn
,在 setter
的时候执行对应的 fn
函数,需要注意的是 fn对应哪个响应数据对象的哪个属性 ,回忆一下我们是不是可以利用WeakMap
的特性来进行实现一下呢
typescript
type keyToDepMap= Map<any, ReactiveEffect>
/**
* 收集所有依赖的 WeakMap 实例
*/
const targetMap = new WeakMap<any, keyToDepMap>()
/**
* 收集依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function track(target: object, key: unknown) {
// 如果:不存在执行函数,则:直接 return
if(!activeEffect) return
// 从 targetMap 中, 通过 target 获取 map
let depsMap = targetMap.get(target)
// 如果:获取的 map 不存在 则:生成新的 map 对象,并将 map 对象 赋值给对应的 value
if(!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 为指定 map,指定 key 设置回调函数
depsMap.set(key, activeEffect)
}
/**
* 触发依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function trigger(target: object, key?: unknown) {
// 根据 target 获取存储的 map 实例
const depsMap = targetMap.get(target)
// 如果:map 不存在,则:直接返回 return
if(!depsMap) {
return
}
// 根据 key,从 depsMap 中获取 value,该 value 是 ReactiveEffect 类型
const effect = depsMap.get(key) as ReactiveEffect
// 如果:effect 不存在,则:直接返回 return
if(!effect) {
return
}
// 执行 effect 中保存的 fn 函数
effect.fn()
}
至此我们已经构建出了一个简易版的 reactive 函数,下面来小试牛刀一下,看看有没有问题。
7. 导出
reactive/src/index.ts
javascript
export { reactive } from "./reactive";
export { effect } from './effect';
vue/src/index.ts
javascript
export { reactive, effect } from '@vue/reactivity'
8. packages/vue/examples/reactivity/reactive.html
应用
xml
<body>
<div id="app"></div>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
effect(() => {
document.querySelector("#app").innerText = obj.name;
})
setTimeout(() => {
obj.name = "李四";
}, 2000);
console.log('obj', obj)
</script>
</body>
我们看到已经可以实现数据的更新了,是不是感觉很完美了,那现在我们改成以下代码,我们上面的逻辑是否还适用呢?
xml
<body>
<div id="app">
<p id="p1"></p>
<p id="p2"></p>
</div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#p1').innerText = obj.name
})
effect(() => {
document.querySelector('#p2').innerText = obj.name
})
setTimeout(() => {
obj.name = '李四'
}, 2000);
</script>
运行我们可以看到p1并未更新,回忆一下,我们在构建
KeyToDepMap
对象时,Value
是不是只能是一个ReactiveEffect
,那该从哪里入手才能实现一对多
呢,请看下图见分晓:
通过改变KeyToDepMap
的 Value
是不是可以对应一个set数组
9. 一对多
dep.ts
typescript
import { ReactiveEffect } from './effect'
export type Dep = Set<ReactiveEffect>
/**
* 依据 effects 生成 dep 实例
*/
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effets) as Dep
return dep
}
effect.ts
- 修改
KeyToDepMap
的泛型
typescript
import { Dep } from './dep'
type keyToDepMap = Map<any, Dep>
- 修改
track
方法,处理Dep
类型数据
scss
export function track(target: object, key: unknown) {
// 获取指定 key 的 dep
let dep = depsMap.get(key)
// 如果:dep 不存在, 则:生成一个新的 dep,并放入到 depsMap 中
if(!dep) {
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
}
/**
* 利用 dep 依次跟踪 指定 key 的 effect
* @param dep
*/
export function trackEffects(dep: Dep) {
dep.add(activeEffect!)
}
- 修改
trigger
方法,依次读取dep
中保存的依赖
scss
export function trigger(target: object, key?: unknown) {
// 根据 target 获取存储的 map 实例
const depsMap = targetMap.get(target)
// 如果:map 不存在,则:直接返回 return
if(!depsMap) {
return
}
// 根据指定的 key,获取 dep 实例
let dep: Dep | undefined = depsMap.get(key)
// 如果:dep 不存在,则:直接返回 return
if(!dep) {
return
}
// 触发 dep
triggerEffects(dep)
}
/**
* 依次触发 dep 中保存的依赖
*/
export function triggerEffects(dep: Dep) {
// 把 dep 构建成一个数组
const effects = isArray(dep) ? dep : [...dep]
// 依次触发
for (const effect of effects) {
triggerEffect(effect)
}
}
/**
* 触发指定的依赖
*/
export function triggerEffect(effect: ReactiveEffect) {
effect.run()
}
shared/src/index.ts
arduino
/**
* 判断是否为一个数组
*/
export const isArray = Array.isArray
✨✨✨bingo
✨✨✨我们再去运行一下demo就可以正常更新啦~~~
小结一下:目前我们的
reactive
结合effect
可以响应数据的渲染和更新,实现大致可以分为以下几个步骤:
-
通过
proxy
的setter
和getter
来实现的数据监听 -
配合
effect
函数进行使用 -
基于
WeakMap
完成的依赖收集和处理 -
通过改变
KeyToDepMap
的Value
实现一对多的更新渲染注:reactive的局限性
- 由于 reactive 是把传入的
objec
作为proxy
的target
参数,对于proxy
来说,只能代理对象
,并不能代理基本数据类型
。 - 由于只有
proxy
类型的 代理对象 才可以被监听getter
和setter
,一旦解构之后,解构的属性,将不具备响应性
.
- 由于 reactive 是把传入的