Применение memoize-one в React

React.js

Написание более быстрого кода React, часть 1: введение в memoize-one

введение

Различные типы услуг требуют различных стандартов производительности. Очевидно, что неразумно и ненужно требовать максимальной скорости и SEO для системы управления фоном ToB.

Первое, что нужно учитывать, это не как оптимизировать, а стоит ли оптимизировать, производительность React достаточно хороша, ведь «преждевременная оптимизация — это дьявол», ситуация всегда «хорошо, но не нужно».

Как разработчик, чрезвычайно важно иметь глубокое понимание того, где инструмент не работает, и иметь возможность его оптимизировать.

Оптимизацию производительности React можно условно разделить на два пункта:

  1. Уменьшить количество повторных рендеров (неизменяемые данные, shouldComponentUpdate, PureComponent)
  2. Уменьшить сложность повторного рендеринга (memoize-one)

В этой статье оптимизируется метод рендеринга на основе memoize-one, чтобы уменьшить ненужную сложность рендеринга.

существующие проблемы

Давайте сначала посмотрим на простой компонент, как показано ниже:

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

Этот компонент получает список, переданный родительским компонентом, отфильтровывает filteredList, содержащий filterText, и отображает его.

в чем проблема?

В отсутствие какой-либо обработки рендеринг родительского компонента всегда будет вызывать рендеринг дочернего компонента, даже если состояние/реквизиты дочернего компонента не изменились, если количество отфильтрованных данных велико, а логика фильтрации сложна, это будет очень важным моментом оптимизации.

Для достижения какого эффекта?

  1. Когда state(filterText)/props(list) не изменились, рендеринг не будет выполняться (введение-оптимизация производительности, пункт 1), который пока не будет обсуждаться здесь.
  2. Когда состояние (filterText)/реквизиты (список) не изменилось, выполните рендеринг и повторно используйте последний результат вычисления.

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

Анализ исходного кода

Менее 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);
}

В качестве альтернативы вы можете использоватьdeckoЭта библиотека со встроенными декораторами bind/memoize/debounce хорошо совместима с React.

Расширение: последовательность Фибоначчи

Ниже приведен пример вычисления последовательности Фибоначчи с использованием итерации вместо рекурсии и использования замыкания для кэширования предыдущих результатов.

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();

Суммировать

В этой статье рассказывается об использовании и принципе библиотеки memoize-one, основанной на React, и достигается эффект, аналогичный вычисляемому свойству Vue (computed) в React, основанном на кэшировании результатов вычислений для уменьшения ненужной сложности рендеринга.

С точки зрения развития бизнеса API, предоставляемый Vue, значительно повышает эффективность разработки.

React не решает многих проблем сам по себе, но благодаря активному сообществу можно найти решения проблем, возникающих в работе, и, изучая эти решения, мы можем изучить многие классические идеи программирования, тем самым уменьшив зависимость от фреймворков.

Я всегда говорил, что React сделает вас лучшим разработчиком JavaScript», — Тайлер МакГиннис.

Ссылка на ссылку