Reselect的学习以及一些思考

Reselect的学习以及一些思考

前言

我们平时在使用react-redux时,用到最多的函数大概就是mapStateToPropsmapDispatchToProps。其中,

mapStateToProps函数允许我们将 store 中的数据作为 props 绑定到组件上,它的第一个参数就是 Redux 的整个状态树(state):

const Counter = props => <div>Count: {props.count}</div>;
const mapStateToProps = state => ({ count: state.count });
export default connect(mapStateToProps)(Counter);

上述代码里,我们从state中摘取了 count 属性。因为返回了具有 count 属性的对象,所以 Counter组件的props中会有一个 count字段。

一切看起来很完美。但是仔细想一下,每次状态树更新之后,redux 都会调用一次mapStateToProps,当我们的应用足够大,组件众多时,这里面的大量冗余计算就值得重视了。

这个时候就需要 Reselect 出马了

正文

我们先看一个小栗子:

假设我们有一组复选框,用来罗列各个商品的名称和价钱,底部用来展示我们选择商品的总价,我们用 redux 实现:

const getItems = state => state.items;

const getTotal1 = createSelector(
  getItems,
  items => {
    console.log("total-with-reselect 将要计算");
    return items.reduce(
      (acc, item) => acc + (item.checked ? item.price : 0),
      0
    );
  }
);

const getTotal2 = state => {
  console.log("total-without-reselect 将要计算");
  return state.items.reduce(
    (acc, item) => acc + (item.checked ? item.price : 0),
    0
  );
};

const mapStateToProps = state => ({
  total1: getTotal1(state),
  total2: getTotal2(state)
});

我们通过createSelector创建了一个记忆选择器getTotal1,另外创建了一个不带缓存的求和函数getTotal2。另外,我们在页面中加一个 input 输入框,并且将其值绑定到 redux 的 state 中,用作干扰项。

结果如下:
在这里插入图片描述

对比发现,从 state 中取值的方式有一些细微改变getTotal1是通过getItems提取 state 中的源数据,然后再通过一个包装后的函数计算总和;getTotal2则是直接从 state 中拿值并计算总和。

前者的好处是:getItems返回值不变时,计算函数不会重新执行。也就是说,当 state 中的items没有变化时,reselect 会直接返回上一次计算缓存的结果,避免多余的重复计算。

那么reselect是如何实现这一机制的呢?看一下源码:

/**
 * 默认的比较函数
 * @param {*} a 待比较项
 * @param {*} b 待比较项
 */
function defaultEqualityCheck(a, b) {
  return a === b;
}

/**
 * 参数是否浅等
 * @param {Function} equalityCheck 比较函数
 * @param {Object} prev 前一次参数
 * @param {Object} 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;
}

/**
 * 默认的记忆函数(核心)
 * @param {Function} func 计算结果函数
 * @param {Function} 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;
  };
}

/**
 * 获取input-selectors数组
 * @param {Array} funcs input-selectors数组
 */
function getDependencies(funcs) {
  // input-selectors数组可以有两种形式:
  // 1.[input-selector1, input-selector2...]
  // 2.[[input-selector1, input-selector2...]]
  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;
}

/**
 * 选择器创建器创建方法
 * @param {Function} memoize 记忆函数
 * @param  {...any} memoizeOptions 记忆函数参数(可不传)
 */
export function createSelectorCreator(memoize, ...memoizeOptions) {
  // funcs: 创建选择器传入的input-selectors和resultFunc
  return (...funcs) => {
    let recomputations = 0;
    const resultFunc = funcs.pop();
    const dependencies = getDependencies(funcs);

    // 真正的记忆函数
    // createSelector方法中,memoize就是defaultMemoize
    const memoizedResultFunc = memoize(function() {
      recomputations++;
      // apply arguments instead of spreading for performance.
      // 这里arguments是各个依赖项(input-selector)执行结果的数组(下文的params)
      return resultFunc.apply(null, arguments);
    }, ...memoizeOptions);

    // 内部二次优化: 使用完全相同的参数(state)调用选择器,则不需要再次遍历依赖项(input-selectors)
    // 因为reducer是不一定会返回一个新的state,所以state没变的时候,真正的记忆函数就不用被调用。
    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.
        // 这里arguments是state
        // 根据传入的state计算每个input-selector结果, 并将结果push到params数组中
        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;
  };
}

// 默认的选择器创建方法
export const createSelector = createSelectorCreator(defaultMemoize);

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(objectKeys.map(key => selectors[key]), (...values) => {
    return values.reduce((composition, value, index) => {
      composition[objectKeys[index]] = value;
      return composition;
    }, {});
  });
}

阅读完源码发现,这个库的代码量很小,主要就是采用了闭包 + 高阶函数来实现,核心方法其实就是defaultMemoize

思考

由于 Reselect 缓存结果这一特性,我们可以将其延伸一下,不一定非要结合 redux 使用。

例如,给定一组坐标数据绘制图表:

const data = [
  { x: 1, y: 3 },
  { x: 2, y: 7 },
  { x: 3, y: 2 },
  { x: 4, y: 9 },
  { x: 5, y: 3 }
];

在这里插入图片描述

普通写法

// 假定有一个画图API:DrawAPI
const drawFn = DrawAPI.draw;

普通写法,每次后台来了新数据时,无论具体的值是否发生变化都重新绘制,势必会造成不必要的性能浪费。

进阶写法

import { defaultMemoize } from "reselect";

const checkEqualFn = (oldData, newData) => {
  if (oldData.length !== newData.length) {
    return false;
  } else {
    const length = oldData.length;
    for (let i = 0; i < length; i++) {
      const { x: xOld, y: yOld } = oldData[i];
      const { x: xNew, y: yNew } = newData[i];
      if (xOld !== xNew || yOld !== yNew) {
        return false;
      }
    }
    return true;
  }
};

const drawFn = defaultMomize(DrawAPI.draw, checkEqualFn);

进阶写法,只有当后台给的数据发生变化时,才重新绘制。

参考链接

https://juejin.im/post/5ae7dac151882567113afc0d
https://segmentfault.com/a/1190000011916280

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值