Автор: @ Ocean Platform Front-End Team
предисловие
Недавно я нашел время, чтобы изучить систему хуков React, и обнаружил, что она действительно ароматная.
Мотивация (официальная)
- Трудно повторно использовать логику с отслеживанием состояния между компонентами.
- Сложные компоненты становятся трудными для понимания
- Классы путают людей и машины
- В большей степени в соответствии с пониманием FP, позиционирование самого компонента React — это функция, функция, которая поглощает данные и выплевывает пользовательский интерфейс.
Общий крючок
useState
const [state, setState] = useState(initialState)
- useState имеет один параметр, который может иметь любой тип данных и обычно используется как значение по умолчанию.
- Возвращаемое значение useState — это массив, первый параметр массива — это состояние, которое нам нужно использовать, а второй параметр — это setFn.
- полный пример
function Love() {
const [like, setLike] = useState(false)
const likeFn = () => (newLike) => setLike(newLike)
return (
<>
你喜欢我吗: {like ? 'yes' : 'no'}
<button onClick={likeFn(true)}>喜欢</button>
<button onClick={likeFn(false)}>不喜欢</button>
</>
)
}
О правилах использования:
- Вызывайте хуки только в функциях React;
- Не вызывайте хуки в циклах, условных выражениях или вложенных функциях. Давайте посмотрим на правило 2, почему это явление происходит, сначала посмотрите на состав крючков.
function mountWorkInProgressHook() {
// 注意,单个 hook 是以对象的形式存在的
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
/* 等价
let workInProgressHook = hooks
firstWorkInProgressHook = workInProgressHook
*/
} else {
workInProgressHook = workInProgressHook.next = hook;
}
// 返回当前的 hook
return workInProgressHook;
}
Каждый хук будет иметь следующий указатель.Объекты хука связаны в односвязный список.В то же время можно обнаружить, что нижний слой useState по-прежнему является useReducerПосмотрите, что произошло на этапе обновления
// ReactFiberHooks.js
const HooksDispatcherOnUpdate: Dispatcher = {
// ...
useState: updateState,
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
if (numberOfReRenders > 0) {
const dispatch = queue.dispatch;
if (renderPhaseUpdates !== null) {
// 获取Hook对象上的 queue,内部存有本次更新的一系列数据
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
// 获取更新后的state
do {
// useState 第一个参数会被转成 useReducer
const action = update.action;
newState = reducer(newState, action);
//按照当前链表位置更新数据
update = update.next;
} while (update !== null);
hook.memoizedState = newState;
// 返回新的 state 以及 dispatch
return [newState, dispatch];
}
}
}
// ...
}
В сочетании с реальностью давайте посмотрим на следующий набор крючков
let isMounted = false
if(!isMounted) {
[name, setName] = useState("张三");
[age] = useState("25");
isMounted = true
}
[sex, setSex] = useState("男");
return (
<button
onClick={() => {
setName(李四");
}}
>
修改姓名
</button>
);
Порядок хуков при первом рендеринге
name => age => sex
Согласно приведенному выше примеру, во время вторичного рендеринга вызывается только один хук.
setSex
Таким образом, чтобы обобщить построение связанного списка на этапе инициализации, этап обновления проходит по ранее созданному связанному списку по порядку и извлекает соответствующую информацию данных для рендеринга.Когда два порядка различны, разница в рендеринге будет вызванный.
Во избежание вышеуказанной ситуации мы можем установитьeslint-plugin-react-hooks
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
useEffect
useEffect(effect, array)
Эффект срабатывает каждый раз, когда завершается рендеринг, и взаимодействует с массивом для имитации жизненного цикла класса.
- Если не передано, каждый раз, когда componentDidUpdate будет запускать returnFunction (если он существует) перед запуском эффекта
- [] имитация компонентаDidMount
- [id] срабатывает только после изменения значения id
- четкий эффект
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
useLayoutEffect
- Подобно useEffect, проблема мерцания страницы в некоторых сценариях функций может быть решена путем синхронного выполнения обновления состояния.
- useLayoutEffect блокирует рендеринг, используйте его с осторожностью Сравнивать
import React, { useLayoutEffect, useEffect, useState } from 'react';
import './App.css'
function App() {
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
const test = () => {
setValue(0)
}
const color = !value ? 'red' : 'yellow'
return (
<React.Fragment>
<p style={{ background: color}}>value: {value}</p>
<button onClick={test}>点我</button>
</React.Fragment>
);
}
export default App;
useContext
const context = useContext(Context)
UseContext Как видно из названия, он использует React Context наподобие Hook.Во-первых, кратко представим концепцию и использование Context
import React, { useContext, useState, useEffect } from "react";
const ThemeContext = React.createContext(null);
const Button = () => {
const { color, setColor } = React.useContext(ThemeContext);
useEffect(() => {
console.info("Context changed:", color);
}, [color]);
const handleClick = () => {
console.info("handleClick");
setColor(color === "blue" ? "red" : "blue");
};
return (
<button
type="button"
onClick={handleClick}
style={{ backgroundColor: color, color: "white" }}
>
toggle color in Child
</button>
);
};
// app.js
const App = () => {
const [color, setColor] = useState("blue");
return (
<ThemeContext.Provider value={{ color, setColor }}>
<h3>
Color in Parent: <span style={{ color: color }}>{color}</span>
</h3>
<Button />
</ThemeContext.Provider>
);
};
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init)
Синтаксический сахар с редуксом почти положил основу 🌰
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useRef
const refContainer = useRef(initialValue);
useRef возвращает изменяемый объект ref, свойство .current которого инициализируется переданным параметром (initialValue).返回的 ref 对象在组件的整个生命周期内保持不变
-
Решите проблему с эталоном — useRef будет возвращать один и тот же объект ref каждый раз при рендеринге.
-
Исправьте некоторые из них, указывающие на проблемы
-
Сравнение createRef — в фазе инициализации два ничем не отличаются, но в обеих стадиях обновления отличается.
-
Мы знаем, что в локальной функции каждый раз, когда функция обновляется, переменные функции будут перегенерированы. Таким образом, каждый раз, когда мы обновляем компонент, мы заново создаем ref. Очевидно, что продолжать использовать createRef в настоящее время нецелесообразно, поэтому официальный запускuseRef. Ссылка, созданная useRef, похожа на глобальную переменную, определенную вне функции, и не будет воссоздана при обновлении компонента. Но при уничтожении компонента он тоже исчезнет без ручного уничтожения
Таким образом, ceateRef возвращает новую ссылку каждый раз при рендеринге, а useRef каждый раз возвращает одну и ту же ссылку.
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Хук, который часто используют для оптимизации производительности, взгляните 🌰
const MemoDemo = ({ count, color }) => {
useEffect(() => {
console.log('count effect')
}, [count])
const newCount = useMemo(() => {
console.log('count 触发了')
return Math.round(count)
}, [count])
const newColor = useMemo(() => {
console.log('color 触发了')
return color
}, [color])
return <div>
<p>{count}</p>
<p>{newCount}</p>
{newColor}</div>
}
В это время мы изменим входящее значение счетчика, и журнал будет выполняться последовательно
считать пожары
count effect
- Можно видеть, что это немного похоже на эффект, отслеживающий значение a и b, чтобы решить, следует ли обновлять пользовательский интерфейс в соответствии с изменением значения.
- memo срабатывает до обновления DOM, как сказал официальный представитель, аналогичный жизненный цикл - shouldComponentUpdate
- В сравненииReact.MemoПо умолчанию используется поверхностное сравнение на основе реквизита, или вы можете включить второй параметр для более глубокого сравнения. Весь компонент заключен в самый внешний слой, и вам нужно вручную написать метод для сравнения этих конкретных реквизитов перед повторным рендерингом. использоватьuseMemo можетТочный контроль, локальный Чистый
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Использование useCallback похоже на использование useMemo выше, это хуки, специально используемые для кэширования функций.
// 下面的情况可以保证组件重新渲染得到的方法都是同一个对象,避免在传给onClick的时候每次都传不同的函数引用
import React, { useState, useCallback } from 'react'
function MemoCount() {
const [value, setValue] = useState(0)
memoSetCount = useCallback(()=>{
setValue(value + 1)
},[])
return (
<div>
<button
onClick={memoSetCount}
>
Update Count
</button>
<div>{value}</div>
</div>
)
}
export default MemoCount
пользовательские крючки
Пользовательский хук — это функция, имя которой начинается с «использовать», и внутри функции могут быть вызваны другие хуки. Обычно я делю крючки на эти категории
util
Как следует из названия, классы инструментов, такие как useDebounce, useInterval, useWindowSize и т. д. Например, ниже useWindowSize
import { useEffect, useState } from 'react';
export default function useWindowSize(el) {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(
() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
},
[el],
);
return windowSize;
}
API
Как и раньше, у нас есть общедоступный интерфейс списка городов, который можно разместить в глобальном публичном доступе при использовании редукции, иначе нам может потребоваться его скопировать и вставить. С хуками нам нужно только использовать их и повторно использовать в других местах.
import { useState, useEffect } from 'react';
import { getCityList } from '@/services/static';
const useCityList = (params) => {
const [cityList, setList] = useState([]);
const [loading, setLoading] = useState(true)
const getList = async () => {
const { success, data } = await getCityList(params);
if (success) setList(data);
setLoading(false)
};
useEffect(
() => {getList();},
[],
);
return {
cityList,
loading
};
};
export default useCityList;
// bjs
function App() {
// ...
const { cityList, loading } = useCityList()
// ...
}
logic
Логический класс, например, у нас есть логика, чтобы щелкнуть аватар пользователя, чтобы подписаться или отписаться от пользователя, который может использоваться в списке комментариев и списке пользователей, Мы можем сделать это
import { useState, useEffect } from 'react';
import { followUser } from '@/services/user';
const useFollow = ({ accountId, isFollowing }) => {
const [isFollow, setFollow] = useState(false);
const [operationLoading, setLoading] = useState(false)
const toggleSection = async () => {
setLoading(true)
const { success } = await followUser({ accountId });
if (success) {
setFollow(!isFollow);
}
setLoading(false)
};
useEffect(
() => {
setFollow(isFollowing);
},
[isFollowing],
);
return {
isFollow,
toggleSection,
operationLoading
};
};
export default useFollow;
Нужно указать только три параметра, чтобы соответствовать большинству сценариев.
UI
Есть также некоторые хуки, которые связаны с пользовательским интерфейсом, но это немного спорно, смешивать ли их с пользовательским интерфейсом или нет. Лично мне совместное использование помогло решить некоторые проблемы повторного использования, поэтому я до сих пор делюсь им.
import React, { useState } from 'react';
import { Modal } from 'antd';
// TODO 为了兼容一个页面有多个 modal, 目前想法通过唯一 key 区分,后续优化
export default function useModal(key = 'open') {
const [opens, setOpen] = useState({
[key]: false,
});
const onCancel = () => {
setOpen({ [key]: false });
};
const showModal = (type = key) => {
setOpen({ [type]: true });
};
const MyModal = (props) => {
return <Modal key={key} visible={opens[key]} onCancel={onCancel} {...props} />;
};
return {
showModal,
MyModal,
};
}
// 使用
function App() {
const { showModal, MyModal } = useModal();
return <>
<button onClick={showModal}>展开</button>
<MyModal onOk={console.log} />
</>
}
Поперечная логика
Я слушал Dangxuan «Другая идея кросс-терминала» на 15-й конференции D2 раньше — совместное использование Write Once, ядро в том, как рендерить, как макетировать и другие изменения пользовательского интерфейса намного больше, чем уровень бизнес-логики, даже Общая парадигма разработки Applet и Flutter не сильно изменилась. Парадигма разработки Flutter очень похожа на React.Это также декларативный пользовательский интерфейс и также существует в VirtualDOM.
Та же часть кода useCount, абстрагированная от AST до dart, выглядит следующим образом.
Выше приведено расширенное приложение, давайте возьмем простой пример. Проект должен быть сайтом для ПК и мобильным терминалом.Несмотря на разумность двойного бизнеса, существует не так много мест, где можно повторно использовать пользовательский интерфейс, но бизнес-логику можно повторно использовать в большом количестве через хуки, который можно рассматривать как псевдологический промежуток
Суммировать
Все больше и больше сторонних библиотек, поддерживающих реакцию, находятся в версии хуков, таких как react-router и redux, которые имеют хуки. В то же время, есть также некоторые полезные библиотеки хуков, такие какahooksТакого рода.