reselect源码阅读

2020-01-14 17:20:17 浏览数 (1)

reselect源码阅读

之前就听闻了reselect是一个用于react性能优化的库,并且源码只有100多行。可谓短小精悍,今天来阅读一波膜拜大佬们的思想

代码语言:javascript复制
import { createSelector } from 'reselect'

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

const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc   item.value, 0)
)

const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

export const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal   tax })
)

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

官网demo如上,通过介绍可以知道,subtotalSelector taxSelector totalSelector在传进去的state不变的情况下,第二次调用不会重新计算,而是会取前一次的计算结果。在涉及到大量运算的时候,例如redux中,可以避免全局state某一小部分改变而引起这边根据小部分state进行计算的重新执行。起到性能优化的作用。下面开始阅读探读部分

先说几个简单的工具函数吧

首先是默认的比较函数,代表比较方式,可以根据业务需求换的。默认是进行全等比较

代码语言:javascript复制
/**
 * 默认的比较函数,只进行一层全等比较。根据业务需要可以自己定制。
 * @param {*} a 
 * @param {*} b 
 */
function defaultEqualityCheck(a, b) {
    return a === b
  }

比较两个参数是否完全相等的库…见注释。记住不要为了装逼而弃用for循环

代码语言:javascript复制
  /**
   * 比较前后两次的参数是否完全相等
   * @param {*} equalityCheck 比较规则,默认是使用defaultEqualityCheck全等比较
   * @param {*} prev 前一次的参数
   * @param {*} next 这次的参数
   */
  function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
    if (prev === null || next === null || prev.length !== next.length) {
      return false
    }
    // 学到了。使用for循环是因为return后不会继续for后面的,forEach和every是会继续的,所以以后不要为了装逼而抛弃for循环了
    // 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  ) {
        // 只要遇到一个参数不想等就退出,判断返回false
      if (!equalityCheck(prev[i], next[i])) {
        return false
      }
    }

    return true
  }

获取依赖,就是保证inputSelectors的每一项都是函数。否则就报错

代码语言:javascript复制
  /**
   * 用来保证inputSelectors的每一项都是函数,非函数就报错。然后返回每一个inputSelector组成的数组(依赖)
   * @param {*} funcs  
   */
  function getDependencies(funcs) {
      // 因为可以两种形式传入,所以处理下
      // createSelector(...inputSelectors | [inputSelectors], resultFunc)
    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
  }

下面是默认记忆化函数defaultMemoize

就是说有个func函数,每次去调用它的时候,先去比较它的参数是否和上一次参数相同,相等就返回上一次结果,不等就重新计算一遍, 并把结果和参数存到lastArgs、lastResult中,这里是用到了闭包来保存的。

代码语言:javascript复制
  /**
   * 默认的记忆化函数
   * @param {*} func 
   * @param {*} equalityCheck 比较的函数,默认情况下是判断是否全等
   */
  export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
    let lastArgs = null
    let lastResult = null
    // 这里为了性能原因。使用arguments而不是rest运算符
    // we reference arguments instead of spreading them for performance reasons
    return function () {
      if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
        // 参数改变了重新计算一遍
        lastResult = func.apply(null, arguments)
      }

      lastArgs = arguments // 记录下这次的参数值
      return lastResult
    }
  }

demo中引入的createSelector其实是export const createSelector = createSelectorCreator(defaultMemoize)这样创建的

下面看看createSelectorCreator的实现

1、memoize是自定义的记忆化函数,默认是上面说到的defaultMemoize。我们可以根据自己的业务需求进行定制

2、它的内部也使用了defaultMemoize进行优化。就是对于相同的state,不需要每一次都一次调用inputSelect去一个个获取对应的值

代码语言:javascript复制
  /**
   * 
   * @param {*} memoize 
   * @param {*} memoizeOptions 
   */
  export function createSelectorCreator(memoize, ...memoizeOptions) {
    return (...funcs) => { // createSelector的本体
      let recomputations = 0
      const resultFunc = funcs.pop() // 最后一个。。就是那个计算函数
      const dependencies = getDependencies(funcs)// funs是inputSelectors

      // resultFunc经过包装。具备了记忆功能
      const memoizedResultFunc = memoize(
        function () {
            // 这里的arguments是每一项是inputSelector后的结果
            // 把inputSelector后的结果传进resultFunc
            // recomputations 用了记录调用了多少次
          recomputations  
          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.
      // reselect内部的一个优化
      // 作用就是对于相同的state,不需要每一次都一次调用inputSelect去一个个获取对应的值
      const selector = defaultMemoize(function () {
          // arguments应该是state
        const params = []
        const length = dependencies.length

        for (let i = 0; i < length; i  ) {
          // 对每一个inputSelect结合state取出相应的值
          params.push(dependencies[i].apply(null, arguments))
        }

        //最后供memoizedResultFunc计算使用Ss s
        return memoizedResultFunc.apply(null, params)
      })

      // 对外提供的几个方法

      // 返回那个函数
      selector.resultFunc = resultFunc

      //返回计算次数
      selector.recomputations = () => recomputations

      // 用来重置
      selector.resetRecomputations = () => recomputations = 0
      return selector
    }
  }

reselect这个库还是很吊的…主要是利用了必=闭包来保存变量,比较函数前后两次的参数值,参数值不同就重新计算。参数相同就返回之前的结果

0 人点赞