当前位置: 首页 > ds >正文

Vue.js设计于实现 - 响应式(三)

副作用函数和响应式数据

  • 副作用函数

再次说明一下,主要读取或修改了公共变量的都是副作用函数
例如

function effect() {document.body.innerText = '123'
}
let val = 1
function effect2() {val = 2
}
  • 响应式数据

如果在一个副作用函数中读取或修改了某个对象的属性值,那么我们希望当值变化后,副作用函数自动重新执行,实现该目标,这个对象就是响应式对象

响应式数据基本实现

拦截一个对象的读取和设置操作,当读取字段时,将副作用函数存入一个“桶”中,当这个字段改变时,再执行副作用函数

// 存储副作用函数的桶
const bucket = new Set()
const data = {text: 'hello world'
}
const obj = new Proxy(data, {get(target, key) {// 收集依赖bucket.add(effect)// 返回属性值return target[key]},set(target, key, value) {target[key] = value// 触发副作用函数bucket.forEach(fn => fn())// 返回 true 表示设置成功return true}
})
  • 实现注册副作用函数

上面的内容只是一个最小型的响应式结构,实际上我们的副作用函数不会都叫effect,那么如何实现不论如何命名函数都能收集依赖呢,这里采用一个全局变量activeEffect来保存

let activeEffect
function effect (fn) {activeEffect = fnfn()
}
// 我们执行自己的函数时,使用effect调用
const changeBody = () => {document.body.innerText = obj.text
}
effect(changeBody)

那么getter的代码就要改为

get(target, key) {// 收集依赖if(activeEffect) {bucket.add(activeEffect)}// 返回属性值return target[key]
},
  • 实现副作用函数与对象的具体属性关联

在上面的内容中,当我们修改obj中的其他属性,并且该属性不在副作用函数中时,副作用函数依然会执行,因此要重新设计“桶”结构,添加映射关系

			-- 键 -- [函数]
对象   -- 键 -- [函数]-- 键 -- [函数]weakMap: {obj1: Map1,obj2: Map2
}
Map1: {key1: [ fn1, fn2 ],key2: [ fn1 ]
}
const data = { text: 'hello world' }// 临时保存副作用函数
let activeEffect
function effect (fn) {activeEffect = fnfn()
}
// 存放依赖的桶
const bucket = new WeakMap()
const obj = new Proxy(data, {get (target, key) {// 触发依赖收集track(target, key)return target[key]},set (target, key, value) {target[key] = value// 触发副作用函数trigger(target, key)}
})
function track (target, key) {if (!activeEffect) returnlet depsMap = bucket.get(target)if (!depsMap) {// 没有对象存对象bucket.set(target, (depsMap = new Map()))}let deps = depsMap.get(key)if (!deps) {// 没有对象的键存对象的键depsMap.set(key, (deps = new Set()))}deps.add(activeEffect)
}
function trigger (target, key) {// 获取obj[key]执行副作用函数const depsMap = bucket.get(target)if (!depsMap) returnconst effects = depsMap.get(key)effects && effects.forEach(fn => fn())
}

这里使用weakMap不使用Map的原因是,weakMap对key是弱引用,一旦key被垃圾回收,则对应的键和值就访问不到了,避免内存溢出

  • 实现cleanup

如果副作用函数是一个三元表达式

function fn () {obj.ok ? obj.text : ''
}

那么当ok为false时,为了避免不必要的更新,我们需要清除obj.text的依赖收集,变为true时再重新依赖

解决方法为,在每次副作用函数执行前,将其从相关联的依赖集合中移除,后续执行过程中不读取该属性就不会进行收集

我们要重新设计副作用函数effect,在内部定义了新的effectFn函数,并为其添加deps属性,用来存储当前副作用函数的依赖集合

let activeEffect
function effect(fn) {const effectFn = () => {activeEffect = effectFnfn()}// 用来存储当前副作用函数的依赖集合effectFn.deps = []effectFn()
}

在track中收集副作用函数对应的键 对应的函数集合

function track (target, key) {if (!activeEffect) returnlet depsMap = bucket.get(target)if (!depsMap) {bucket.set(target, (depsMap = new Map()))}let deps = depsMap.get(key)if (!deps) {depsMap.set(key, (deps = new Set()))}deps.add(activeEffect)// 将对应键的对应函数集合存入副作用函数的deps数组中activeEffect.deps.push(deps)
}

这样在副作用函数执行前遍历deps,删除其中包含自身的,即可实现移除

function cleanup(effectFn) {for(let i = 0; i < effectFn.deps.length; i++) {// deps为Set集合const deps = effectFn.deps[i]// 删除自身deps.delete(effectFn)}// 重置effectFn.deps数组effectFn.deps.length = 0
}
let activeEffect
function effect(fn) {const effectFn = () => {// 调用清除cleanup(effectFn)activeEffect = effectFnfn()}// 用来存储当前副作用函数的依赖集合effectFn.deps = []effectFn()
}

此时运行代码会无限循环,原因是trigger使用了forEach遍历,在语言规范中,当forEach遍历Set集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时forEach遍历没有结束,那么该值会被重新访问
即这个代码会无限循环

const set = new Set([1])
set.forEach(item => {set.delete(1)set.add(1)console.log('这段代码会无限循环')
})

因此要修改trigger

function trigger(target, key) {const depsMap = buckect.get(target)if(!depsMap) returnconst effects = depsMap.get(key)// 创建中间变量用于执行const effectToRun = new Set(effects) effectToRun.forEach(effectFn => effectFn())
}

目前的代码为

const data = { text: 'hello world' }// 清除副作用函数
function cleanup(effectFn){for(let i = 0; i < effectFn.deps.length; i++) {const deps = effectFn.deps[i]// 删除掉所有Set中的自身deps.delete(effectFn)}// 清空副作用函数的依赖集合effectFn.deps.length = 0
}// 临时保存副作用函数
let activeEffect
function effect (fn) {const effectFn = () => {// 执行副作用函数前删除集合中的自身cleanup(effectFn)activeEffect = effectFnfn()}// 存储当前副作用函数依赖集合effectFn.deps = []effectFn()
}
// 存放依赖的桶
const bucket = new WeakMap()
const obj = new Proxy(data, {get (target, key) {// 触发依赖收集track(target, key)return target[key]},set (target, key, value) {target[key] = value// 触发副作用函数trigger(target, key)}
})
function track (target, key) {if (!activeEffect) returnlet depsMap = bucket.get(target)if (!depsMap) {// 没有对象存对象bucket.set(target, (depsMap = new Map()))}let deps = depsMap.get(key)if (!deps) {// 没有对象的键存对象的键depsMap.set(key, (deps = new Set()))}deps.add(activeEffect)// 将当前副作用函数存入依赖集合activeEffect.deps.push(deps)
}
function trigger (target, key) {// 获取obj[key]执行副作用函数const depsMap = bucket.get(target)if (!depsMap) returnconst effects = depsMap.get(key)// 创建中间变量避免无限循环const effectsToRun = new Set(effects)effectsToRun.forEach(effectFn => effectFn())// effects && effects.forEach(fn => fn())
}
http://www.xdnf.cn/news/17687.html

相关文章:

  • 音视频学习(五十二):ADTS
  • Graham 算法求二维凸包
  • Python 2025:最新技术趋势与展望
  • 每日五个pyecharts可视化图表-line:从入门到精通 (2)
  • lesson34:深入理解Python线程:从基础到实战优化
  • jupyter notebook如何打开其他盘目录
  • MCP学习与实践
  • [激光原理与应用-222]:机械 - 3D设计与2D设计的异同比较
  • Linux 虚拟机磁盘空间占满-全面清理方案
  • Cesium1.95中如何高效管理 1500 个高频实体
  • 赋值运算符指南
  • 代码可读性与维护性的实践与原则
  • word中,添加新的参考文献后,其他参考文献的交叉引用不能及时更新的解决办法
  • 《Webpack与Vite热模块替换机制深度剖析与策略抉择》
  • 二维前缀和问题
  • 如何在 Ubuntu 24.04 LTS Linux 上安装 MySQL 服务器
  • 电脑本地摄像头做成rtsp流调用测试windows系统中
  • 【大智慧数据】心智开花的时候
  • AI测试助手如何让Bug无处可藏
  • Dify 从入门到精通(第 26/100 篇):Dify 的知识图谱集成
  • 2025最新免费的大模型和免费的大模型API有哪些?(202508更新)
  • 2025年6月电子学会全国青少年软件编程等级考试(Python二级)真题及答案
  • 【Linux指南】Vim的全面解析与深度应用
  • C语言第八章指针四
  • 【接口自动化】初识pytest,一文讲解pytest的安装,识别规则以及配置文件的使用
  • Jotai:React轻量级状态管理新选择
  • Code Exercising Day 10 of “Code Ideas Record“:StackQueue part02
  • SQL三剑客:DELETE、TRUNCATE、DROP全解析
  • CentOS7.9 离线安装mysql数据库
  • CPP继承