1

What is a computed computed property? It dynamically displays new calculation results based on the data it depends on, and the calculation results are cached. If you are a Vue developer, you are no stranger to this feature, and it is very commonly used. For React developers, if you have used mobx, it is not unfamiliar, a decorator will take effect 🐮. What if it was Redux? ? (In silence...) Yes, reselect Well, haha 😄. Slap, liar, this is a fake computed property, it needs to provide all dependencies manually, each dependency is a function callback to determine the dependency value, how much I want to break my mechanical keyboard (roar) every time I write so much code.

So, redux has nothing to do with computed properties? It cannot be said that the solution is always more difficult than the difficulty. Although redux is a one-way data flow and cannot do responsive operations, we can create a listener object

 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]));
        },
      });
    }
  }
}

Unpretentious, there is no ES6-based Proxy because the compatibility is not good. Since it is a front-end application, it is natural to take care of the ES5 environment, so choosing defineProerty is a good solution.

With the monitor driver, wouldn't monitoring be as easy as the palm of your hand?

 // 假设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();

Now let's see how many dependencies are collected in collector

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

Yes, two dependencies, the first deps chain is ['user', 'firstName'] and the second is ['user', 'lastName'] .

Principle analysis:

  1. Every time a proxy is created, the constructor executes collector.push(this) to add itself to the collector.
  2. When the proxy accesses firstName , it actually accesses the getter. There is one in the getter this.deps.push(key) to collect dependencies immediately and return the proxy value of the next level. And so on, even proxy.a.b.c.d this kind of deep operation is not rejected, because every visit to the next level can collect dependencies and merge them into the deps array.
  3. When the proxy accesses lastName , since the proxy instance is actually occupied by firstName (judged by the visited variable), the getter logic will directly return a 新的ObjectDeps instance. At this time, the lastName has not been matched with the proxy variable we saw. any relationship.

Automatic collection of dependencies has been implemented, let's try how to cache properties

 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;
  }
}

Through the judgment of isDirty() , that is, by comparing the latest value under deps with the old value again, you can know whether this dependency is 脏值 . This step is the key to caching.


Now do you believe that reselect is a liar, and it can be automatically relied on, so you have to write a few more lines of code to increase the mental burden? Please, not everyone needs KPI pressure.

Teacher, I want to use this computed property directly in the project, where should I find the ready-made one? Nonsense, of course, I went to Shandong to find Lan Xiang. Check out Lanxiang Dafa:

 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>;
};

Ok? What just happened, seems to see dva flying past? Flying your head is the React state management library written by my brother foca , based on redux and react-redux, the computed analysis just now is excerpted from it ( see here for the specific implementation logic). Although it's a soft advertisement, but redux也算是支持computed了 , you guys, don't spray redux every day, it's not good, that's not good, okay 😠, the second package is true love.

Life is too short, it is most important to have an artifact in hand, write less code, and get off work early: https://github.com/foca-js/foca


夜葬
3.6k 声望1.2k 粉丝

极客