You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化JavaScript状态管理中JSON路径订阅的监听效率?

优化状态管理库的订阅回调触发效率

我开发了一个简易状态管理库,包含基础store对象,希望仅在store的特定JSON路径发生变更时触发回调。比如订阅路径$subjects.maths.passingMarks,执行store.setState({subjects: {science: {passingMarks: 60}}})时不触发回调,执行store.setState({subjects: {maths: {passingMarks: 72}}})时才触发回调。

当前实现的publish方法会遍历全部回调列表,通过JSONPath获取并对比前后状态对应路径的值来触发回调,但当存在大量订阅时,单次状态变更需要遍历所有回调,效率很低,想找减少迭代或者更高效的实现方案。

初始状态与回调示例

const store = {
  subjects: {science: {passingMarks: 50}, maths: {passingMarks: 50}}
}

const callback = (state) => {
  console.log('callback :: '+state);
}

状态变更示例

// 此操作不应触发订阅$subjects.maths.passingMarks的回调
store.setState({subjects: {science: {passingMarks: 60}}});
// 此操作应触发订阅$subjects.maths.passingMarks的回调
store.setState({subjects: {maths: {passingMarks: 72}}});

当前publish实现代码

const isObject = (val) =>
  val != null && typeof val === "object" && Array.isArray(val) === false;

const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);

publish(currentState, nextState) {
    if (!isObject(currentState))
      throw new Error("currentState should be an object");
    if (!isObject(nextState)) throw new Error("nextState should be an object");
    for (var key in this.callbackList) {
      const currentValue = JSONPath({ path: key, json: currentState });
      const nextValue = JSONPath({ path: key, json: nextState });
      if (!isEqual(currentValue, nextValue)) {
        const callbacks = this.callbackList[key];
        callbacks.forEach((cl) => {
          cl(nextState);
        });
      }
    }
  }

优化方案

1. 先定位变更路径,再匹配订阅

不用遍历所有订阅,先找出本次状态变更涉及的所有具体路径,再匹配对应的订阅回调,减少无效迭代:

// 递归找出新旧对象间所有变更的路径
function getChangedPaths(oldObj, newObj, parentPath = '') {
  const changedPaths = [];
  // 处理新对象的键
  for (const key of Object.keys(newObj)) {
    const currentPath = parentPath ? `${parentPath}.${key}` : key;
    const oldVal = oldObj[key];
    const newVal = newObj[key];
    if (isObject(oldVal) && isObject(newVal)) {
      changedPaths.push(...getChangedPaths(oldVal, newVal, currentPath));
    } else if (!deepEqual(oldVal, newVal)) {
      changedPaths.push(currentPath);
    }
  }
  // 处理旧对象有但新对象没有的键
  for (const key of Object.keys(oldObj)) {
    if (!(key in newObj)) {
      const currentPath = parentPath ? `${parentPath}.${key}` : key;
      changedPaths.push(currentPath);
    }
  }
  return changedPaths;
}

// 替换原isEqual的深度对比函数
function deepEqual(a, b) {
  if (a === b) return true;
  if (a == null || b == null) return false;
  if (typeof a !== 'object' || typeof b !== 'object') return false;
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);
  if (keysA.length !== keysB.length) return false;
  for (const key of keysA) {
    if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
  }
  return true;
}

// 优化后的publish方法
publish(currentState, nextState) {
  if (!isObject(currentState)) throw new Error("currentState should be an object");
  if (!isObject(nextState)) throw new Error("nextState should be an object");
  
  // 获取所有变更路径并转为JSONPath格式
  const changedPaths = getChangedPaths(currentState, nextState);
  const jsonChangedPaths = changedPaths.map(path => `$.${path}`);
  
  // 只遍历与变更路径相关的订阅
  for (const subPath of Object.keys(this.callbackList)) {
    const shouldTrigger = jsonChangedPaths.some(changedPath => {
      // 匹配逻辑:变更路径是订阅路径的子路径,或订阅路径是变更路径的父路径
      return changedPath.startsWith(subPath) || subPath.startsWith(changedPath);
    });
    if (shouldTrigger) {
      // 二次确认值是否真的变化,避免路径匹配误判
      const currentValue = getValueByPath(currentState, subPath);
      const nextValue = getValueByPath(nextState, subPath);
      if (!deepEqual(currentValue, nextValue)) {
        this.callbackList[subPath].forEach(cl => cl(nextState));
      }
    }
  }
}

// 轻量路径取值函数,替代JSONPath提升性能
function getValueByPath(obj, path) {
  const keys = path.replace(/^\$./, '').split('.');
  let current = obj;
  for (const key of keys) {
    if (!current || typeof current !== 'object') return undefined;
    current = current[key];
  }
  return current;
}

2. 树形结构存储订阅路径

将订阅路径按层级拆分为树形结构,比如:

// 订阅存储示例
this.subTree = {
  "$": {
    "subjects": {
      "maths": {
        "passingMarks": [callback1]
      },
      "science": {
        "passingMarks": [callback2]
      }
    }
  }
}

当变更路径是$.subjects.maths.passingMarks时,可直接遍历树形结构定位到对应的回调,无需遍历所有订阅项,同时还能方便处理父路径变更触发子路径回调的场景。

3. 移除JSONPath依赖

JSONPath的解析和查询有额外性能开销,对于点分隔的简单路径,用上面的getValueByPath这类轻量函数替代,能显著提升取值速度。

内容的提问来源于stack exchange,提问作者Radhika Singal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 08:31:02