编写更快的 React 代码(一):memoize-one 简介

编写更快的 React 代码(一):memoize-one 简介

引言

不同类型业务要求的性能标准各不相同。如果对一个 ToB 的后台管理系统要求首屏速度以及 SEO,显然不合理也没必要。

第一要考虑的不是如何去优化,而是值不值得去优化,React 性能已经足够优秀,毕竟“过早优化是魔鬼”,情况总是“可以,但没必要”。

作为一个开发人员,深入了解工具不足之处,并拥有对其进行优化的能力,是极其重要的。

React 性能优化大抵可分为两点:

  1. 减少 rerender 次数 (immutable data、shouldComponentUpdate、PureComponent)
  2. 减轻 rerender 复杂度 (memoize-one)

本文基于 memoize-one 对 render 方法进行优化,达到减轻不必要 render 复杂度的效果。

存在的问题

先看一个简单的组件,如下所示:

class Example extends Component {
  state = {
    filterText: ""
  };

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    const filteredList = this.props.list.filter(item =>
      item.text.includes(this.state.filterText)
    );

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>
          {filteredList.map(item => (
            <li key={item.id}>{item.text}</li>
          ))}
        </ul>
      </Fragment>
    );
  }
}

该组件接收父组件传递的 list,筛选出包含 filterText 的 filteredList 并进行展示。

问题是什么?

在未进行任何处理的情况下,父组件 render,总会导致子组件 render,即使子组件的 state/props 并未发生变化,如果筛选的数据量大,筛选逻辑复杂,这将是一个很重要的优化点。

要达到怎样的效果?

  1. state(filterText)/props(list)未发生变化时,不进行 render(引言-性能优化第 1 点), 此处暂不讨论
  2. state(filterText)/props(list)未发生变化时,进行 render,复用上一次计算结果

memoize-one

A memoization library which only remembers the latest invocation

基本使用

import memoize from "memoize-one";

const add = (a, b) => a + b; // 基本计算方法
const memoizedAdd = memoize(add); // 生成可缓存的计算方法

memoizedAdd(1, 2); // 3

memoizedAdd(1, 2); // 3
// Add 函数没有被执行:上一次的结果直接返回

memoizedAdd(2, 3); // 5
// Add 函数被调用获取新的结果

memoizedAdd(2, 3); // 5
// Add 函数没有被执行:上一次的结果直接返回

memoizedAdd(1, 2); // 3
// Add 函数被调用获取新的结果
// 即使该结果在之前已经缓存过了
// 但它并不是最近一次的缓存结果,所以缓存结果丢失了

在了解基本使用后,我们来对上述案例进行优化。

优化案例

import memoize from "memoize-one";

class Example extends Component {
  state = { filterText: "" };

  // 只有在list或filterText改变的时候才会重新调用真正的filter方法(memoize入参)
  filter = memoize((list, filterText) =>
    list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    // 在上一次render后,如果参数没有发生改变,`memoize-one`会重复使用上一次的返回结果
    const filteredList = this.filter(this.props.list, this.state.filterText);

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>
          {filteredList.map(item => (
            <li key={item.id}>{item.text}</li>
          ))}
        </ul>
      </Fragment>
    );
  }
}

源码解析

如果除去 ts 相关以及注释,不到 20 行。memoize-one 本质是一个高阶函数,真正计算函数作为参数,返回一个新的函数,新的函数内部会缓存上一次入参以及上一次返回值,如果本次入参与上一次入参相等,则返回上一次返回值,否则,重新调用真正的计算函数,并缓存入参以及结果,供下一次使用。

假装这里有一张流程图 :)

// 默认比较先后入参是否相等的方法,使用者可自定义比较方法
import areInputsEqual from './are-inputs-equal';

// 函数签名
export default function<ResultFn: (...any[]) => mixed>(
  resultFn: ResultFn,
  isEqual?: EqualityFn = areInputsEqual,
): ResultFn {
  // 上一次的this
  let lastThis: mixed;
  // 上一次的参数
  let lastArgs: mixed[] = [];
  // 上一次的返回值
  let lastResult: mixed;
  // 是否已经初次调用过了
  let calledOnce: boolean = false;

  // 被返回的函数
  const result = function(...newArgs: mixed[]) {
    // 如果参数或this没有发生变化或非初次调用
    if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
      // 直接返回上一次的计算结果
      return lastResult;
    }

    // 参数发生变化或者是初次调用
    lastResult = resultFn.apply(this, newArgs);
    calledOnce = true;
    // 保存当前参数
    lastThis = this;
    // 保存当前结果
    lastArgs = newArgs;
    // 返回当前结果
    return lastResult;
  };

  // 返回新的函数
  return (result: any);
}

拓展:斐波那契数列

下面是一个计算斐波那契数列的例子,该例子使用迭代代替递归,并且利用闭包缓存之前的结果。

const createFab = () => {
  const cache = [0, 1, 1];
  return n => {
    if (typeof cache[n] !== "undefined") {
      return cache[n];
    }
    for (let i = 3; i <= n; i++) {
      if (typeof cache[i] !== "undefined") continue;
      cache[i] = cache[i - 1] + cache[i - 2];
    }
    return cache[n];
  };
};

const fab = createFab();

总结

本文基于 React 介绍了 memoize-one 库的相关使用及其原理,在 React 中实现了类似与 Vue 计算属性(computed)的效果 —— 基于依赖缓存计算结果,达到减轻不必要 render 复杂度的效果。

从业务开发角度来讲,Vue 提供的 API 极大地提高了开发效率。

React 自身解决的问题并不多,但得益于活跃的社区,工作中遇到的解决问题都能找到解决方案,并且在摸索这些解决方案的同时,我们能够学习到诸多经典的编程思想,从而减轻对框架的依赖。

I’ve always said that React will make you a better JavaScript developer. - Tyler McGinnis

参考链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容