js数据劫持和监听实现ui更新技术细节
JsonOb 响应式:数据劫持 + 变化监听完整技术细节

先纠正PPT笔误: Object.Defefirbairty → Object.defineProperty
这套方案是Vue2 同源响应式方案,也是很多游戏引擎、小程序自研状态库 JsonOb 的底层实现。
一、整体流程对照PPT架构
plaintext
原始JS对象
↓
Object.defineProperty 深度劫持(get/set)【数据拦截】
↓
数据发生修改 → set触发 → 发送变更信号
↓
信号广播系统(订阅发布模式)
↓
通知所有依赖当前字段的UI组件,执行UI更新
二、核心1:Object.defineProperty 数据劫持原理
- API基础作用
Object.defineProperty(obj, key, descriptor)
对对象的指定属性重新定义,拦截 读取(get)、赋值(set)。
javascript
let rawData = { name:"test" };
// 劫持name属性
Object.defineProperty(rawData, 'name', {
get(){
// 读取时:收集当前正在执行的UI渲染函数(依赖收集)
track()
return this._value;},
set(newVal){
if(newVal === this._value) return; // 值无变化跳过
this._value = newVal;
// 修改时:触发信号派发,通知订阅者更新
trigger()}
})
- 深度劫持(深层对象递归处理)
普通 defineProperty 只能劫持一层属性,嵌套对象 {a:{b:1}} 内层b不会监听。
实现方案:
1. 递归遍历对象所有属性;
2. 如果属性值依然是 Object/Array ,继续对它执行 defineProperty 包装;
这就是PPT写的【深度属性劫持】。
- 致命短板(defineProperty 天生缺陷)
1. 无法监听新增属性、删除属性
obj.xxx = 1 新增字段不会触发set; delete obj.xxx 无钩子。
解决方案:框架提供 $set / $del API。
2. 不能原生监听数组下标修改
arr[2] = 999 不会触发set。
三、核心2:数组监听怎么实现(PPT第二条)
原生数组无法靠defineProperty监听下标变更,JsonOb采取 方法重写拦截:
1. 复制数组原型 Array.prototype ;
2. 重写会改变数组内容的变异方法: push pop shift unshift splice sort reverse ;
3. 当业务调用 arr.push() 时,先走重写后的包装函数:
- 执行原生数组操作
- 手动调用 trigger() 发送变更信号
javascript
// 伪代码思路
const rawPush = Array.prototype.push;
Array.prototype.push = function(...args){
const res = rawPush.apply(this, args);
trigger(this); // 广播数组变更
return res;
}
注意: arr[0] = xxx 、 arr.length = 10 依然无法被捕获,框架一般同样提供API规避。
四、核心3:依赖收集 & 信号广播(订阅发布)
劫持只是拿到“数据变化通知”,还需要知道哪些UI依赖这个字段。
- track() 依赖收集(get阶段)
1. UI组件执行渲染函数,读取响应式字段 data.name ;
2. 触发 get() ,执行 track ;
3. 把「当前渲染函数」存入当前属性的依赖列表。
数据结构简易模型:
plaintext
targetMap = {
对象: {
属性名: [组件更新回调1, 组件更新回调2]}
}
- trigger() 信号广播(set阶段)
当 set 检测数值改变:
1. 获取该属性保存的所有依赖回调;
2. 批量执行回调 → UI组件执行刷新。
也就是PPT里的【信号广播】。
五、PPT重点特性:enableBatch 批量变更
场景:一帧内连续多次修改数据
plaintext
data.a = 1
data.b = 2
data.c = 3
不做批处理:每次赋值立刻触发UI刷新 → 3次渲染,性能浪费。
enableBatch实现逻辑:
1. 开启批量模式后, trigger 不会立刻执行更新;
2. 将所有变更任务存入队列;
3. 等到当前JS执行栈结束(下一帧/微任务时机);
4. 去重、合并所有变更,统一执行一次UI更新。
游戏引擎、前端渲染库高频修改场景极其重要,减少重绘次数。
六、内存管理(PPT最后一条)
这套响应式很容易产生内存泄漏:
对象销毁后,依赖列表依然保存UI组件引用,组件无法GC。
JsonOb处理方案:
1. 组件销毁时,主动清除对应属性的依赖回调;
2. 使用弱引用容器(WeakMap)保存targetMap;
3. 对外提供销毁API,解除对象劫持,清空订阅列表。
七、拓展对比:defineProperty vs Proxy
你这套JsonOb使用 Object.defineProperty(Vue2方案)
现代方案用 Proxy (Vue3):
1. Proxy原生支持监听新增属性、数组下标、删除;
2. 不需要递归深度劫持,可以惰性代理;
缺点:不支持低版本运行环境。
很多小游戏引擎、老框架继续沿用defineProperty实现的JsonOb。
八、完整数据流总结(对应PPT框图)
1. 开发者修改响应式数据;
2. Object.defineProperty set 拦截赋值(数据拦截);
3. 判断值是否真正变化,避免无效通知;
4. 发送变更信号(批量模式下先入队列);
5. 信号广播遍历所有订阅该字段的UI组件;
6. 各个UI组件执行自身更新逻辑;
7. 组件卸载后自动清理依赖引用,防止内存泄露。
如果你需要,我可以写一份精简可运行的 JsonOb最小实现Demo(defineProperty版本),直观复现这套劫持+广播逻辑。
版权属于:Joyber
本文链接:https://blog.qqvbc.com/default/1502.html
转载时须注明出处及本声明