前言
为了实现响应式系统,我们先明确2个名词的概念,响应式数据和副作用函数。
- 响应式数据:当数据发生改变时,使用该数据的函数会重新执行。
- 副作用函数:直接或间接地影响其他函数的执行,比如在函数里面读取或修改了其他函数也可以访问的变量,如全局变量。
我们先看以下代码:
// 01
const obj = { name: 'vue', version: 3 }
const effect = () => {
document.body.innerHTML = obj.name
}
effect()
obj.name = 'new vue'
effect
执行时,读取了obj
的属性,我们希望obj
发生改变时,会重新执行effect
函数,但显然obj
只是一个普通的对象,根本无法实现这个目标,于是我们利用ES6的新特性Proxy
,把obj
变成响应式数据。
响应式数据的基本实现
上面的代码涉及到数据的读取和修改:执行时effect
里面读取了数据(obj.name
),然后修改obj.name
,因此我们对obj
的读取和修改分别做拦截:
- 当读取时,把
effect
放进一个“桶”里 - 当设置时,把“桶”里面的
effect
拿出来执行
因此,我们按照上面的思路实现以下代码:
// 02
const bucket = new Set()
const obj = { name: 'vue', version: 3 }
const data = new Proxy(obj, {
get(target, key, receiver) {
bucket.add(effect)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
for (const effect of bucket) {
effect()
}
return true
}
})
const effect = () => {
document.body.innerHTML = data.name
}
effect()
setTimeout(() => {
data.name = 'new vue'
}, 1000);
发现1s后修改了data
,effect
重新执行了,到这里,我们就实现了一个基本的响应式系统,但是上面的代码仍然有很多待改善的地方,接下来我们就一步一步去完善它。
在那之前,我们大脑里要把一句话深深印在脑海里:(重要的事情说3遍)
副作用函数执行时建立与数据之间的联系!
副作用函数执行时建立与数据之间的联系!!
副作用函数执行时建立与数据之间的联系!!!
好了,我们可以开始后面的内容了。
完善的响应式系统
1. 解决副作用函数硬编码的问题
上面的代码我们是使用编码的形式来把副作用函数effect
放进“桶”里面的,因此我们要首先解决这个问题,于是通过提供注册副作用函数机制来解决,我们改写effect
代码如下:
// 03
let activeEffect = null
const effect = (fn) => {
// 将当前的副作用函数赋给activeEffect
activeEffect = fn
// 执行副作用函数
activeEffect()
}
我们通过向effect
注册一个函数,然后把当前执行的副作用函数赋给全局变量activeEffect
,一是为了解决硬编码的问题,二则是在收集对象的副作用函数时直接读取全局变量activeEffect
。
于是我们需要修改data
的写法如下:
const data = new Proxy(obj, {
get(target, key, receiver) {
bucket.add(activeEffect) // 修改为添加activeEffect
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
for (const effect of bucket) {
effect()
}
return true
}
})
测试:
// 向effect注册一个匿名函数
effect(()=> {
document.body.innerHTML = data.name
})
setTimeout(() => {
data.name = 'new vue'
}, 1000);
这样就可以了。
2. 不同对象的不同属性之间的分别关联的副作用函数
大家可能注意到了,就是当我们修改version
属性的时候,同样会执行匿名副作用函数,而该函数里面只读取了对象的name
属性,我们希望副作用函数的执行只跟与它们关联的对象属性绑定,于是我们很容易得到以下关系:这里可能有n个对象(target),一个对象有n个属性(key),一个属性关联n个副作用函数(deps)。据上关系设计如下数据格式:
const bucket = new WeakMap() // 键值为target,值为depsMap
const depsMap = new Map() // 键值为key, 值为deps
const deps = new Set() // 存储副作用函数,我们也叫依赖集合
因此我们根据上面的数据结构和逻辑重新修改响应式数据:
// 04
const data = new Proxy(obj, {
get(target, key, receiver) {
if (!activeEffect) return Reflect.get(target, key, receiver)
// 获取该对象的所有依赖映射
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 获取对应key的依赖集合
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
const deps = bucket.get(target)?.get?.(key)
deps && deps.forEach(fn => fn())
return true
}
})
到这里,我们就已经基本实现了响应式系统,下一篇我们将继续改造。