JsonOb 响应式:数据劫持 + 变化监听完整技术细节

Screenshot_20260729_085722_tv_danmaku_bili_UnitedBizDetailsActivity.jpg

先纠正PPT笔误: Object.Defefirbairty  →  Object.defineProperty 
这套方案是Vue2 同源响应式方案,也是很多游戏引擎、小程序自研状态库 JsonOb 的底层实现。

一、整体流程对照PPT架构

plaintext

原始JS对象

Object.defineProperty 深度劫持(get/set)【数据拦截】

数据发生修改 → set触发 → 发送变更信号

信号广播系统(订阅发布模式)

通知所有依赖当前字段的UI组件,执行UI更新
 

二、核心1:Object.defineProperty 数据劫持原理

  1. 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()

}
})
 

  1. 深度劫持(深层对象递归处理)

普通 defineProperty 只能劫持一层属性,嵌套对象  {a:{b:1}}  内层b不会监听。
实现方案:

1. 递归遍历对象所有属性;
2. 如果属性值依然是  Object/Array ,继续对它执行  defineProperty  包装;

这就是PPT写的【深度属性劫持】。

  1. 致命短板(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依赖这个字段。

  1. track() 依赖收集(get阶段)

1. UI组件执行渲染函数,读取响应式字段  data.name ;
2. 触发  get() ,执行 track ;
3. 把「当前渲染函数」存入当前属性的依赖列表。
数据结构简易模型:

plaintext

targetMap = {
对象: {

属性名: [组件更新回调1, 组件更新回调2]

}
}
 

  1. 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版本),直观复现这套劫持+广播逻辑。

标签: none

添加新评论