Some time ago, I asked React if there were any good learning materials. Yong Ge recommended Reselect to me and asked me to read the source code and teach him =_=. I’ll go to Github and see what it is. It’s a memory function that can be used in conjunction with Redux. Although React Hook can now use useMemo for caching, Reselect is much more powerful and can be used in a wide range of scenarios. The source code is just over 100 lines long and serves as an example of functional programming.

introduce

A quick introduction to Reselect:

Selectors can compute derived data, allowing Redux to store the minimal possible state.

Selectors are efficient. A selector is not recomputed unless one of its arguments changes.

Selectors are composable. They can be used as input to other selectors.

This is the opening paragraph of the Github project description. In short, it can cache data, it can be used in conjunction with Redux, and it can be recomputed only when dependencies change, and it can be combined, all features of functional programming.

Here’s an example of how it works:

import { createSelector } from 'reselect'

const shopItemsSelector = state= > state.shop.items
const taxPercentSelector = state= > state.shop.taxPercent

const subtotalSelector = createSelector(
  shopItemsSelector,
  items= > items.reduce((subtotal, item) = > subtotal + item.value, 0))const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) = > subtotal * (taxPercent / 100))const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) = > ({ total: subtotal + tax })
)

const exampleState = {
  shop: {
    taxPercent: 8.items: [{name: 'apple'.value: 1.20 },
      { name: 'orange'.value: 0.95}}},]console.log(subtotalSelector(exampleState)) / / 2.15
console.log(taxSelector(exampleState))      / / 0.172
console.log(totalSelector(exampleState))    // {total: 2.322}
Copy the code

The example is clear enough that it needs no explanation.

The source code parsing

Go to Github and download the source code from SRC/index.js, over 100 lines. We went to createSelector first and found:

export const createSelector = /* #__PURE__ */ createSelectorCreator(defaultMemoize)
Copy the code

Before moving on to the source code, take a look at the API documentation createSelectorCreator for what it does:

createSelectorCreator(memoize, … memoizeOptions)

createSelectorCreator can be used to make a customized version of createSelector.

The memoize argument is a memoization function to replace defaultMemoize.

The … memoizeOptions rest parameters are zero or more configuration options to be passed to memoizeFunc.

This is a factory function that allows you to create a custom createSelector. Its first parameter is the memory function, which by default is defaultMemoize, followed by the parameter memoizeOptions which is passed to Memoize. After reading the source code we can see.

export function createSelectorCreator(memoize, ... memoizeOptions) {
  return (. funcs) = > {
    let recomputations = 0
    const resultFunc = funcs.pop()
    const dependencies = getDependencies(funcs)

    const memoizedResultFunc = memoize(
      function () {
        recomputations++
        // apply arguments instead of spreading for performance.
        return resultFunc.apply(null.arguments)},... memoizeOptions )// If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
    const selector = memoize(function () {
      const params = []
      const length = dependencies.length

      for (let i = 0; i < length; i++) {
        // apply arguments instead of spreading and mutate a local list of params for performance.
        params.push(dependencies[i].apply(null.arguments))}// apply arguments instead of spreading for performance.
      return memoizedResultFunc.apply(null, params)
    })

    selector.resultFunc = resultFunc
    selector.dependencies = dependencies
    selector.recomputations = () = > recomputations
    selector.resetRecomputations = () = > recomputations = 0
    return selector
  }
}
Copy the code
Copy the code

The first thing we know is that it returns a createSelector function, which in turn returns a selector. Let’s look at the code. Recomputations record the number of operations. ResultFunc receives the last parameter, which is essentially a reducer to do the work. If you understand the difficulty, look at the last parameter of createSelector. Const dependencies = getDependencies(funcs)

function getDependencies(funcs) {
  const dependencies = Array.isArray(funcs[0])? funcs[0] : funcs

  if(! dependencies.every(dep= > typeof dep === 'function')) {
    const dependencyTypes = dependencies.map(
      dep= > typeof dep
    ).join(', ')
    throw new Error(
      'Selector creators expect all input-selectors to be functions, ' +
      `instead received the following types: [${dependencyTypes}] `)}return dependencies
}
Copy the code

It accepts a series of functions, starting by checking whether the first element of funcs is an array. What does that mean? Take a look at the API documentation:

createSelector(... inputSelectors | [inputSelectors], resultFunc)Copy the code

It may receive an inputSelector as an array, while createSelectorCreator returns a return (… Funcs), so if passed to the createSelector is an array, naturally the first element is an array, that is, [inputSelectors]. Then check to see if all elements are functions. If not, an error is reported.

const memoizedResultFunc = memoize(
  function () {
    recomputations++
    // apply arguments instead of spreading for performance.
    return resultFunc.apply(null.arguments)},... memoizeOptions )Copy the code

Call memoize, passing in an anonymous function. Since we passed in defaultMemoize earlier, we’ll look at it:

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // we reference arguments instead of spreading them for performance reasons
  return function () {
    if(! areArgumentsShallowlyEqual(equalityCheck, lastArgs,arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null.arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}
Copy the code

This is the main logic of ResELECT: the cache, which depends on whether the data has changed and calls the passed function if it has. EqualityCheck is the comparison function, defaultEqualityCheck by default:

function defaultEqualityCheck(a, b) {
  return a === b
}
Copy the code

DefaultMemoize return a function, the first to use areArgumentsShallowlyEqual for parameters of shallow comparison:

function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
  if (prev === null || next === null|| prev.length ! == next.length) {return false
  }

  // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
  const length = prev.length
  for (let i = 0; i < length; i++) {
    if(! equalityCheck(prev[i], next[i])) {return false}}return true
}
Copy the code

The code compares the parameters passed in with the previously cached parameters. So the question is what was the cached parameter before? You’ll see.

From this we can infer that if we want to call createSelectorCreator, the memory function also needs to be similar to defaultMemoize: a function that must return a closure, needs to cache, needs to call comparison methods, and calls arithmetic functions.

const selector = memoize(function () {
  const params = []
  const length = dependencies.length

  for (let i = 0; i < length; i++) {
    // apply arguments instead of spreading and mutate a local list of params for performance.
    params.push(dependencies[i].apply(null.arguments))}// apply arguments instead of spreading for performance.
  return memoizedResultFunc.apply(null, params)
})

selector.resultFunc = resultFunc
selector.dependencies = dependencies
selector.recomputations = () = > recomputations
selector.resetRecomputations = () = > recomputations = 0
return selector
Copy the code

Call the memory function again to create the selector to return, but without passing memoizeOptions. Focus on the function passed in, which iterates over the call dependencies to retrieve the store property. When was store passed in? Let’s look at defaultMemoize again:

  return function () {
if(! areArgumentsShallowlyEqual(equalityCheck, lastArgs,arguments)) {
  // apply arguments instead of spreading for performance.
  lastResult = func.apply(null.arguments) / / note
}
Copy the code

That’s where the incoming comes in. If you find it a bit messy, look at defaultMemoize again. And then we pass it to the operator. There are two memory functions that are used here, selector is used to cache dependent functions, see if the function has changed, and memoizedResultFunc is used to cache if the data that was pulled out of the Store has changed.

Source code basic analysis. What inspired me was the use of factory functions to separate user-defined logos from functions’ own logos, making them more abstract and reusable.