Reselect 为什么可以优化 React 项目性能?

·  阅读 2931
Reselect 为什么可以优化 React 项目性能?

我正在参加「掘金·启航计划」。

Hi,请问有没有听说过 reselect ?

没接触过

Reselect 是一个用于创建记忆的“selector”函数的库。

通常与 Redux 一起使用,但也适用于任何普通的 JS 不可变数据场景

  • selector 可以计算衍生数据,它允许让 Redux 存储尽可能少的 state
  • selector 很高效,它只有在某个参数发生变化时才会进行重新计算
  • selector 是可组合的,它可以作为其他 selector 的入参

以上是来自官方的介绍,个人简单理解:

我们可以用它包装数据(如 Redux 的 state),并利用其缓存入参的能力减少不必要的更新,从而达到性能优化,一举多得。

来看个简单的用例感受一下魅力。

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 }
复制代码

用过而已

如果你正在使用 reselect,并还没去深入了解过,建议打开源码(v4.0.0 非 ts 版本一共 108 行)学习一番,因为它真的小而美。

开始源码阅读之前,我们先了解两个核心工具方法。

defaultEqualityCheck

全等比较 a b 两个参数。

function defaultEqualityCheck(a, b) {
    return a === b
}
复制代码

(偷偷告诉你,这是源码第一行,真美!)

areArgumentsShallowlyEqual

主要目的是比较 prev 与 next 两组参数的差异。

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
}
复制代码

如上代码片段,它遍历各元素,借助 equalityCheck 进行比对,一旦不相等就直接退出。


一般我们只会用到 reselect 的 createSelector 方法,那就从它开始好了。

createSelector

学习之前,我们先看看它的函数签名

createSelector(...inputSelectors | [inputSelectors], resultFunc)
复制代码

它接受若干个 selector 或一个 selector 数组以及 resultFunc (最后一个)作为参数,其中resultFunc 接收前面 selector 计算出来的结果作为入参进行加工,并得到期望结果。


下面继续

export const createSelector = createSelectorCreator(defaultMemoize)
复制代码

就一行代码:使用 defaultMemoize 作为 createSelectorCreator 的参数,并将结果导出。

看样子我们得去看看 createSelectorCreator 了。

createSelectorCreator

createSelectorCreator 接收若干个参数,返回一个接收若干个以函数为参数的方法,即 selector 。

export function createSelectorCreator(memoize, ...memoizeOptions) {
  return (...funcs) => {
    // 重新计算的次数
    let recomputations = 0
    // 使用时传入的最后一个参数
    const resultFunc = funcs.pop()
    /*
        pop 最后一个参数后,前面参数的都是依赖,可参考 createSelector 的函数签名
        如果传入的依赖不全是函数,将会抛出错误
    */
    const dependencies = getDependencies(funcs)

    /*
        根据上文函数签名,resultFunc 接收其他 selector 参数的计算结果作为参数
        并使用记忆函数缓存入参,使用 recomputations 统计重新计算次数
    */
    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

      // 一一计算依赖 selector 的结果,存入 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.
      return memoizedResultFunc.apply(null, params)
    })
	
    // selector 上的其他属性可以在开发单测时使用
    selector.resultFunc = resultFunc
    selector.dependencies = dependencies
    selector.recomputations = () => recomputations
    selector.resetRecomputations = () => recomputations = 0
    return selector
  }
}
复制代码

看完以上代码,带着疑问来看看 memoize 是何方神圣

defaultMemoize

createSelector 中使用的是默认的 defaultMemoize

// func 即要调用的函数,equalityCheck 默认使用提到的全等判断 defaultEqualityCheck
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  // 前一次参数
  let lastArgs = null
  // 前一次执行结果
  let lastResult = null

  // 借助闭包缓存前一次执行的参数与结果,仍返回一个函数
  return function () {
    // 如果前后两次参数不一样,则执行 func,否则返回之前的执行结果 lastResult
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null, arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}
复制代码

defaultMemoizereselect 的核心方法,它借助闭包保存前一次参数与结果,通过比对前后参数的差异来决定是否需要执行原 func,达到记忆函数的目的。

自定义

如果觉得 createSelector 不能满足你的性能追求,reselect 完全支持用户通过 createSelectorCreator 使用自定义的 memoizeequalityCheck

详细的使用在官方文档中也有提到,不再展开。


以上就是对 reselect createSelector 的完整链路源码分析,在这份“小而美”的源码中并没有太多出奇的地方,现在回到文章题目:Reselect 为什么可以优化 React 项目性能?

我想认真看完以上分析的各位应该都能回答一二。

reselect 使用闭包保存上一次的参数 lastArgs 与结果 lastResult ,只有当依赖中的某个 Redux state 发生了变化,导致前后参数比对不一致了,才会触发 selector 的再次计算。这避免了 react 组件的不必要的更新,从而达到了性能优化的效果。

还能更优吗?

但还没完,我们会发现真正在项目使用过程中,往往会有需要使用变量查询型的 selector,如:

const conversationLastMessageSelector = (conversationId: string) => createSelector(
  getMessageSelector(conversationId),
	entitySelector,
  (messages, entity) => {
    // ...
  }
)
复制代码

通过上诉分析,我们知道对于 conversationLastMessageSelector 这个 selector,仅仅只会缓存输入的 conversationId 与上一次相同的结果,对于实际列表使用场景来说,缓存将不复存在。

学习 Faster JavaScript Memoization For Improved Application Performance,我们得到高效而又简单的缓存函数

function memoize (f) {
  return function () {
    const args = Array.prototype.slice.call(arguments)

    f.memoize = f.memoize || {}

    return args in f.memoize
      ? f.memoize[args]
      : (f.memoize[args] = f.apply(this, args))
  }
}
复制代码

不难看出,我们可以使用高阶缓存函数 memoize 包裹 conversationLastMessageSelector,它将实现对每个 conversationId 的记忆函数的缓存,大概如下

f.memoize = {
    123: function memoize() {...},
    234: ...
    345: ...
}
复制代码

通过 f.memoize 实现了变量查询型 selector 的缓存相比无缓存版本对 React 组件渲染有质的优化效果。但不同的 conversationId 对应的缓存函数都在挂载在 f.memoize 上,如果没有加任何缓存策略进行维护,不断增加的 conversationId 将给运行时内存带来损耗,用户需要权衡决定最佳实践。

分类:
前端
收藏成功!
已添加到「」, 点击更改