欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > 《Vue进阶教程》(8) 初步完善响应式系统详细介绍

《Vue进阶教程》(8) 初步完善响应式系统详细介绍

2025/2/22 2:05:36 来源:https://blog.csdn.net/2403_88103571/article/details/144502768  浏览:    关键词:《Vue进阶教程》(8) 初步完善响应式系统详细介绍

经过前面的努力, 我们初步了解最基本的响应式原理, 并且实现了一个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) 实现思路

  1. 将当前副作用函数保存到一个全局变量
  2. 当执行副作用函数时, 会触发代理对象的自定义get操作
  3. 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 小结

💡 小结

  1. 在get时收集依赖: 收集不同代理对象不同属性所依赖的副作用函数
  2. 在set时触发依赖: 取出当前属性所依赖的所有副作用函数, 重新执行

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词