如何订阅JavaScript对象的变更?求非废弃实现方案
嘿,我完全懂你想要实现类似Redux那种订阅JavaScript对象变更的需求——毕竟Object.prototype.watch()和Object.observe()都废弃了,老帖子的方案又可能过时,确实让人头疼。下面给你几个当前完全可用的现代JS方案,结合你给出的store示例来讲解:
ES6引入的Proxy是专门用来拦截对象各种操作的API,完美替代废弃的监听方案,而且能处理数组、嵌套对象的变更,灵活性拉满。我们可以给目标对象套一层Proxy,同时维护一个订阅者队列,当对象的属性(包括数组元素)发生变化时,自动通知所有订阅者。
示例代码:
// 创建一个带订阅功能的状态容器 function createStore(initialState) { // 存储订阅者的数组 const subscribers = []; // 创建Proxy来拦截状态变更 const state = new Proxy(initialState, { set(target, property, value) { // 先执行原始赋值操作 target[property] = value; // 通知所有订阅者(返回状态副本避免外部直接修改) subscribers.forEach(callback => callback({ ...state })); return true; }, deleteProperty(target, property) { // 处理删除操作 const result = delete target[property]; subscribers.forEach(callback => callback({ ...state })); return result; } }); // 订阅方法:添加回调到队列,返回取消订阅函数 function subscribe(callback) { subscribers.push(callback); return () => { const index = subscribers.indexOf(callback); if (index !== -1) subscribers.splice(index, 1); }; } return { state, subscribe }; } // 用你的示例初始化store const { state: store, subscribe } = createStore({ anArray: ['Hi', 'my', '...'] }); // 订阅变更:每次store变化时打印最新状态 const unsubscribe = subscribe((newState) => { console.log('Store更新了:', newState); }); // 测试变更 store.anArray.push('name'); // 触发订阅回调 store.anArray[0] = 'Hello'; // 触发订阅回调 delete store.anArray[1]; // 删除元素同样触发 // 取消订阅(如果需要的话) // unsubscribe();
这个方案能监听数组的push、splice、直接修改索引等操作,完全覆盖你的需求。
如果你想要更贴近Redux的核心思路(单一数据源、通过action修改状态、订阅变更),可以手动实现一个极简版的状态管理类,所有状态变更都通过dispatch触发,这样更可控,也更容易调试。
示例代码:
class ReduxLikeStore { constructor(initialState) { this.state = { ...initialState }; this.subscribers = []; } // 获取当前状态(返回副本,避免外部直接修改原状态) getState() { return { ...this.state }; } // 订阅变更 subscribe(callback) { this.subscribers.push(callback); return () => { const index = this.subscribers.indexOf(callback); if (index !== -1) this.subscribers.splice(index, 1); }; } // 触发状态变更(类似Redux的dispatch) dispatch(action) { switch (action.type) { case 'UPDATE_ARRAY': this.state.anArray = action.payload; break; case 'PUSH_TO_ARRAY': this.state.anArray.push(action.payload); break; case 'MODIFY_ARRAY_ITEM': this.state.anArray[action.index] = action.payload; break; // 可以根据需求扩展更多action类型 default: return; } // 通知所有订阅者 this.subscribers.forEach(callback => callback(this.getState())); } } // 初始化你的store const store = new ReduxLikeStore({ anArray: ['Hi', 'my', '...'] }); // 订阅变更 const unsubscribe = store.subscribe((newState) => { console.log('Store更新了:', newState); }); // 通过dispatch修改状态 store.dispatch({ type: 'PUSH_TO_ARRAY', payload: 'is' }); store.dispatch({ type: 'MODIFY_ARRAY_ITEM', index: 0, payload: 'Hello' }); // 取消订阅 // unsubscribe();
这种方式的好处是所有状态变更都有迹可循,和Redux的思路完全一致,适合复杂的应用场景。
如果你的对象结构非常固定(比如只有anArray这一个属性),可以直接用ES5的getter和setter来拦截属性的读写操作,实现简单的监听。不过这个方案灵活性差,不适合嵌套对象或动态属性的场景。
示例代码:
const initialState = { anArray: ['Hi', 'my', '...'] }; const subscribers = []; // 创建带getter/setter的store const store = { get anArray() { return [...initialState.anArray]; // 返回副本,避免外部直接修改原数组 }, set anArray(newValue) { initialState.anArray = newValue; // 通知订阅者 subscribers.forEach(callback => callback({ ...store })); } }; // 订阅方法 function subscribe(callback) { subscribers.push(callback); return () => { const index = subscribers.indexOf(callback); if (index !== -1) subscribers.splice(index, 1); }; } // 测试 const unsubscribe = subscribe((newState) => { console.log('Store更新了:', newState); }); store.anArray = [...store.anArray, 'name']; // 触发订阅 // 注意:直接修改返回的副本不会触发setter,必须重新赋值整个数组 // store.anArray[0] = 'Hello'; // 这行不会触发订阅
这个方案适合非常简单的场景,但局限性比较大,比如无法监听数组内部的push操作,必须重新赋值整个数组才会触发setter。
以上三个方案都是当前非废弃、完全可用的,你可以根据自己的场景选择:如果需要最大的灵活性,选Proxy;如果想要贴近Redux的可控性,选第二个方案;如果只是简单固定属性,选第三个。
内容的提问来源于stack exchange,提问作者Eyk Rehbein

