Неправильное понимание useCallback/useMemo

JavaScript React.js
Неправильное понимание useCallback/useMemo

При написании кода React Hook,useCallbackа такжеuseMemoЧасто сбивает с толку. Хотя мы знаем, что их функции заключаются в кэшировании и оптимизации производительности, нас беспокоит негативная оптимизация из-за неправильного использования. Эта статья объяснитuseCallbackа такжеuseMemoОбщие методы использования и недоразумения в разработке, в сочетании с анализом исходного кода причин, знать, что и почему.

1.useCallback

1.1 Не злоупотреблятьuseCallback

Рассмотрим следующие примеры:

import React from 'react';

function Comp() {
    const onClick = () => {
        console.log('打印');
    }
    
    return <div onClick={onClick}>Comp组件</div>
}

когдаCompКогда компонент сам запускает обновление или в качестве дочернего компонента следует за родительским компонентом для обновления, мы замечаемonClickбудет переназначен. Чтобы «улучшить производительность», используйтеuseCallbackпакетonClickДля целей кэширования:

import React, { useCallback } from 'react';

function Comp() {
    const onClick = useCallback(() => {
        console.log('打印');
    }, []);
    
    return <div onClick={onClick}>Comp组件</div>
}

Итак, вопрос в том, улучшилась ли производительность? Ответ — нет, не так хорошо, как раньше, после того, как мы перепишем логическую структуру кода, причина будет предельно ясна:

import React, { useCallback } from 'react';

function Comp() {
    const onClick = () => {
        console.log('打印');
    };
    
    const memoOnClick = useCallback(onClick, []);
    
    return <div onClick={memoOnClick}>Comp组件</div>
}

Выполнение каждой дополнительной строки кода влечет за собой затраты, даже если эта стоимость представляет собой всего лишь небольшое количество тепла процессора. В официальной документации указано, чтоНе нужно беспокоиться о создании функций, вызывающих проблемы с производительностью., поэтому используйтеuseCallbackПреобразование компонентов в этом сценарии не дает никаких преимуществ (функция все равно будет создана), но стоимость, которую она приносит, обременяет компоненты (нужно сравнивать, изменились ли зависимости),useCallbackЧем больше вы используете, тем больший вес вы несете. С точки зрения javascript, когда компонент обновляется, он неuseCallbackОбернутый метод будет удален сборщиком мусора и переопределен, ноuseCallbackСделанное закрытие будет содержать ссылку на функцию обратного вызова и зависимости.

1.2 useCallbackправильный способ использования

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

  • 1. Уменьшите количество повторных рендеров. Поскольку наиболее ресурсоемкой частью React является согласование, согласование не будет запущено, пока оно не отобразится.
  • 2. Уменьшить объем расчета, об этом естественно говорить не приходится.

Итак, рассмотрим следующие сценарии:

import React, { useState } from 'react';

function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = () => {
        setDataB(o => o + 1);
    }
    
    return <div>
        <Cheap onClick={onClickA}>组件Cheap:{dataA}</div>
        <Expensive onClick={onClickB}>组件Expensive:{dataB}</Expensive>
    </div>
}

Expensiveявляется очень дорогим компонентом для рендеринга, но щелчокCheapкомпоненты также могут вызыватьExpensiveперерисовать, даже еслиdataBНичего не изменилось. Причина в том,onClickBпереопределяется, в результате чего React определяет, что компонент изменился при сравнении старого и нового компонентов. В этот моментuseCabllbackа такжеmemoЭто сработало:

import React, { useState, memo, useCallback } from 'react';

function Expensive({ onClick, name }) {
  console.log('Expensive渲染');
  return <div onClick={onClick}>{name}</div>
}

const MemoExpensive = memo(Expensive);

function Cheap({ onClick, name }) {
  console.log('cheap渲染');
  return <div onClick={onClick}>{name}</div>
}

export default function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = useCallback(() => {
        setDataB(o => o + 1);
    }, []);
    
    return <div>
        <Cheap onClick={onClickA} name={`组件Cheap:${dataA}`}/>
        <MemoExpensive onClick={onClickB} name={`组件Expensive:${dataB}`} />
    </div>
}

memoЭто новый метод, добавленный React v16.6.0.Подобно PureComponent, первый отвечает за оптимизацию функционального компонента, а второй — за компонент класса. Оба они выполняют поверхностное сравнение старых и новых данных, переданных в компонент, и, если они совпадают, они не запускают рендеринг.

такuseCallbackобещатьonClickBБез изменений, нажмите сейчасCheapКомпонент не срабатываетExpensiveОбновление компонента, только нажмитеExpensiveкомпонент сработает. Внедряя оптимизации для уменьшения ненужного рендеринга,useCallbackа такжеmemoЭто пара оружия.Запустите пример кода

1.3 Расширение

useCallbackИсходный код выглядит следующим образом:

// 初始化阶段
function mountCallback(callback, deps) {
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    hook.memoizedState = [callback, nextDeps];
    return callback;
}

// 更新阶段
function updateCallback(callback, deps) {
    const hook = updateWorkInProgressHook();,
    const nextDeps = deps === undefined ? null : deps;
    const prevState = hook.memoizedState;
    if (prevState !== null) {
        if (nextDeps !== null) {
            const prevDeps = prevState[1];
            // 比较是否相等
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                // 如果相等,返回旧的 callback
                return prevState[0];
            }
        }
    }
  
    hook.memoizedState = [callback, nextDeps];
    return callback;
}

Основная логика заключается в сравненииdepsЕсть ли изменение, если есть изменение, верните новыйcallbackфункция, в противном случае вернуть исходную функцию. из них сравнительные методыareHookInputsEqualВнутренние вызовы ReactisМетод инструмента:

// 排除以下两种特殊情况:
// +0 === -0  // true
// NaN === NaN // false

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
  );
}

2.useMemo

2.1 Не злоупотреблятьuseMemo

import React, { useMemo } from 'react';

function Comp() {
    const v = 0;
    const memoV = useMemo(() => v, []);
    
    return <div>{memoV}</div>;
}

СоздайтеmemoVНакладные расходы не нужны по тем же причинам, что и в разделе 1. Это необходимо только в том случае, если само поведение создания влечет за собой большие накладные расходы (например, тысячи вычислений для создания значения переменной).useMemo, конечно, такие сцены редкость.

2.2 useMemoправильный способ использования

Ранее мы упоминали, что одна из двух основных идей по оптимизации производительности компонентов React заключается в уменьшении объема вычислений, что такжеuseMemoИспользование:

import React, { useMemo } from 'react';

function Comp({ a, b }) {
    const v = 0;
    const calculate = (a, b) => {
        // ...  complex calculation
        return c;
    }
    
    const memoV = useMemo((a, b) => v, [a, b]);
    
    return <div>{memoV}</div>;
}

3. Резюме

React Hook предъявляет очень высокие требования к командному сотрудничеству и согласованности.useCallbackа такжеuseMemoЭта пара методов является хорошим примером, для более сложных сценариев есть также парыuseRef, использование пользовательских хуков и многое другое. Исходя из опыта, командам необходимо усилить проверку кода при кодировании ловушек, в противном случае могут возникнуть трудно обнаруживаемые ошибки или проблемы с производительностью. В настоящее время различные методы Hook не совершенны, и в Twitter есть много аргументов Мы с нетерпением ждем последующих версий React, которые предоставят более зрелые и простые в использовании решения.