Вы все еще беспокоитесь о том, использовать ли компоненты без состояния (функция) или компоненты с состоянием (класс)? - С хуками вам больше не нужно писать класс, все ваши компоненты будут функциями.
Вы все еще пытаетесь понять, какую функцию хука жизненного цикла использовать? - С хуками функции хуков жизненного цикла могут быть отброшены в первую очередь.
Вас все еще смущает указатель this в компоненте? —— Поскольку класс был утерян, где это? Впервые в жизни вам больше не придется с этим сталкиваться.
Таким образом, не будет преувеличением сказать, что React Hooks — самая популярная новая функция года. Если вы также заинтересованы в реакции или используете реакцию для разработки проекта, пообещайте мне, пожалуйста, уделите хотя бы 30 минут, чтобы прочитать эту статью?
Один из самых простых крючков
Сначала давайте посмотрим на простой компонент с отслеживанием состояния:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Давайте посмотрим на версию после использования хуков:
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Не проще ли! можно увидеть,Example
Она становится функцией, но у этой функции есть свое состояние (count), а также она может обновлять свое состояние (setCount). Что делает эту функцию такой удивительной, так это то, что она внедряет хук--useState
, именно этот хук делает нашу функцию функцией с состоянием.
КромеuseState
В дополнение к этому крючку, есть много других крючков, таких какuseEffect
предоставляет что-то вродеcomponentDidMount
такие функции, как хуки жизненного цикла,useContext
Предоставляет контекстные (контекстные) функции и так далее.
Хуки — это, по сути, специальные функции, которые внедряют некоторые специальные функции в ваши функциональные компоненты. Хм? Звучит немного похоже на сильно оклеветанных Миксинов? Миксины возвращаются к жизни в React? Конечно нет, о разнице поговорим позже. В общем, цель этих хуков — позволить вам перестать писать классы и позволить функциям править миром.
Почему React хочет заниматься хуками?
Попытка повторно использовать компонент с отслеживанием состояния слишком сложна!
Все мы знаем, что основная идея реакции состоит в том, чтобы разбить страницу на кучу независимых повторно используемых компонентов и соединить эти компоненты в однонаправленном потоке данных сверху вниз. Но если вы используете React в большом рабочем проекте, вы обнаружите, что многие компоненты React в вашем проекте на самом деле утомительны и сложны для повторного использования. Особенно те компоненты, которые написаны как классы, сами содержат состояние, поэтому повторное использование таких компонентов становится очень проблематичным.
До этого, какова официальная рекомендация по решению этой проблемы? ответ:Рендер реквизита такжеКомпоненты высшего порядка. Мы можем кратко рассмотреть эти два режима, немного отойдя от темы.
Свойства рендеринга относятся к использованию реквизита, значение которого является функцией для передачи узлов или компонентов, которые необходимо визуализировать динамически. Как вы можете видеть в приведенном ниже коде, нашDataProvider
Компоненты содержат весь код, связанный с состоянием, в то время какCat
Компоненты могут быть простым компонентом отображения, поэтомуDataProvider
могут быть повторно использованы по отдельности.
import Cat from 'components/cat'
class DataProvider extends React.Component {
constructor(props) {
super(props);
this.state = { target: 'Zac' };
}
render() {
return (
<div>
{this.props.render(this.state)}
</div>
)
}
}
<DataProvider render={data => (
<Cat target={data.target} />
)}/>
Хоть этот режим и называется Render Props, это не означает, что обязательно нужно использовать реквизит под названием render.Принято писать следующее:
...
<DataProvider>
{data => (
<Cat target={data.target} />
)}
</DataProvider>
Лучше понятна концепция компонентов высшего порядка, если говорить прямо, то функция принимает компонент в качестве параметра и после серии обработок, наконец, возвращает новый компонент. См. пример кода ниже,withUser
Функция — это компонент более высокого порядка, который возвращает новый компонент, обладающий функциональностью, которую он предоставляет для получения информации о пользователе.
const withUser = WrappedComponent => {
const user = sessionStorage.getItem("user");
return props => <WrappedComponent user={user} {...props} />;
};
const UserPage = props => (
<div class="user-container">
<p>My name is {props.user}!</p>
</div>
);
export default withUser(UserPage);
Два приведенных выше шаблона выглядят довольно хорошо, и многие библиотеки также используют этот шаблон, например, наш часто используемый React Router. Но если мы внимательно посмотрим на эти два шаблона, то увидим, что они добавляют иерархии к нашему коду. Самый интуитивно понятный вариант, откройте devtool, чтобы увидеть, не преувеличена ли ваша вложенность на уровне компонентов. А пока давайте вернемся к примеру с хуками, который мы привели в предыдущем разделе, более ли он лаконичен и в нем нет избыточного уровня вложенности? Запишите все нужные функции в многократно используемый пользовательский хук.Когда ваш компонент хочет использовать какую функцию, вы можете напрямую вызвать этот хук в компоненте.
Логика в функции хука жизненного цикла слишком запутана!
Обычно мы хотим, чтобы функция делала только одну вещь, но наши хуки жизненного цикла обычно делают много вещей одновременно. Например, нам нужноcomponentDidMount
Инициируйте запрос ajax для получения данных, привязки некоторых прослушивателей событий и т. д. В то же время иногда нам также необходимоcomponentDidUpdate
Сделайте то же самое снова. Когда проект усложняется, код этой части также становится менее интуитивным.
классы такие запутанные!
Когда мы используем класс для создания реагирующих компонентов, есть еще одна очень неприятная вещь, а именно проблема указания. Чтобы убедиться, что указание this правильное, мы часто пишем такой код:this.handleClick = this.handleClick.bind(this)
, или такой код:<button onClick={() => this.handleClick(e)}>
. Как только мы случайно забудем это привязать, последуют различные баги, что очень хлопотно.
Есть еще одна вещь, которая меня очень беспокоит. Как я уже говорил в предыдущих статьях серии React, старайтесь как можно чаще писать свои компоненты как компоненты без состояния, потому что их легче повторно использовать и тестировать независимо. Однако часто мы используем функцию для написания лаконичного и совершенного компонента без состояния, а позже, из-за изменения спроса, этот компонент должен иметь свое собственное состояние, и мы должны изменить функцию на класс.
В этом контексте родился Hooks!
Что такое хуки состояния?
Возвращаясь к примеру, который мы использовали в начале, давайте разберем его, чтобы увидеть, что делают хуки состояния:
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
объявить переменную состояния
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
useState
Это функция-ловушка, которая поставляется с реакцией, и ее функция заключается в объявлении переменных состояния.useState
Параметр, который получает эта функция, является нашим начальным состоянием, и она возвращает массив с первым значением массива.[0]
item - текущее текущее значение состояния, первое[1]
Элементы — это функции-методы, которые могут изменять значения состояния.
Итак, что мы делаем, так это фактически объявляем переменную состояния count, устанавливаем ее начальное значение в 0 и предоставляем функцию setCount, которая может изменять count.
Приведенное выше выражение заимствовано из деструктуризации массива es6 (array destructuring), это может сделать наш код более лаконичным. Если вы не знаете, как его использовать, вы можете сначала прочитать мою статью.Освойте основной контент ES6/ES2015 за 30 минут (часть 1).
Без деструктуризации массива это можно записать следующим образом. На самом деле, деструктуризация массива — очень затратная вещь, и если вы воспользуетесь следующим способом записи или вместо этого примените деструктуризацию объектов, то производительность значительно улучшится. Конкретно можно перейти к анализу этой статьиArray destructuring for multi-value returns (in light of React hooks), мы не будем здесь его подробно раскрывать, просто следуем официальной рекомендации использовать деструктуризацию массива.
let _useState = useState(0);
let count = _useState[0];
let setCount = _useState[1];
прочитать значение состояния
<p>You clicked {count} times</p>
Это супер просто? Поскольку наш счетчик состояний — это просто переменная, нам больше не нужно записывать его как{this.state.count}
Вот и все.
обновить состояние
<button onClick={() => setCount(count + 1)}>
Click me
</button>
Когда пользователь нажимает кнопку, мы вызываем функцию setCount, которая получает новое измененное значение состояния. Следующее, что осталось сделать, это повторно отобразить наш компонент Example и использовать обновленное новое состояние, которое равно count=1. Здесь мы должны остановиться и немного подумать.Пример тоже обычная функция в природе.Почему он может помнить предыдущее состояние?
жизненно важный вопрос
Здесь мы находим проблему, вообще говоря, переменная, которую мы объявляем в функции, будет уничтожена, когда функция завершится (мы не будем здесь рассматривать замыкание и другие ситуации), например, рассмотрим следующий пример:
function add(n) {
const result = 0;
return result + 1;
}
add(1); //1
add(1); //1
Сколько бы раз мы ни вызывали функцию добавления, результатом всегда будет 1. Потому что каждый раз, когда мы вызываем add, переменная результата начинается с начального значения 0. Так почему же приведенная выше функция примера принимает значение состояния последнего выполнения в качестве начального значения каждый раз, когда она выполняется? Ответ таков: реакция помогает нам помнить. Что касается того, какой механизм React использует для запоминания, мы можем подумать об этом еще раз.
Что делать, если компонент имеет несколько значений состояния?
Во-первых, useState можно вызывать несколько раз, поэтому мы можем написать его так:
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
Во-вторых, начальное значение, получаемое useState, не оговаривает, что это должен быть простой тип данных, такой как строка/число/логическое значение, которое может полностью принимать объекты или массивы в качестве параметров. Единственное, что следует отметить, это то, что до нашегоthis.setState
Что он делает, так это возвращает новое состояние после слияния состояний, в то время какuseState
Это непосредственная замена старого состояния и возврат к новому состоянию. Наконец, react также предоставляет нам хук useReducer, если вы предпочитаете решение для управления состоянием в стиле redux.
Из функции ExampleWithManyStates мы видим, что независимо от того, сколько раз вызывается useState, они не зависят друг от друга. Это очень важно. Почему ты это сказал?
На самом деле, когда мы смотрим на «форму» ловушки, она немного похожа на решение Mixins, которое ранее было официально отклонено и которое обеспечивает возможность «внедрения подключаемых функций». Причина, по которой примеси запрещены, заключается в том, что механизм примесей позволяет нескольким примесям совместно использовать пространство данных объекта, поэтому трудно гарантировать, что состояния, от которых зависят разные примеси, не конфликтуют.
А теперь наш хук, с одной стороны, он используется непосредственно в функции, а не в классе, с другой стороны, каждый хук независим друг от друга, и разные компоненты вызывают один и тот же хук для обеспечения независимости своих состояний . Это существенное различие между ними.
Как реакция гарантирует, что несколько useStates не зависят друг от друга?
Давайте посмотрим на приведенный выше пример ExampleWithManyStates: мы вызываем useState три раза, каждый раз, когда параметр, который мы передаем, является только значением (например, 42, «банан»), мы не сообщаем react, какой ключ соответствует этим значениям, так как работает ли реакция?Как убедиться, что эти три useStates находят свое соответствующее состояние?
Ответ заключается в том, что реакция основана на порядке появления useState. Давайте рассмотрим подробно:
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
useState('banana'); //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
useState([{ text: 'Learn Hooks' }]); //...
Предположим, мы меняем код:
let showFruit = true;
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
Таким образом,
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
// useState('banana');
useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错
Ввиду этого реакция предусматривает, что мы должны писать хуки на самом внешнем уровне функции, а не в условных операторах, таких как ifelse, чтобы гарантировать согласованность порядка выполнения хуков.
Что такое хуки эффектов?
Мы добавляем новую функцию в пример из предыдущего раздела:
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 更新文档的标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Давайте сравним, что бы мы написали, если бы не было крючков?
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Компоненты с отслеживанием состояния, которые мы пишем, обычно имеют много побочных эффектов, таких как инициирование ajax-запросов для получения данных, добавление некоторого контроля регистрации и отмены регистрации, ручное изменение dom и так далее. Мы писали эти функции побочных эффектов в обработчиках функций жизненного цикла ранее, таких как componentDidMount, componentDidUpdate и componentWillUnmount. И теперь useEffect представляет собой набор этих декларативных периодических перехватчиков функций. Это один к трем.
В то же время, ведь упомянутые крючки можно использовать многократно и независимо друг от друга. Таким образом, наш разумный подход состоит в том, чтобы дать каждому побочному эффекту отдельный хук useEffect. Таким образом, эти побочные эффекты больше не накапливаются в ловушках жизненного цикла, и код становится чище.
Что делает useEffect? ###
Еще раз разберем логику следующего кода:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
Во-первых, мы объявляем переменную состоянияcount
, установите его начальное значение равным 0. Затем мы говорим реагировать, что этот наш компонент имеет побочный эффект. мы даемuseEffect
Хуку передается анонимная функция, и эта анонимная функция является нашим побочным эффектом. В этом примере нашим побочным эффектом является вызов API браузера для изменения заголовка документа. Когда React собирается отображать наш компонент, он сначала запоминает побочные эффекты, которые мы используем. После того, как react обновляет DOM, он по очереди выполняет функции побочных эффектов, которые мы определили.
Несколько замечаний: Во-первых, функция, переданная в useEffect, вызывается реакцией один раз для первого и каждого последующего рендеринга. Раньше мы использовали две функции цикла объявления для представления первого рендеринга (componentDidMount) и повторного рендеринга, вызванного последующими обновлениями (componentDidUpdate).
Во-вторых, выполнение функций побочных эффектов, определенных в useEffect, не помешает браузеру обновить представление, что означает, что эти функции выполняются асинхронно, в то время как предыдущий код в componentDidMount или componentDidUpdate выполняется синхронно. Такое расположение приемлемо для большинства побочных эффектов, но есть и исключения. Например, иногда нам нужно вычислить размер элемента на основе DOM перед повторным рендерингом. В настоящее время мы хотим, чтобы этот повторный рендеринг происходил синхронно. , что означает, что это происходит до того, как браузер фактически отрисует страницу.
Как useEffect устраняет некоторые побочные эффекты
Такой сценарий встречается очень часто, когда мы добавляем регистрацию в componentDidMount, мы должны сразу очистить добавленную нами регистрацию в componentWillUnmount, то есть до того, как компонент будет разрегистрирован, иначе возникнет проблема утечки памяти.
Как это очистить? Пусть функция побочного эффекта, которую мы передаем в useEffect, возвращает новую функцию. Эта новая функция будет выполнена после следующего повторного рендеринга компонента. Этот шаблон распространен в некоторых реализациях шаблона pubsub. См. пример ниже:
import { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
Здесь есть один момент, на который стоит обратить внимание! Этот режим отвязки отличается от componentWillUnmount. componentWillUnmount будет выполняться только один раз, прежде чем компонент будет уничтожен, а функция в useEffect будет выполняться один раз каждый раз, когда компонент визуализируется, включая функцию очистки, возвращаемую функцией побочного эффекта, которая будет выполняться снова. Итак, давайте рассмотрим следующий вопрос.
Почему функция побочного эффекта должна выполняться каждый раз при обновлении компонента?
Давайте сначала посмотрим на предыдущий шаблон:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
Очень понятно, регистрируемся в componentDidMount, а потом очищаем регистрацию в componentWillUnmount. Но если на этот разprops.friend.id
Что, если он изменится? Нам пришлось добавить еще один компонентDidUpdate, чтобы справиться с этой ситуацией:
...
componentDidUpdate(prevProps) {
// 先把上一个friend.id解绑
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 再重新注册新但friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
...
видеть это? Это громоздко, но у нас нет этой проблемы с useEffect, потому что он будет выполняться повторно каждый раз при обновлении компонента. Итак, порядок выполнения кода следующий:
1.页面首次渲染
2.替friend.id=1的朋友注册
3.突然friend.id变成了2
4.页面重新渲染
5.清除friend.id=1的绑定
6.替friend.id=2的朋友注册
...
Как пропустить некоторые ненужные функции побочных эффектов
Согласно идее, изложенной в предыдущем разделе, явно неэкономично выполнять эти функции побочных эффектов каждый раз при повторном рендеринге. Как пропустить некоторые ненужные вычисления? Нам нужно только передать второй параметр в useEffect. Используйте второй параметр, чтобы заставить реакцию выполнять переданную нами функцию побочного эффекта (первый параметр) только при изменении значения этого параметра.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句
Когда мы передаем пустой массив [] в качестве второго параметра, это фактически эквивалентно выполнению только при первом рендеринге. То есть режим componentDidMount плюс componentWillUnmount. Однако такое использование может привести к ошибкам и должно использоваться с осторожностью.
Какие из них поставляются с EFFECT HOOKS?
В дополнение к useState и useEffect, выделенным выше, react также предоставляет нам много полезных хуков:
useContext useReducer useCallback useMemo useRef useImperativeMethods useMutationEffect useLayoutEffect
Я не буду представлять их по одному, вы можете сами ознакомиться с официальной документацией.
Как написать собственные хуки эффектов?
Почему мы должны сами писать хуки эффектов? Таким образом, мы можем извлечь многократно используемую логику и превратить ее в «заглушки», которые можно подключать и отключать по желанию. Какой компонент я хочу использовать, я его вставлю в какой компонент, так просто! Взгляните на полный пример, и вы поймете.
Например, мы можем извлечь функцию определения того, находится ли друг в сети, в компоненте FriendStatus, написанном выше, и создать хук useFriendStatus специально для определения того, находится ли идентификатор в сети.
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
В настоящее время компонент FriendStatus может быть сокращен как:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
Просто Идеально! Если в это время у нас есть список друзей, нам также нужно отобразить информацию о том, онлайн он или нет:
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Просто Потрясающе!
конец
Я не знаю, как вы себя чувствуете после прочтения всей статьи, или у вас есть какой-то угол зрения и вы думаете о крючках Добро пожаловать, чтобы обсудить вместе в области комментариев. Кроме того, если у вас есть планы сменить работу, в нашем отделе действительно не хватает людей, добро пожаловать в личные сообщения~ (Alibaba, базовый отдел в Шэньчжэне, Лазада, требуется три года или более опыта работы, вы можете написать мне или забрать свое резюме и скинуть Мне: zeqiang.wang@alibaba-inc.com)