Reselect源码解析

591 阅读4分钟

前段时间我在群里问React有什么好的学习资料,群里大神勇哥推介 Reselect,叫我把源码读一遍,然后教他=_=。 我去github上看一下是什么玩意,看介绍知道是一个记忆函数,可以结合Redux使用。儘管现在React Hook可以使用 useMemo进行缓存,但 Reselect更加强大,使用场景更广。源码只有100多行,适合用来作为函数式编程的范例。

介绍

先简单介绍下 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.

这是Github项目说明开首的一段话。简单来说,它可以缓存数据,可以结合Redux使用,只有当依赖数据发生变化才重新运算,还有它可以组合,都是函数式编程的特色。

用一个示例说明它是怎样使用:

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 }

示例已经很清晰,不用解释。

源码解析

去Github把源码下载下来,源码在src/index.js,一百多行。我们先去找createSelector,然后发现:

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

继续看源码之前,看下api文档createSelectorCreator是做什么的:

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.

它是一个工厂函数,可以自定义一个createSelector,它的第一个参数是记忆函数,默认情况下是defaultMemoize,之后的参数 memoizeOptions用来传给memoize。之后读源码我们就能看到。

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

首先我们知道是返回一个createSelector函数,而这个函数又返回一个selector。接下来看具体代码。recomputations用来记录运算次数。resultFunc接收最后一个参数,它其实就是一个reducer,用来运算,如果了解困难,去看一下createSelector的最后一个参数。const dependencies = getDependencies(funcs)调用getDependencies方法:

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
}

它接收一系列函数,先查看funcs的第一个元素是否是数组。这是什么意思?看一下api文档就明白:

createSelector(...inputSelectors | [inputSelectors], resultFunc)

它接收的inputSelector可能是数组,而createSelectorCreator返回的是return (...funcs),因此如果传入给createSelector是数组,自然第一个元素是数组,即[inputSelectors]。之后检查是否所有元素都是函数,不是的话就报错。

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

调用记忆函数memoize,传入一个匿名函数。因为我们之前传入的是defaultMemoize,所以我们要看下它:

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

简单看下就知道这是reselect的主要逻缉:缓存,比较依赖数据是否发生变化,有的话调用传入的函数运算。equalityCheck就是比较的函数,默认是defaultEqualityCheck:

function defaultEqualityCheck(a, b) {
  return a === b
}

defaultMemoize返回一个函数,首先使用areArgumentsShallowlyEqual作参数的浅层比较:

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
}

看代码知道是比较传入的参数和之前缓存的参数。问题是到底之前缓存的参数是什么?之后就知道了。

由此我们可以推断如果我们想调用createSelectorCreator,记忆函数也要类似defaultMemoize:必须返回一个闭包的函数,需要缓存,需要调用比较方法和调用运算函数。

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

再次调用记忆函数来创建要返回的selector,但不传memoizeOptions。重点看传入的函数,它会遍历调用dependencies来取出store的属性。store是什么时候传入?不妨再看一次defaultMemoize:

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

就是这时传入。如果觉得有点乱,再看一次defaultMemoize。最后传入给运算函数。这里总共用了两次记忆函数,selector是用来缓存依赖函数,查看函数有没有发生变化,memoizedResultFunc是用来缓存store取出的数据有没有发生变化。

源码基本解析完。要说对我有什么启发就是利用工厂函数把用户自定义逻缉与函数自己的逻缉分离,从而更加抽象,複用性更强。