1. Введение
Если вы используете React 16, вы можете попробовать стиль функционального компонента для большей гибкости. Но прежде чем попробовать, лучше прочитать эту статью и получить предварительное представление о режиме мышления функционального компонента, чтобы предотвратить проблемы, вызванные асинхронным режимом мышления.
2. Интенсивное чтение
Что такое функциональный компонент?
Функциональный компонент — это компонент React, созданный в виде функции:
function App() {
return (
<div>
<p>App</p>
</div>
);
}
То есть тот, который возвращает JSX илиcreateElement
Функцию можно рассматривать как компонент React, и эта форма компонента является функциональным компонентом.
Итак, я уже изучил функциональный компонент?
Не волнуйтесь, история только начинается.
Что такое крючки?
Хуки — это инструменты, помогающие функциональному компоненту. НапримерuseState
Это хук, который можно использовать для управления состоянием:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
useState
Возвращаемый результат представляет собой массив, первый элемент массивастоимость, второй членФункция назначения,useState
Первый параметр функцииПо умолчанию, также поддерживает функции обратного вызова. Для получения более подробной информации см.Интерпретация правил хуков.
Сначала назначьте, а затем установите печать Timeout
мы будемuseState
а такжеsetTimeout
Объедините их и посмотрите, что вы найдете.
Создайте кнопку, которая увеличивает счетчик при нажатии,Но распечатывает с задержкой в 3 секунды:
function Counter() {
const [count, setCount] = useState(0);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
если мыНажмите три раза подряд в течение трех секунд,Такcount
Значение в конечном итоге станет3
, и в результате получается . . ?
0
1
2
Что ж, это кажется правильным, но всегда кажется немного странным?
Как насчет использования Class Component для повторной реализации?
Стукнем по доске, вернемся в знакомый нам режим Class Component и снова реализуем вышеуказанные функции:
class Counter extends Component {
state = { count: 0 };
log = () => {
this.setState({
count: this.state.count + 1
});
setTimeout(() => {
console.log(this.state.count);
}, 3000);
};
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.log}>Click me</button>
</div>
);
}
}
Ну, результаты должны быть эквивалентны, верно? Нажмите кнопку три раза в течение 3 секунд, на этот раз результат является:
3
3
3
Почему результат отличается от функционального компонента?
Это первое препятствие, которое вы должны преодолеть, чтобы правильно использовать функциональный компонент.Пожалуйста, убедитесь, что вы полностью поняли следующий отрывок:
Сначала объясните компонент класса:
- Первое состояние неизменно,
setState
После этого будет сгенерирована новая ссылка на состояние. - Но компонент класса проходит
this.state
способ чтения состояния,Это приводит к тому, что последняя ссылка на состояние извлекается каждый раз при выполнении кода., поэтому результат трех быстрых щелчков3 3 3
.
Затем для функционального компонента:
-
useState
Сгенерированные данные также являются неизменяемыми.После установки нового значения через второй параметр массива исходное значение сформирует новую ссылку при следующем рендеринге. - Но так как чтение состояния не удается
this.
таким образом, чтокаждый разsetTimeout
Оба считывают данные среды закрытия рендеринга в то время.Хотя последнее значение изменилось с последним рендерингом, в старом рендеринге состояние остается старым значением.
Чтобы было легче понять, давайте смоделируем состояние, когда кнопка нажимается три раза в режиме функционального компонента:
Первый клик, отрендеренный всего 2 раза,setTimeout
эффективным в1
Второй рендеринг, состояние:
function Counter() {
const [0, setCount] = useState(0);
const log = () => {
setCount(0 + 1);
setTimeout(() => {
console.log(0);
}, 3000);
};
return ...
}
Второй щелчок, отрендеренный всего 3 раза,setTimeout
эффективным в2
Второй рендеринг, состояние:
function Counter() {
const [1, setCount] = useState(0);
const log = () => {
setCount(1 + 1);
setTimeout(() => {
console.log(1);
}, 3000);
};
return ...
}
Третий клик, отрендеренный всего 4 раза,setTimeout
эффективным в3
Второй рендеринг, состояние:
function Counter() {
const [2, setCount] = useState(0);
const log = () => {
setCount(2 + 1);
setTimeout(() => {
console.log(2);
}, 3000);
};
return ...
}
Как видите, каждый рендеринг является независимым замыканием.count
Значения в каждом рендере0 1 2
, так что всеsetTimeout
Как долго задержка, распечатанный результат всегда будет0 1 2
.
Имея это в виду, мы можем двигаться дальше.
Как сделать так, чтобы функциональный компонент также печатался3 3 3
?
Значит ли это, что функциональный компонент не может переопределить функцию классового компонента? совершенно нет,Я надеюсь, что прочитав эту статью, вы сможете не только решить эту проблему, но и понять, почему код, реализованный с помощью Function Component, более разумен и элегантен..
Первое решение — использовать новый хук —useRef
Способность:
function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
Результат печати этой схемы3 3 3
.
Чтобы понять почему, нужно сначала понятьuseRef
функция:пройти черезuseRef
Созданный объект только с одной копией его значения и общий для всех визуализаторов.
По этому мыcount.current
Назначение или чтение, чтение всегда является его последним значением, независимо от закрытия рендеринга, поэтому, если вы быстро щелкните три раза, оно обязательно вернется3 3 3
результат.
Но есть проблема с этой схемой, то есть с использованиемuseRef
замененыuseState
При создании значения возникает естественный вопрос: как добиться того же эффекта, не меняя способ написания исходного значения?
Как также печатать без преобразования исходного значения3 3 3
?
Самый простой способ сделать это — создать новыйuseRef
Значение даетsetTimeout
использовать, в то время как остальная часть программы использует оригиналcount
:
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect(() => {
currentCount.current = count;
});
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
В этом примере мы вводим новый, такжеСамый главный крючок -useEffect
, обязательно хорошо разбирайтесь в этой функции.
useEffect
заключается в том, чтобы иметь дело с побочными эффектами, и время его выполненияПосле каждого рендера, другими словами, он будет выполняться каждый раз при рендеринге, но только после завершения фактической манипуляции с DOM.
Мы можем воспользоваться этой функцией, после каждого рендерингаcount
Последнее значение в это время присваиваетсяcurrentCount.current
, Таким образом делаяcurrentCount
Значение автоматически синхронизируется.count
последнее значение .
Чтобы все понялиuseEffect
, автор более многословен и разбирает его цикл выполнения на каждый рендеринг. Предположим, вы быстро нажимаете кнопку три раза за три секунды, тогда вам нужно смоделировать в своем мозгу, что происходит в следующих трех рендерах:
Первый клик, отрендеренный всего 2 раза,useEffect
эффективным в2
Вторичный рендеринг:
function Counter() {
const [1, setCount] = useState(0);
const currentCount = useRef(0);
useEffect(() => {
currentCount.current = 1; // 第二次渲染完毕后执行一次
});
const log = () => {
setCount(1 + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return ...
}
Второй щелчок, отрендеренный всего 3 раза,useEffect
эффективным в3
Вторичный рендер:
function Counter() {
const [2, setCount] = useState(0);
const currentCount = useRef(0);
useEffect(() => {
currentCount.current = 2; // 第三次渲染完毕后执行一次
});
const log = () => {
setCount(2 + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return ...
}
Третий клик, отрендеренный всего 4 раза,useEffect
эффективным в4
Вторичный рендер:
function Counter() {
const [3, setCount] = useState(0);
const currentCount = useRef(0);
useEffect(() => {
currentCount.current = 3; // 第四次渲染完毕后执行一次
});
const log = () => {
setCount(3 + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return ...
}
Обратите внимание на контраст сsetTimeout
Какая разница при рендеринге.
Чтобы быть осторожным,useEffect
Также зависит от каждого рендера,Между различными рендерингами одного и того же компонента,useEffect
Внутренняя среда закрытия полностью независима. Для этого примераuseEffect
выполнено всегочетыре раза, после четырех назначений следующим образом, наконец, становится3
:
currentCount.current = 0; // 第 1 次渲染
currentCount.current = 1; // 第 2 次渲染
currentCount.current = 2; // 第 3 次渲染
currentCount.current = 3; // 第 4 次渲染
Прежде чем читать дальше, убедитесь, что вы поняли это предложение:
-
setTimeout
например, три клика вызывают четыре рендеринга, ноsetTimeout
вступает в силу при 1-м, 2-м и 3-м рендеринге соответственно, поэтому значение равно0 1 2
. -
useEffect
например, три щелчка также запускают четыре рендеринга, ноuseEffect
Вступают в силу в 1-й, 2-й, 3-й и 4-й визуализациях соответственно и, наконец, делаютcurrentCount
значение становится3
.
Оберните его специальным крючкомuseRef
Вам не хочется писать их кучу каждый раз?useEffect
Синхронизировать данные сuseRef
раздраженный? Да, для упрощения нужно ввести новое понятие:Пользовательские крючки.
Прежде всего, пользовательские хуки позволяют создавать пользовательские хуки, если имя функции следует заuse
В начале возвращайте не-JSX-элементы, то есть хуки!Все пользовательские хуки, включая встроенные хуки, также могут быть вызваны из пользовательских хуков..
То есть мы можемuseEffect
Напишите в пользовательский хук:
function useCurrentValue(value) {
const ref = useRef(0);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
Вот новая концепция,то естьuseEffect
Второй параметр, зависимости. зависимости Этот параметр определяетuseEffect
, в новом рендере, пока ссылки всех зависимостей не меняются,useEffect
не будет выполняться, а когда зависимость[]
час,useEffect
Выполняется только один раз при инициализации, последующие рендереры никогда не будут выполняться.
В этом примере мы говорим React: только еслиvalue
Значение , а затем синхронизируйте его последнее значение сref.current
.
Затем этот пользовательский хук можно вызвать в любом функциональном компоненте:
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useCurrentValue(count);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
После инкапсуляции код намного чище, а самое главное инкапсулировать логику, надо только понятьuseCurrentValue
Этот хук может создать значение, последнее значение которого всегда синхронизировано с входным параметром.
Смотрите здесь, возможно, некоторые маленькие партнеры испытали порыв вдохновения:БудуuseEffect
Второй параметр установлен в пустой массив, и этот пользовательский хук представляетdidMount
Жизненный цикл!
Да, но всем рекомендуюПерестаньте думать о вещах жизненного цикла, что помешает лучшему пониманию функционального компонента. Потому что следующая тема должна сказать вам:всегда быть правымuseEffect
Зависимости честные, а зависимые параметры должны быть заполнены, иначе будут баги, которые очень сложно обнаружить и исправить.
БудуsetTimeout
заменитьsetInterval
что случится
Вернемся к исходной точке и поставим первыйsetTimeout
Заменено в демо наsetInterval
, и посмотрим, что произойдет:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
Этот пример приведет ко второму камню преткновения в изучении функционального компонента, и после его понимания мы сможем глубоко понять принцип рендеринга функционального компонента.
Во-первых, давайте представим введенную новую концепцию,useEffect
возвращаемое значение функции. Его возвращаемое значение — это функция, которая находится вuseEffect
Когда он собирается повторно выполняться, последний рендерер будет выполнен первым.useEffect
Функция возврата первого обратного вызова, а затем выполнение следующего рендерингаuseEffect
Первый обратный звонок.
Взяв в качестве примера два последовательных рендеринга, расширенный эффект выглядит следующим образом:
Первый рендер:
function Counter() {
useEffect(() => {
// 第一次渲染完毕后执行
// 最终执行顺序:1
return () => {
// 由于没有填写依赖项,所以第二次渲染 useEffect 会再次执行,在执行前,第一次渲染中这个地方的回调函数会首先被调用
// 最终执行顺序:2
}
});
return ...
}
Второй рендер:
function Counter() {
useEffect(() => {
// 第二次渲染完毕后执行
// 最终执行顺序:3
return () => {
// 依此类推
}
});
return ...
}
Тем не менее, эта демонстрация будетuseEffect
Второй параметр установлен в[]
, то его функция возврата будет выполняться только при уничтожении компонента.
Прочитав предыдущий пример, вы должны подумать, что эта демонстрация надеется использовать[]
зависит отuseEffect
так какdidMount
использовать, комбинироватьsetInterval
каждый разcount
самоприращение, так что ожидание будетcount
Значение увеличивается на 1 каждую секунду.
Однако результат таков:
1
1
1
...
пониматьsetTimeout
Читатели примера должны понять, почему:setInterval
Всегда в закрытии первого рендера,count
Значение всегда0
, что эквивалентно:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(0 + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
Однако виновником являетсяне быть честным в отношении довериявызванный. примерuseEffect
очевидно, зависело отcount
, но зависимости должны быть записаны[]
, поэтому возникает непонятная ошибка.
Так что способ исправить эточестность о зависимости.
Навсегда зависимости честные
Когда мы честно говорим о зависимостях, мы получаем правильный эффект:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
мы будемcount
так какuseEffect
зависимости, вы получите правильный результат:
1
2
3
...
Так как риск отсутствия зависимостей настолько велик, естественно существуют защитные меры, т.е.eslint-plugin-react-hooksЭтот плагин будет автоматически пересматривать вашу зависимость в вашем коде, вы не можете этого сделать, вы не можете этого сделать!
Однако для этого примера в коде все еще есть ошибка: счетчик каждый раз создается заново, и затраты производительности неприемлемы, если он заменяется другими дорогостоящими операциями.
Как не создавать повторный экземпляр при каждом рендереsetInterval
?
Самый простой способ - использоватьuseState
Использование второго присваивания , не зависит напрямую отcount
, но присвойте значение в обратном вызове функции:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
Это письмо действительно делает это:
- не зависеть от
count
, так что будьте честны насчет зависимостей. - Зависимость
[]
, будет только инициализацияsetInterval
создать экземпляр.
И вывод по-прежнему правильный1 2 3 ...
, Причина вsetCount
в функции обратного вызова,c
Значение всегда указывает на последнююcount
значение, поэтому нет логических дыр.
Но когда сообразительные ученики хорошенько об этом подумают, они обнаружат новую проблему: если нужно использовать более двух переменных, этот трюк бесполезен.
При использовании более двух переменных одновременно?
Если вам нужноcount
а такжеstep
Накопить две переменные, затемuseEffect
Зависимость должна быть записана с определенным значением, и снова возникает проблема частого инстанцирования:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return <h1>{count}</h1>;
}
В этом примере, посколькуsetCount
получить только последнююcount
значение, и для того, чтобы каждый раз получать последниеstep
значение, оно должно бытьstep
объявитьuseEffect
зависимость, в результате чегоsetInterval
часто создается экземпляр.
Эта проблема, естественно, беспокоила команду React, поэтому они придумали новый хук для решения проблемы:useReducer
.
что такое useReducer
Пока не думайте о Redux. Просто рассмотрите приведенный выше сценарий и поймите, почему команда ReactuseReducer
Перечислен как один из встроенных хуков.
Представьте это на это сначалаuseReducer
Применение:
const [state, dispatch] = useReducer(reducer, initialState);
useReducer
Возвращаемая структура такая же, какuseState
Почти так же, за исключением того, что второй элемент в массивеdispatch
, а полученных параметров тоже два, начальное значение ставится на второе место, первоеreducer
.
reducer
Определяет, как преобразовывать данные, такие как простойreducer
следующим образом:
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
count: state.count + 1
};
default:
return state;
}
}
Это можно сделать, позвонивdispatch({ type: 'increment' })
способ достиженияcount
самовозрастающий.
Итак, вернемся к этому примеру, нам просто нужно немного переписать использование:
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
function reducer(state, action) {
switch (action.type) {
case "tick":
return {
...state,
count: state.count + state.step
};
}
}
Видно, что мы проходимreducer
изtick
введите полную паруcount
накопление , в то время как вuseEffect
В функции полностью игнорируетсяcount
,step
эти две переменные. такuseReducer
Также известен как «черная магия» для решения таких проблем.
На самом деле, как бы это ни называлось, суть его в том, чтобы отделить функции от данных.Функция только что выпустила директиву без необходимости беспокоиться об использовании данных обновляется, необходимо повторно инициализировать себя.
Внимательные читатели обнаружат, что в этом примере все еще есть зависимость, т.е.dispatch
,Однакоdispatch
Ссылка никогда не меняется, поэтому ее эффект можно игнорировать.Это также отражает честность в отношении зависимостей, несмотря ни на что.
Это также вызывает еще одно замечание:Попробуйте написать функцию вuseEffect
внутренний.
написать функцию вuseEffect
внутренний
Чтобы избежать пропущенных зависимостей, функция должна быть написана наuseEffect
внутри вот такeslint-plugin-react-hooksЧтобы завершить зависимости с помощью статического анализа:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
function getFetchUrl() {
return "https://v?query=" + count;
}
getFetchUrl();
}, [count]);
return <h1>{count}</h1>;
}
getFetchUrl
Эта функция зависит отcount
, а если функция определена вuseEffect
Снаружи трудно разглядеть машина это или человеческий глазuseEffect
Зависимости содержатcount
.
Однако при этом возникает новая проблема: запись всех функций вuseEffect
Не сложно ли ухаживать за салоном?
как извлечь функциюuseEffect
внешний?
Чтобы решить эту проблему, мы собираемся представить новый хук:useCallback
, это решение для извлечения функции вuseEffect
внешние проблемы.
давайте сначала посмотримuseCallback
Применение:
function Counter() {
const [count, setCount] = useState(0);
const getFetchUrl = useCallback(() => {
return "https://v?query=" + count;
}, [count]);
useEffect(() => {
getFetchUrl();
}, [getFetchUrl]);
return <h1>{count}</h1>;
}
можно увидеть,useCallback
Так же есть второй параметр - зависимости, будемgetFetchUrl
Зависимости функции передаются черезuseCallback
пакет на новыйgetFetchUrl
функция, тоuseEffect
просто зависеть отgetFetchUrl
Эта функция реализуетcount
косвенные зависимости.
Другими словами, мы используемuseCallback
БудуgetFetchUrl
функция подобранаuseEffect
внешний.
ЗачемuseCallback
СравниватьcomponentDidUpdate
лучше использовать
Вспомните паттерн Class Component, как мы повторно получаем данные при изменении параметров функции:
class Parent extends Component {
state = {
count: 0,
step: 0
};
fetchData = () => {
const url =
"https://v?query=" + this.state.count + "&step=" + this.state.step;
};
render() {
return <Child fetchData={this.fetchData} count={count} step={step} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
if (
this.props.count !== prevProps.count &&
this.props.step !== prevProps.step // 别漏了!
) {
this.props.fetchData();
}
}
render() {
// ...
}
}
Вышеприведенный код должен быть знаком тем, кто часто использует Class Component, но выявленные проблемы не малы.
нам нужно понятьprops.count
props.step
одеялоprops.fetchData
используется функция, поэтому вcomponentDidUpdate
Когда выясняется, что эти два параметра изменились, запускается повторная выборка.
Но вопрос в том, не слишком ли высока цена этого понимания? если родительская функцияfetchData
Это не я написал, откуда я знаю, что это зависит от него, не читая исходный кодprops.count
а такжеprops.step
Шерстяная ткань?Серьезнее, если однаждыfetchData
полагаться наparams
Этот параметр нижестоящей функции понадобится всем вcomponentDidUpdate
переопределить эту логику, иначеparams
не будет повторно загружаться при изменении.可以想象,这种方式维护成本巨大,甚至可以说几乎无法维护。
Функциональный компонент думать об этом! Попробуйте провести только что упомянутыеuseCallback
Решать проблему:
function Parent() {
const [ count, setCount ] = useState(0);
const [ step, setStep ] = useState(0);
const fetchData = useCallback(() => {
const url = 'https://v/search?query=' + count + "&step=" + step;
}, [count, step])
return (
<Child fetchData={fetchData} />
)
}
function Child(props) {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
}
Видно, что когдаfetchData
После изменения зависимости нажмите клавишу сохранения,eslint-plugin-react-hooksОбновленные зависимости будут добавлены автоматически,Нисходящий код не нужно вносить никаких изменений, нижестоящему нужно только заботиться о зависимостях.fetchData
Этой функции достаточно.Что касается того, от чего зависит эта функция, то она инкапсулирована вuseCallback
Затем его упаковали и передали.
Не только решает проблему обслуживания, но и дляПока параметры меняются, повторно выполнять определенную логику, которая особенно подходит для использованияuseEffect
Действительно, размышление о проблемах с таким мышлением делает ваш код более «умным», а мышление с разделенными жизненными циклами сделает ваш код фрагментированным и легко упустит все виды возможностей.
useEffect
Абстракция бизнеса очень удобна.Автор приводит несколько примеров:
- зависимости являются параметрами запроса, тогда
useEffect
Если параметр запроса изменится, список будет автоматически загружен и обновлен. Обратите внимание, что мы изменили время выборки со стороны триггера на сторону получателя. - Когда список будет обновлен, повторно зарегистрируйте событие ответа на перетаскивание. Точно так же зависимым параметром является список.Пока список изменяется, реакция перетаскивания будет повторно инициализирована, так что мы можем уверенно изменять список, не беспокоясь о сбое события перетаскивания.
- Пока определенные данные в потоке данных изменяются, заголовок страницы изменяется синхронно. Точно так же нет необходимости изменять заголовок каждый раз при изменении данных, но
useEffect
"слушает" изменения в данных, что является"Инверсия контроля"мышление.
Сказав так много, его сущность все еще используетсяuseCallback
Извлеките функцию независимо, чтобыuseEffect
внешний.
Тогда думай дальше,Можно ли абстрагировать функцию от всего компонента?
Это также возможно, но требует гибкого использования пользовательских реализаций хуков.
Извлечь функцию за пределы компонента
с вышеуказаннымfetchData
Функция в качестве примера, если вы хотите извлечь снаружи весь компонент, это не использоватьuseCallback
Это делается, но для этого используются пользовательские хуки:
function useFetch(count, step) {
return useCallback(() => {
const url = "https://v/search?query=" + count + "&step=" + step;
}, [count, step]);
}
Как видите, мы будемuseCallback
Упаковано и перенесено на кастомный крючокuseFetch
, то для достижения того же эффекта в функции требуется только одна строка кода:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const fetch = useFetch(count, step); // 封装了 useFetch
useEffect(() => {
fetch();
}, [fetch]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>setCount {count}</button>
<button onClick={() => setStep(c => c + 1)}>setStep {step}</button>
<button onClick={() => setOther(c => c + 1)}>setOther {other}</button>
</div>
);
}
По мере того, как пользоваться им станет удобнее, мы можем сосредоточиться на производительности. Наблюдение показывает, что,count
а такжеstep
часто меняется, и каждое изменение приводит кuseFetch
серединаuseCallback
Изменения зависимостей, которые, в свою очередь, вызывают перегенерацию функции. Однако на самом деле нет необходимости перегенерировать такую функцию каждый раз, повторная генерация функции приведет к большим потерям производительности.
Другой пример можно увидеть более четко:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const [other, setOther] = useState(0);
const drag = useDraggable(count, step); // 封装了拖拽函数
}
Предположим, мы используемSortablejsДля выполнения мониторинга перетаскиванием в определенной области потеря производительности этой функции очень велика, когда она каждый раз выполняется повторно.Однако эта функция может сообщать только о некоторых журналах, поэтому она зависит от тех, которые фактически не используются.count
step
Переменная:
function useDraggable(count, step) {
return useCallback(() => {
// 上报日志
report(count, step);
// 对区域进行初始化,非常耗时
// ... 省略耗时代码
}, [count, step]);
}
В этом случае функциональные зависимости особенно неразумны.Хотя изменения зависимости должны инициировать повторное выполнение функции, если стоимость повторного выполнения функции очень высока, а зависимость является лишь необязательным украшением, выигрыш перевешивает потерю.
Используйте Ref, чтобы убедиться, что требующие много времени зависимости функций остаются неизменными.
Один из способов — преобразовать зависимость в Ref:
function useFetch(count, step) {
const countRef = useRef(count);
const stepRef = useRef(step);
useEffect(() => {
countRef.current = count;
stepRef.current = step;
});
return useCallback(() => {
const url =
"https://v/search?query=" + countRef.current + "&step=" + stepRef.current;
}, [countRef, stepRef]); // 依赖不会变,却能每次拿到最新的值
}
Этот метод довольно сложен.** Отделите область, требующую обновления, от области, занимающей много времени, а затем предоставьте содержимое, которое нужно обновить, области, требующей много времени, с помощью ссылки, чтобы добиться оптимизации производительности.
Однако это относительно много для функции функции, и существует более распространенная практика решения таких задач.
Универсальные пользовательские хуки решают проблему повторного создания экземпляра функции
мы можем использоватьuseRef
Вместо этого создайте собственный хукuseCallback
,Когда значение, от которого он зависит, изменяется, обратный вызов не будет выполняться повторно, но получит самое последнее значение!
Этот волшебный крючок записывается следующим образом:
function useEventCallback(fn, dependencies) {
const ref = useRef(null);
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
Еще раз ощутите всемогущество кастомных хуков.
Сначала посмотрите на этот абзац:
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
когдаfn
Когда функция обратного вызова изменяется,ref.current
Повторить на последнююfn
Эта логика вполне удовлетворительна. Дело в том, что, опираясь наdependencies
изменить, а также повторноref.current
назначить, в это времяfn
Внутреннийdependencies
Значение актуально, и следующий фрагмент кода:
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
Он выполняется только один раз (ссылка ref не изменится), поэтому он может возвращаться каждый разdependencies
является последнимfn
,а такжеfn
повторно выполняться не будет.
Предположим, у нас естьuseEventCallback
Переданная функция обратного вызова называется X,Смысл этого кода в том, что при закрытии каждого рендеринга callback-функция X всегда получает ту, что в последнем замыкании Renderer, поэтому зависимое значение всегда самое последнее, и функция не будет переинициализирована.
React Эта парадигма официально объявлена устаревшей, поэтому для этого сценария используйте
useReducer
, передать функцию черезdispatch
называется в.ты помнишь?dispatch
Это своего рода черная магия, позволяющая обойти зависимости, о которых мы упоминали в разделе «Что такое useReducer».
При использовании функционального компонента вы также постепенно заботитесь о производительности функции, и это здорово. Затем следующий фокус, естественно, должен сосредоточиться на производительности рендеринга.
PureRender с памяткой
В компоненте функции компонент классаPureComponent
Эквивалентная концепцияReact.memo
, давайте познакомимсяmemo
Применение:
const Child = memo((props) => {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
})
использоватьmemo
Обернутый компонент будет перерисовываться для каждогоprops
Элементы сравниваются неглубоко, и повторный рендеринг не запускается, если ссылка не изменилась. такmemo
Отличный инструмент для оптимизации производительности.
Нижеследующее представляет собой, казалось бы,memo
Его сложно использовать, но после того, как вы действительно поймете его, вы обнаружите, что он на самом деле лучше, чемmemo
Улучшенные функции оптимизации рендеринга:useMemo
.
Локальный PureRender с useMemo
в сравнении сReact.memo
этот инопланетянин,React.useMemo
А вот серьезный официальный Хук:
const Child = (props) => {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return useMemo(() => (
// ...
), [props.fetchData])
}
Как видите, мы используемuseMemo
Оберните код рендеринга так, чтобы даже если функцияChild
потому чтоprops
Изменения вносятся повторно, пока функция рендеринга используетprops.fetchData
Без изменений, без повторного рендеринга.
нашел здесьuseMemo
Первое преимущество:Более детальный оптимизированный рендеринг.
Так называемый мелкозернистый оптимизированный рендеринг относится к функцииChild
в целом можно использоватьA
,B
дваprops
, при рендеринге используется толькоB
, затем используйтеmemo
строить планы,A
Изменения могут привести к рендерингу и использованиюuseMemo
плана не будет.
а такжеuseMemo
Преимущества на этом не заканчиваются, давайте оставим здесь предзнаменование. Давайте сначала рассмотрим новую проблему: когда параметров становится все больше и больше, используйтеprops
Передача функций и значений между компонентами очень многословна:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const fetchData = useFetch(count, step);
return <Child fetchData={fetchData} setCount={setCount} setStep={setStep} />;
}
несмотря на то чтоChild
в состоянии пройтиmemo
илиuseMemo
Для оптимизации, ** но когда программа сложна, может быть несколько функций, общих для всех функциональных компонентов **, тогда необходим новый хук:useContext
прийти на помощь.
Пакетная прозрачная передача с использованием Context
В функциональном компоненте вы можете использоватьReact.createContext
Создайте контекст:
const Store = createContext(null);
вnull
начальное значение, обычно устанавливаемое наnull
не важно. Далее есть два шага, которые нужно использовать на корневом узле.Store.Provider
Внедрить и использовать официальный хук на дочерних узлахuseContext
Получить введенные данные:
использовать в корневом узлеStore.Provider
впрыск:
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const fetchData = useFetch(count, step);
return (
<Store.Provider value={{ setCount, setStep, fetchData }}>
<Child />
</Store.Provider>
);
}
использовать на дочерних узлахuseContext
Получить введенные данные (т. е. получитьStore.Provider
изvalue
):
const Child = memo((props) => {
const { setCount } = useContext(Store)
function onClick() {
setCount(count => count + 1)
}
return (
// ...
)
})
Таким образом, нет необходимости прозрачно передавать параметры между каждой функцией, а общедоступные функции можно разместить в контексте.
Но когда функций больше,Provider
изvalue
станет очень раздутым, мы можем объединить ранее упомянутыеuseReducer
решить эту проблему.
использоватьuseReducer
Тоньше для передачи контента в контексте
использоватьuseReducer
, все функции обратного вызова вызываютсяdispatch
готово, затем просто передайте Contextdispatch
Функция подойдет:
const Store = createContext(null);
function Parent() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });
return (
<Store.Provider value={dispatch}>
<Child />
</Store.Provider>
);
}
Здесь, является ли это корневым узломProvider
, или вызовы дочерних элементов очень освежают:
const Child = useMemo((props) => {
const dispatch = useContext(Store)
function onClick() {
dispatch({
type: 'countInc'
})
}
return (
// ...
)
})
Вы можете вскоре понять, чтоstate
также прошлоProvider
Не лучше ли сделать инъекцию? Да, но здесь важно знать о возможных проблемах с производительностью.
Будуstate
Также поместите в контекст
слегка модифицированный, т.state
Он также помещен в Контекст, что очень удобно для присвоения и получения значения!
const Store = createContext(null);
function Parent() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });
return (
<Store.Provider value={{ state, dispatch }}>
<Count />
<Step />
</Store.Provider>
);
}
правильноCount
Step
Для этих двух подэлементов нам нужно быть осторожными.Если мы реализуем эти два подэлемента следующим образом:
const Count = memo(() => {
const { state, dispatch } = useContext(Store);
return (
<button onClick={() => dispatch("incCount")}>incCount {state.count}</button>
);
});
const Step = memo(() => {
const { state, dispatch } = useContext(Store);
return (
<button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
);
});
результат:независимо от того, нажмитеincCount
ещеincStep
, запустит средства визуализации обоих компонентов одновременно.
Проблема в:memo
Мы можем стоять только в самом внешнем слое, иuseContext
Вставка данных происходит внутри функции и будетобходитьmemo
.
при срабатыванииdispatch
Привести кstate
изменяется, когда все используетсяstate
Все компоненты будут вынуждены обновляться. В это время, если вы хотите оптимизировать количество визуализации, вы можете вынуть толькоuseMemo
!
useMemo
СотрудничатьuseContext
использоватьuseContext
компонент, если он не используется сам по себеprops
, вы можете полностью использоватьuseMemo
заменятьmemo
Проведите оптимизацию производительности:
const Count = () => {
const { state, dispatch } = useContext(Store);
return useMemo(
() => (
<button onClick={() => dispatch("incCount")}>
incCount {state.count}
</button>
),
[state.count, dispatch]
);
};
const Step = () => {
const { state, dispatch } = useContext(Store);
return useMemo(
() => (
<button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
),
[state.step, dispatch]
);
};
В этом примере, нажав соответствующую кнопку,Только используемые компоненты будут перерендерены, и эффект будет таким, как ожидалось.комбинироватьeslint-plugin-react-hooksИспользование плагина, дажеuseMemo
Второй параметр зависимостей — все автодополнение.
Читая здесь, я не знаю, думаешь ли ты об этом.ReduxизConnect
?
Давайте сравнимConnect
а такжеuseMemo
, найдет поразительное сходство.
Обычный компонент Redux:
const mapStateToProps = state => (count: state.count);
const mapDispatchToProps = dispatch => dispatch;
@Connect(mapStateToProps, mapDispatchToProps)
class Count extends React.PureComponent {
render() {
return (
<button onClick={() => this.props.dispatch("incCount")}>
incCount {this.props.count}
</button>
);
}
}
Обычный функциональный компонент:
const Count = () => {
const { state, dispatch } = useContext(Store);
return useMemo(
() => (
<button onClick={() => dispatch("incCount")}>
incCount {state.count}
</button>
),
[state.count, dispatch]
);
};
Эффект от этих двух фрагментов кода абсолютно одинаков.Помимо того, что он более лаконичен, функциональный компонент имеет большее преимущество:Полностью автоматический вывод зависимостей.
Одной из причин появления хуков является облегчение статического анализа зависимостей и упрощение стоимости использования неизменяемых потоков данных.
мы видимConnect
Сценарий:
Поскольку вы не знаете, какие данные используются дочерним компонентом, вам необходимоmapStateToProps
Напишите его заранее, и когда вам нужно использовать новые переменные в потоке данных, он недоступен в компоненте, мы должны вернуться кmapStateToProps
Добавьте эту зависимость и вернитесь к компоненту, чтобы использовать ее.
а такжеuseContext
+ useMemo
Сценарий:
из-за впрыскаstate
Он заполнен. Вы можете использовать все, что хотите в функции рендеринга. Когда вы нажимаете клавишу сохранения,eslint-plugin-react-hooksпройдет статический анализ, вuseMemo
Второй параметр автоматически заполняет внешние переменные, используемые в коде, такие какstate.count
,dispatch
.
Кроме того, можно обнаружить, что Context очень похож на Redux, поэтому как использовать асинхронную выборку, реализованную асинхронным промежуточным программным обеспечением в режиме компонента класса.useReducer
сделай это? Ответ таков: это невозможно сделать.
Конечно, дело не в том, что функциональный компонент не может обеспечить асинхронную выборку, а в том, что используемый инструмент неверен.
Обработка побочных эффектов с помощью пользовательских хуков
Например, в приведенном выше сценарии асинхронной выборки наилучшей практикой в функциональном компоненте является инкапсуляция его в виде пользовательского хука:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
const result = await axios(url);
if (!didCancel) {
dispatch({ type: "FETCH_SUCCESS", payload: result.data });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: "FETCH_FAILURE" });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const doFetch = url => setUrl(url);
return { ...state, doFetch };
};
Как видите, пользовательский хук имеет полный жизненный цикл, мы можем инкапсулировать процесс выборки и выставлять только состояние — загружается ли он:isLoading
Не удалось получить:isError
данные:data
.
Очень удобно использовать в компонентах:
function App() {
const { data, isLoading, isError } = useDataApi("https://v", {
showLog: true
});
}
Если это значение необходимо сохранить в потоке данных, совместно используемом всеми компонентами, мы можем объединитьuseEffect
а такжеuseReducer
:
function App(props) {
const { dispatch } = useContext(Store);
const { data, isLoading, isError } = useDataApi("https://v", {
showLog: true
});
useEffect(() => {
dispatch({
type: "updateLoading",
data,
isLoading,
isError
});
}, [dispatch, data, isLoading, isError]);
}
На этом вводная концепция функционального компонента завершена, и, наконец, добавлено пасхальное яйцо: как работать с DefaultProps функционального компонента?
Как установить defaultprops для функционального компонента?
Этот вопрос может показаться простым, но это не так. У нас есть как минимум два способа присвоить значения DefaultProps функционального компонента, которые объясняются ниже.
Прежде всего, для компонента класса существует только один способ написания DefaultProps, с которым согласны все:
class Button extends React.PureComponent {
defaultProps = { type: "primary", onChange: () => {} };
}
Однако в функциональном компоненте это все виды вещей.
Используйте функции ES6 для назначения значений на этапе определения параметра
function Button({ type = "primary", onChange = () => {} }) {}
Этот подход может показаться элегантным, но он таит в себе большую скрытую опасность:пропущенныйprops
Ссылка отличается на каждом визуализации.
Посмотрите на эту сцену:
const Child = memo(({ type = { a: 1 } }) => {
useEffect(() => {
console.log("type", type);
}, [type]);
return <div>Child</div>;
});
если толькоtype
Ссылка неизменна,useEffect
не будет выполняться часто. Обновление через родительский элемент теперь приводит кChild
После обновления мы обнаружили, чтоКаждый рендер будет распечатывать журнал, а это значит, что каждый раз, когда вы рендерите,type
Цитаты разные.
Есть менее элегантный способ решить эту проблему:
const defaultType = { a: 1 };
const Child = ({ type = defaultType }) => {
useEffect(() => {
console.log("type", type);
}, [type]);
return <div>Child</div>;
};
На этом этапе, если родительский элемент постоянно обновляется, журнал будет распечатан только один раз, потому чтоtype
Ссылки те же.
Мы используем DefaultProps. Первоначальное намерение должно состоять в том, чтобы надеяться, что ссылка значения по умолчанию такая же,Если вы не хотите поддерживать ссылку на переменную отдельно, вы также можете позаимствовать встроенный в ReactdefaultProps
метод решения.
Используйте встроенные решения React
Встроенное решение React может лучше решить проблему частых изменений ссылок:
const Child = ({ type }) => {
useEffect(() => {
console.log("type", type);
}, [type]);
return <div>Child</div>;
};
Child.defaultProps = {
type: { a: 1 }
};
В приведенном выше примере, если родительский элемент постоянно обновляется, журнал будет распечатан только один раз.
Поэтому рекомендуется использовать встроенное решение React для значения параметра функционального компонента по умолчанию, поскольку чисто функциональное решение не способствует сохранению ссылки неизменной.
Наконец, добавьте классический случай родительского компонента «яма» дочернего компонента.
Не связывайтесь с подкомпонентами
Мы делаем кнопку, которая нажимается и накапливается как родительский компонент, тогда родительский компонент будет обновляться после каждого нажатия:
function App() {
const [count, forceUpdate] = useState(0);
const schema = { b: 1 };
return (
<div>
<Child schema={schema} />
<div onClick={() => forceUpdate(count + 1)}>Count {count}</div>
</div>
);
}
Кроме того, мы будемschema = { b: 1 }
Передайте его к детскому компоненту, это большая дыра, похороненная.
Код дочернего компонента выглядит следующим образом:
const Child = memo(props => {
useEffect(() => {
console.log("schema", props.schema);
}, [props.schema]);
return <div>Child</div>;
});
пока родительprops.schema
Изменения распечатают журнал. В результате, естественно, каждый раз, когда родительский компонент обновляется, дочерний компонент будет печатать журнал, т.е.Подсборка[props.schema]
Полностью аннулирован, потому что ссылки продолжают меняться.
фактическиПодкомпоненты заботятся о значениях, а не о ссылках, поэтому одним из решений является переопределение зависимостей подкомпонента:
const Child = memo(props => {
useEffect(() => {
console.log("schema", props.schema);
}, [JSON.stringify(props.schema)]);
return <div>Child</div>;
});
Это гарантирует, что дочерние компоненты визуализируются только один раз.
Но настоящим виновником является родительский компонент, нам нужно использовать Ref для оптимизации родительского компонента:
function App() {
const [count, forceUpdate] = useState(0);
const schema = useRef({ b: 1 });
return (
<div>
<Child schema={schema.current} />
<div onClick={() => forceUpdate(count + 1)}>Count {count}</div>
</div>
);
}
такschema
Ссылка всегда может оставаться неизменной. Если вы прочитаете эту статью полностью, вы сможете полностью понять первый пример.schema
— это новая ссылка в каждом моментальном снимке рендеринга, а в случае Refschema
В каждом моментальном снимке рендеринга есть только одна уникальная ссылка.
3. Резюме
Итак, вы начинаете работать с функциональным компонентом?
В этом интенсивном чтении остался вопрос: в каких деталях легко допустить ошибку в процессе разработки функционального компонента?
Адрес обсуждения:Интенсивное чтение «Введение в функциональный компонент» · Выпуск №157 · dt-fe/weekly
Если вы хотите принять участие в обсуждении, пожалуйста,кликните сюда, с новыми темами каждую неделю, выходящими по выходным или понедельникам. Интерфейс интенсивного чтения — поможет вам отфильтровать надежный контент.
Сфокусируйся наАккаунт WeChat для интенсивного чтения в интерфейсе
special Sponsors
Заявление об авторских правах: Бесплатная перепечатка - некоммерческая - не производная - сохранить авторство (Лицензия Creative Commons 3.0)