1

reselect

version: 4.0.0

描述

reselect是能够缓存函数运行结果的一个缓存函数生成器,其接收一个函数返回一个新的具有缓存能力的函数

用途

  1. 用来缓存需要大量计算的结果
  2. 用来缓存频繁的重复运算的结果

解析

核心缓存函数,实现很简单
主要是把新旧参数是否不变的判断规则交给了equalityCheck

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

默认的比较函数只是定义了一个简单的规则,如果要深度对比对象或者数组,需要自行实现

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

在判断新旧参数是否相同的过程中,需要对旧的参数进行遍历,将equalityCheck规则应用到每个参数的对比上

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
}

至此就已经获得了一个缓存函数生成器defaultMemoize,用法类似这样

function add(a, b) {
    return a + b
}
let memoryAdd = defaultMemoize(add)
memoryAdd(1, 2)
// 直接返回结果
memoryAdd(1, 2)

但是reselect进行了进一步的扩展,定义了一个创建缓存函数的工厂,返回一个使用memoize来缓存结果的函数,而且该函数对传入的参数做了处理

export function createSelectorCreator(memoize, ...memoizeOptions) {
  // 返回一个函数,该函数接收多个函数并最终返回selector
  return (...funcs) => {
    let recomputations = 0
    // 最后一个函数用来计算结果
    const resultFunc = funcs.pop()
    // 除最后一个外的函数,都将作为参数传递给resultFunc
    const dependencies = getDependencies(funcs)
    // resultFunc的缓存函数
    const memoizedResultFunc = memoize(
      function () {
        // 用来统计resultFunc真正被调用的次数
        recomputations++
        // apply arguments instead of spreading for performance.
        return resultFunc.apply(null, arguments)
      },
      // 传递给memory,对于defaultMemoize来说可用来覆盖默认的defaultEqualityCheck比较规则
      ...memoizeOptions
    )

    // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
    // selector是一个memoize生成的缓存函数,如果每次调用的参数相同则会直接返回结果
    const selector = memoize(function () {
      const params = []
      const length = dependencies.length

      // 当调用selector时将arguments传入每个dependencie,并将运行结果push进params里,然后传给resultFunc计算最终结果
      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.
      // 这里并没有直接调用resultFunc,而是调用对resultFunc进一步缓存得到的memoizedResultFunc,
      // 也就是说如果selector调用时的参数arguments发生了改变,但是dependencies计算后的结果前后参数依然相等,则直接返回结果,
      // 这里主要是处理arguments是引用类型时,可能基于数据不可变原则,arguments可能只是引用发生了改变,其内部的values并未发生改变,那么就可以直接返回ResultFunc的运行结果
      return memoizedResultFunc.apply(null, params)
    })

    // 获得最终计算函数
    selector.resultFunc = resultFunc
    // 获得参数函数列表
    selector.dependencies = dependencies
    // 获取实时的计算次数
    selector.recomputations = () => recomputations
    // 重置recomputations
    selector.resetRecomputations = () => recomputations = 0
    return selector
  }
}

所以createSelectorCreator的使用方式应该如下

export const createSelector = createSelectorCreator(defaultMemoize)
// createSelector返回的就是上面的selector
let memoryFunc = createSelector(
    user => user.name,
    user => user.age,
    user => user.gender,
    (name, age, gender) => [name, age, gender].join('')
)
// memoryFunc接收的参数会被传递给每个前置函数,比如user,也可以传递多个参数进去
let user = {
    name: 'a',
    age: 22,
    type: 'male'
}
memoryFunc(user)
// 在react中此处的user就是redux的state

如果默认的===比较不满足需求,则可以自己实现一个比较函数并创建自己的createSelector函数

function customEqualityCheck(a, b) { // doSomething }
const createCustomSelector = createSelectorCreator(defaultMemoize, customEqualityCheck)

上面还有个getDependencies的函数主要是做参数处理

function getDependencies(funcs) {
  // 从这里可以看出依赖项也可以放进一个数组中
  const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs
  // 如果dependencies中存在不是函数的参数,则抛出异常
  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
}

此外reselect还存在createStructuredSelector,是一个基于createSelector进一步封装用来结构化输出的函数,接受一个自定义结构的对象obj,其每个key对应的value都必须是个函数,返回结果是一个和obj具有相同key的对象,其每个key对应的值为obj上相应的key的value函数执行结果。

export function createStructuredSelector(selectors, selectorCreator = createSelector) {
  if (typeof selectors !== 'object') {
    throw new Error(
      'createStructuredSelector expects first argument to be an object ' +
      `where each property is a selector, instead received a ${typeof selectors}`
    )
  }
  const objectKeys = Object.keys(selectors)
  return selectorCreator(
    // selector是个对象,selectorCreator接收的参数全部为函数,或者一个函数数组加一个函数,这里将selector的values放入数组中作为依赖参数
    objectKeys.map(key => selectors[key]),
    (...values) => {
      // composition贯穿整个reduce遍历
      return values.reduce((composition, value, index) => {
        // 读取的objectKeys[index]也就是key值,value是依赖函数的执行结果
        composition[objectKeys[index]] = value
        return composition
      }, {})
    }
  )
}

createStructuredSelector是相当于省略了最后一个函数,但是如果需要在最后一个函数中处理额外的逻辑,就不满足需求了,应该用createSelector

let selector = createStructuredSelector({
    name: user => user.firstName + user.lastName,
    age: user => user.age,
    description: user => user.details.join(' '),
})
selector({
    firstName: 'redux',
    lastName: 'reselect',
    age: 0,
    details: [ 'oneoneone' ]
})
// 输出结构
=> {
    name: 'reduxreselect',
    age: 0,
    description: 'oneoneone',
}
// 上面这段代码用createSelector来写是这样的
let selector = createSelector({
    name: user => user.firstName + user.secondName,
    age: user => user.age,
    description: user => user.details.join(' '),
    (name, age, description) => { name, age, description }
})

理解

  1. createSelectorCreator出现的目的是什么

在createSelectorCreator中主要的功能是定制参数和再次缓存resultFunc,如果按照只是需要一个能缓存值的函数defaultMemoize已经达到了需求,而createSelectorCreator中能够接受多个函数,前面所有函数作为最后一个函数的参数,
为何这样设计
如果是用defaultMemoize,代码可能如下

const state = {}
let memoriedFuncA = defaultMemoize(function(state) {
    // get taskC
    // todo taskA
    return ''
})
let memoriedFuncB = defaultMemoize(function(state) {
    // get taskC
    // todo taskB
    return ''
})
// 上面的代码很明显可以将公共模块分离出来
let memoriedFuncC = defaultMemoize(function(state) {
    // todo taskC
    return ''
})
let memoriedFuncA = defaultMemoize(function(state) {
    let resultc = memoriedFuncC(state)
    // todo taskA
    return ''
})
let memoriedFuncB = defaultMemoize(function(state) {
    let resultc = memoriedFuncC(state)
    // todo taskB
    return ''
})

但是这样memoriedFuncC是硬编码到memoriedFuncA、memoriedFuncB中,考虑耦合问题可以将memoriedFuncC作为参数传入,而memoriedFuncC本身是个函数,并且可能存在多个依赖,自然就想到传入多个函数

let memoriedFuncB = defaultMemoize(function(state, taskC) {
    let resultc = taskC(state)
    // todo taskB
    return ''
})
// createSelectorCreator对参数进行了整理
let memoriedFuncC = createSelector(
    state => 'c'
)
let memoriedFuncA = createSelector(
    memoriedFuncC,
    c => 'a'
)
let memoriedFuncB = createSelector(
    memoriedFuncC,
    c => 'b'
)

总结

reselect主要是实现函数缓存运行结果的功能,其代码主要分为以下几块

  1. 缓存函数defaultMemoize,判断前后参数是否相等来决定执行还是直接返回结果
  2. 分离出前后参数对比的逻辑areArgumentsShallowlyEqual
  3. 分理出判断是否相等的逻辑defaultEqualityCheck
  4. 定义工厂createSelectorCreator,定制了参数格式也就是调用方式
  5. 基于createSelector对结构化输出进行了封装(createStructuredSelector)

broken
3 声望0 粉丝

to be creator