经过前面的努力, 我们初步了解最基本的响应式原理, 并且实现了一个Demo
接下来, 我们要一步步完善响应式系统~😀
1 实现一对多
所谓一对多
: 一个属性对应多个副作用函数
🤔思考
如果一个属性存在多个与之对应的副作用函数
理论上当属性改变时, 属性关联的每一个副作用函数都应该重新执行
1) 手动方式
示例
<!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><div id="app">xiaopang</div><script>function isObject(value) {return typeof value === 'object' && value !== null}/*** 创建响应式数据* @param [object]: 普通对象* @return [Proxy]: 代理对象*/function reactive(data) {if (!isObject(data)) returnreturn new Proxy(data, {get(target, key) {return target[key]},set(target, key, value) {target[key] = valueeffect() // 调用副作用函数, 调用effect函数会重新获取新的数据effect1()return true},})}// 定义一个响应式数据 : 触发者const state = reactive({ name: 'xiaopang' })// 定义一个副作用函数 : 响应者function effect() {console.log('effect被执行了...')app.innerHTML = state.name}function effect1() {console.log('effect1被执行了...')state.name}// 当state.name改变时, 重新执行对应副作用函数effectsetTimeout(() => {state.name = 'xxp'}, 1000)</script></body>
</html>
很显然, 我们可以通过手动的方式, 依次执行每一个关联的副作用函数.
2) 自动方式
我们可以创建一个存储所有副作用函数的空间, 这个空间叫做副作用桶
首先想到的是数组, 数组的每个元素都是一个副作用函数
<!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><div id="app">xiaopang</div><script>// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数const bucket = [] // 新增function isObject(value) {return typeof value === 'object' && value !== null}/*** 创建响应式数据* @param [object]: 普通对象* @return [Proxy]: 代理对象*/function reactive(data) {if (!isObject(data)) returnreturn new Proxy(data, {get(target, key) {return target[key]},set(target, key, value) {target[key] = value// 从副作用函数桶中依次取出每一个元素(副作用函数)执行bucket.forEach((fn) => fn()) // 修改return true},})}// 定义一个响应式数据 : 触发者const state = reactive({ name: 'xiaopang' })// 定义一个副作用函数 : 响应者function effect() {console.log('effect被执行了...')app.innerHTML = state.name}bucket.push(effect)bucket.push(effect)function effect1() {console.log('effect1被执行了...')state.name}bucket.push(effect1)// 当state.name改变时, 重新执行对应副作用函数effectsetTimeout(() => {state.name = 'xxp'}, 1000)</script></body>
</html>
但是数组元素是也可重复的, 为了效率, 我们还要去重, 而Set
集合是默认去重的
因此, 考虑用Set
实现, Set集合中存放的每个元素也是副作用函数
<!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><div id="app">xiaopang</div><script>// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数const bucket = new Set() // 修改function isObject(value) {return typeof value === 'object' && value !== null}/*** 创建响应式数据* @param [object]: 普通对象* @return [Proxy]: 代理对象*/function reactive(data) {if (!isObject(data)) returnreturn new Proxy(data, {get(target, key) {return target[key]},set(target, key, value) {target[key] = value// 从副作用函数桶中依次取出每一个元素(副作用函数)执行bucket.forEach((fn) => fn())return true},})}// 定义一个响应式数据 : 触发者const state = reactive({ name: 'xiaopang' })// 定义一个副作用函数 : 响应者function effect() {console.log('effect被执行了...')app.innerHTML = state.name}bucket.add(effect)function effect1() {console.log('effect1被执行了...')state.name}bucket.add(effect1)const effect2 = () => {console.log(state.name)}bucket.add(effect2)// 当state.name改变时, 重新执行对应副作用函数effectsetTimeout(() => {state.name = 'xxp'}, 1000)</script></body>
</html>
2 实现依赖收集
1) 为什么要依赖收集
前面, 我们并没有区分不同属性对应的副作用函数, 而是全部放入到副作用桶
里.
示例
<!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>// 定义一个副作用桶bucketconst bucket = new Set() // 修改/*** 定义响应式* @param [object] : 普通对象* @return [Proxy] : 代理对象*/function reactive(data) {// 如果传入的data不是一个普通对象, 不处理if (typeof data !== 'object' || data == null) returnreturn new Proxy(data, {get(target, key) {// console.log(`自定义访问${key}`)return target[key]},set(target, key, value) {// console.log(`自定义设置${key}=${value}`)target[key] = value // 先更新值bucket.forEach((fn) => fn())return true},})}const pState = reactive({ name: 'hello', age: 20 })function effectName() {console.log('effectName...', pState.name)}bucket.add(effectName)function effectAge() {console.log('effectAge...', pState.age)}bucket.add(effectAge)setTimeout(() => {pState.name = 'brojie'}, 1000)</script></body>
</html>
接下来我们思考这样的问题
🤔思考
如果一个副作用函数effectName
只引用了name
另一个副作用函数effectAge
只引用了age
理论上, 更新name
只需要重新执行effectName
而不需要重新执行effectAge
换句话说, 依赖收集就是建立属性
与副作用函数
的对应关系
2) 实现思路
- 将当前
副作用函数
保存到一个全局变量 - 当执行
副作用函数
时, 会触发代理对象的自定义get
操作 - 在
get
操作时, 将全局变量中保存的函数添加到副作用桶
3) 具体实现
<!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>// 定义一个副作用桶bucketconst bucket = new Set() // 修改// 定义一个全局变量, 作于保存 `当前副作用函数`let activeEffect = null/*** 定义响应式* @param [object] : 普通对象* @return [Proxy] : 代理对象*/function reactive(data) {// 如果传入的data不是一个普通对象, 不处理if (typeof data !== 'object' || data == null) returnreturn new Proxy(data, {get(target, key) {// console.log(`自定义访问${key}`)if (activeEffect != null) {bucket.add(activeEffect)}return target[key]},set(target, key, value) {// console.log(`自定义设置${key}=${value}`)target[key] = value // 先更新值bucket.forEach((fn) => fn())return true},})}const pState = reactive({ name: 'hello', age: 20 })// 定义副作用函数function effectName() {console.log('effectName...', pState.name)}// 将副作用函数保存到全局变量中activeEffect = effectName// 执行副作用函数effectName()// 重置全局变量activeEffect = nullsetTimeout(() => {pState.name = 'brojie'}, 1000)</script></body>
</html>
4) 优化
接下来, 我们优化一下, 封装一个注册函数
, 方便注册副作用函数
<!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>// 定义一个副作用桶bucketconst bucket = new Set() // 修改// 定义一个全局变量, 作于保存 `当前副作用函数`let activeEffect = null/*** 定义响应式* @param [object] : 普通对象* @return [Proxy] : 代理对象*/function reactive(data) {// 如果传入的data不是一个普通对象, 不处理if (typeof data !== 'object' || data == null) returnreturn new Proxy(data, {get(target, key) {// console.log(`自定义访问${key}`)if (activeEffect != null) {bucket.add(activeEffect)}return target[key]},set(target, key, value) {// console.log(`自定义设置${key}=${value}`)target[key] = value // 先更新值bucket.forEach((fn) => fn())return true},})}/*** 注册副作用函数* @params [function]: 要注册的 副作用函数*/function registEffect(fn) {if (typeof fn !== 'function') return// 将当前注册的副作用函数 保存 到全局变量中activeEffect = fn// 执行当前副作用函数, 收集依赖fn()// 重置全局变量activeEffect = null}const pState = reactive({ name: 'hello', age: 20 })registEffect(function effectName() {console.log('effectName...', pState.name)})setTimeout(() => {pState.name = 'brojie'}, 1000)</script></body>
</html>
提示
源码里注册副作用函数的函数名就叫effect
这里我们重在理解函数的功能, 不用去纠结名字
5) 改进桶结构
看起来现在可以自动收集依赖. 但是依然解决不了不同的属性对应不同的副作用函数集合这个问题.
因此, 我们需要改进桶结构
将桶
改造成一个Map映射表, 不同的属性对应不同的Set集合
示例
// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
const bucket = new Map() // 修改 [name: Set(fn, fn), age: Set(fn, fn)]// 定义一个全局变量, 保存当前正在执行的副作用函数
let activeEffect = nullfunction isObject(value) {return typeof value === 'object' && value !== null
}// 收集依赖
function track(target, key) {if (!activeEffect) returnlet depSet = bucket.get(key)if (!depSet) {depSet = new Set()bucket.set(key, depSet)}// 只有activeEffect有值时(保存的副作用函数), 才添加到桶中depSet.add(activeEffect)
}function trigger(target, key) {// 从副作用函数桶中依次取出每一个元素(副作用函数)执行let depSet = bucket.get(key)if (depSet) {depSet.forEach((fn) => fn())}
}
/**
* 创建响应式数据
* @param [object]: 普通对象
* @return [Proxy]: 代理对象
*/
function reactive(data) {if (!isObject(data)) returnreturn new Proxy(data, {get(target, key) {// 在get操作时, 收集依赖track(target, key)return target[key]},set(target, key, value) {target[key] = value// 在set操作时, 触发副作用重新执行trigger(target, key)return true},})
}/**
* 注册副作用函数
* @param [function]: 需要注册的 副作用函数
*/
function effect(fn) {if (typeof fn !== 'function') return// 记录正在执行的副作用函数activeEffect = fn// 调用副作用函数fn()// 重置全局变量activeEffect = null
}
至此, 我们通过改进桶结构, 可以区分同一个代理对象的不同的属性
6) 进一步改进桶结构
🤔思考
如果不同的源对象存在同名属性, 就会出现问题
比如
pState代理的源对象上存在name
属性
pState1代理的源对象上也存在name
属性
这样, 在bucket桶里, 就不能区分不同的代理对象
问题示例
<!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>// 定义一个副作用桶bucketconst bucket = new Map() // 修改// 定义一个全局变量, 作于保存 `当前副作用函数`let activeEffect = null// 收集依赖function track(target, key) {// 根据不同的key, 获取对应的集合let depSet = bucket.get(key)if (!depSet) {// 如果不存在, 创建一个新的集合, 并添加到桶中depSet = new Set()bucket.set(key, depSet) // 建立 key -> Set的对应关系}depSet.add(activeEffect) // 将当前副作用函数添加到对应的集合}function trigger(target, key) {let depSet = bucket.get(key)if (depSet) {// 如果对应的集合存在, 遍历集合中的每个函数depSet.forEach((fn) => fn())}}/*** 定义响应式* @param [object] : 普通对象* @return [Proxy] : 代理对象*/function reactive(data) {// 如果传入的data不是一个普通对象, 不处理if (typeof data !== 'object' || data == null) returnreturn new Proxy(data, {get(target, key) {// console.log(`自定义访问${key}`)if (activeEffect != null) {// 收集依赖track(target, key)}return target[key]},set(target, key, value) {// console.log(`自定义设置${key}=${value}`)target[key] = value // 先更新值// 触发更新trigger(target, key)return true},})}/*** 注册副作用函数* @params [function]: 要注册的 副作用函数*/function registEffect(fn) {if (typeof fn !== 'function') return// 将当前注册的副作用函数 保存 到全局变量中activeEffect = fn// 执行当前副作用函数, 收集依赖fn()// 重置全局变量activeEffect = null}const pState = reactive({ name: 'hello', age: 20 })const pState1 = reactive({ name: 'p1' })registEffect(function effectFn() {console.log('effectFn...', pState.name)})registEffect(function effectFn1() {console.log('effectFn1...', pState1.name)})console.log(bucket)setTimeout(() => {pState.name = 'brojie'}, 1000)</script></body>
</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>// 定义一个副作用桶bucketconst bucket = new WeakMap() // 修改// 定义一个全局变量, 作于保存 `当前副作用函数`let activeEffect = null// 收集依赖function track(target, key) {// 根据不同的target, 获取对应的Maplet depMap = bucket.get(target)if (!depMap) {depMap = new Map()bucket.set(target, depMap) // 建立target -> Map的对应关系}// 根据不同的key, 获取对应的集合let depSet = depMap.get(key)if (!depSet) {// 如果不存在, 创建一个新的集合depSet = new Set()depMap.set(key, depSet) // 建立 key -> Set的对应关系}depSet.add(activeEffect) // 将当前副作用函数添加到对应的集合}function trigger(target, key) {let depMap = bucket.get(target)if (!depMap) returnlet depSet = depMap.get(key)if (depSet) {// 如果对应的集合存在, 遍历集合中的每个函数depSet.forEach((fn) => fn())}}/*** 定义响应式* @param [object] : 普通对象* @return [Proxy] : 代理对象*/function reactive(data) {// 如果传入的data不是一个普通对象, 不处理if (typeof data !== 'object' || data == null) returnreturn new Proxy(data, {get(target, key) {// console.log(`自定义访问${key}`)if (activeEffect != null) {// 收集依赖track(target, key)}return target[key]},set(target, key, value) {// console.log(`自定义设置${key}=${value}`)target[key] = value // 先更新值// 触发更新trigger(target, key)return true},})}/*** 注册副作用函数* @params [function]: 要注册的 副作用函数*/function registEffect(fn) {if (typeof fn !== 'function') return// 将当前注册的副作用函数 保存 到全局变量中activeEffect = fn// 执行当前副作用函数, 收集依赖fn()// 重置全局变量activeEffect = null}const pState = reactive({ name: 'hello', age: 20 })registEffect(function effectName() {console.log('effectName...', pState.name)})registEffect(function effectAge() {console.log('effectAge...', pState.age)})console.log(bucket)setTimeout(() => {pState.name = 'brojie'}, 1000)</script></body>
</html>
至此, 我们可以区分不同的代理对象下不同属性对应的副作用函数集合.
真正实现了完善的桶结构~✌
如果学到这里, 恭喜你, 已经实现了一个可用的响应式系统! ^_^😃
3 小结
💡 小结
- 在get时收集依赖: 收集不同代理对象不同属性所依赖的副作用函数
- 在set时触发依赖: 取出当前属性所依赖的所有副作用函数, 重新执行