乐趣区

关于前端:如何用redux实现computed计算属性

什么是 computed 计算属性?它会依据所依赖的数据动态显示新的计算结果, 该计算结果会被缓存起来。如果是 Vue 开发者,对这个性能并不生疏,而且很罕用。对于 React 开发者,如果用过 mobx,那其实也不生疏,一个装璜器就失效了🐮。那如果是 Redux 呢??(缄默中。。。)有了,reselect嘛,哈哈😄。啪,骗子,这是 假的 计算属性,它要手动提供全副依赖,每个依赖都是一个函数回调确定依赖值,每次写这么多代码是有多想敲坏我的机械键盘(嘶吼)。

这么说,redux 和计算属性无缘?也不能这么说,方法总比艰难多。尽管 redux 是单向数据流,无奈做响应式操作,不过,咱们能够发明出一个监听对象

import {Store} from 'redux';

const collector = [];

class ObjectDeps {protected readonly deps: string[];
  protected readonly name: string;
  protected readonly store: Store;
  protected snapshot: any;

  constructor(store: Store, name: string, deps: string[] = []) {
    this.store = store;
    this.name = name;
    this.deps = deps;
    collector.push(this);
  }
  
  proxy(currentState) {if (state === null || typeof state != 'object') return state;
    
    const proxyData = Array.isArray(state) : [] : {};
    const currentDeps = this.deps.slice();
    const keys = Object.keys(currentState);
    
    for (let i = keys.length; i-- > 0;) {const key = keys[i]!;

      Object.defineProperty(proxyData, key, {
        enumerable: true,
        get: () => {if (visited) {
            return new ObjectDeps(
              this.store, 
              this.name, 
              currentDeps.slice(),).proxy(currentState)[key];
          }

          visited = true;
          this.deps.push(key);
          return this.proxy((this.snapshot = currentState[key]));
        },
      });
    }
  }
}

朴实无华,没有基于 ES6 的 Proxy,因为兼容性不好。既然是前端的利用,天然是要关照到 ES5 的环境的,因而抉择defineProerty 是个不错的计划。

有了监听驱动,那监听岂不是大海捞针?


// 假如 user 里的对象为:{firstName: 'lady', lastName: 'gaga'}
const userState = store.getState()['user'];

function computedFullName() {const proxy = new ObjectDeps(store, 'user').proxy(userState);
  return proxy.firstName + '-' + proxy.lastName;
}

const fullname = computedFullName();

当初咱们看看 collector 里收集到多少个依赖

console.log(collector); // [ObjectDeps, ObjectDeps]

不错,两条依赖,第一条的 deps 链为['user', 'firstName'],第二条为['user', 'lastName']

原理剖析:

  1. 每次创立 proxy 时,构造函数均会执行 collector.push(this) 向采集器退出本人。
  2. proxy 拜访 firstName 时,其实拜访的是 getter,getter 中有一条 this.deps.push(key) 立刻收集依赖,并返回下一级的 proxy 值。以此类推,即便是 proxy.a.b.c.d 这种深度操作也来者不拒,因为每次拜访下一级都能收集依赖并合并到 deps 数组中。
  3. proxy 拜访 lastName 时,因为 proxy 实例其实曾经被 firstName 占用了(通过 visited 变量判断),所以 getter 逻辑中会间接返回一个 新的 ObjectDeps实例,此时 lastName 曾经和咱们看到的 proxy 变量没有任何关系了。

主动收集依赖曾经实现了,咱们试一下如何缓存属性

class ObjectDeps {
  protected snapshot: any;

  proxy() {...}
  
  isDirty() {return this.snapshot !== this.getSnapshot();
  }
  
  protected getSnapshot() {
    const deps = this.deps;
    let snapshot = this.store.getState();

    for (let i = 0; i < deps.length; ++i) {if (snapshot == null || typeof snapshot !== 'object') {break;}
      snapshot = snapshot[deps[i]!];
    }

    return snapshot;
  }
}

通过 isDirty() 的判断,即再次取得 deps 下的最新值和旧值做比照,便能够晓得这个依赖是否为 脏值。这一步便是缓存的要害。


当初你置信 reselect 是骗子了吧,明明能够主动依赖,非要多写几行代码减少心智累赘?托付,不是每个人都须要 KPI 压力的。

老师,我想间接在我的项目中应用上这个什么 computed 属性,应该去哪里找现成的呢?废话,当然是去山东找蓝翔。看看蓝翔大法:

import {defineModel, useComputed} from 'foca';

export const userModel = defineModel('user', {
  initialState: {
    firstName: 'lady',
    lastName: 'gaga',
  },
  computed: {
    // 清新
    fullName() {return this.state.firstName + '-' + this.state.lastName;},
  },
});

// App.tsx
const App: FC = () => {const fullName = useComputed(userModel.fullName);
  
  return <div>{fullName}</div>;
};

嗯?刚刚产生了什么,如同看到 dva 飞过去?飞你个头,是哥写的 React 状态治理库foca,基于 redux 和 react-redux,方才的 computed 解析就是从外面摘抄的(具体实现逻辑请看这里)。尽管是个软广告,不过redux 也算是反对 computed 了,各位大佬就不要天天喷 redux 这个不好那个不好了行吧😠,二次封装才是真爱。

人生苦短,手握神器,少写代码,早点上班最要紧:https://github.com/foca-js/foca

退出移动版