Автор - RichLab Ink Book
К концу 2021 года React Hooks произвели большой фурор в экосистеме React, охватив почти все приложения React. И это естественное соответствие функционально-компонентной и оптоволоконной архитектуре, и на данный момент у нас нет причин отказываться от него.
Верно то, что хуки решают давнюю проблему React Mixins, но, судя по разнообразному странному опыту, я думаю, что хуки на данном этапе не являются хорошей абстракцией.
Красное лицо слишком обычное, давай споем и черное лицо,Эта статья будет стоять с «тернистой» точки зрения, расскажите о React Hooks в моих глазах ~
«Странные» правила.
Официальный представитель реагирования разработал ряд спецификаций написания крючков, используемых для обогащения ошибок, но это просто подвергается его проблемам.
имя
Хуки — это не обычные функции, мы обычно используемuse
Названа в начале, чтобы отличить ее от других функций.
Но, соответственно, это нарушает и семантику именования функций. стабильныйuse
Из-за префиксов сложно назвать хуки, выuseGetState
Путано и непонятно с такой номенклатуройuseTitle
что это такоеuse
Далеко.
По сравнению с
_
частные переменные-члены, начинающиеся с и$
Суффиксные потоки не страдают от подобных проблем. Конечно, это просто разница в привычках использования, а не большая проблема.
время вызова
В использованииuseState
Когда вы когда-нибудь задумывались:useState
Хотя каждый разrender()
будет называться, но он может быть использован для меняДержатьСостояние, если я напишу их много, как оно узнает, какое состояние я хочу?
const [name, setName] = useState('xiaoming')
console.log('some sentences')
const [age, setAge] = useState(18)
дваждыuseState
Разница только в параметрах, а семантической разницы нет (просто присваиваем семантику возвращаемому значению), стоя наuseState
с точки зрения, как React узнает, когда я хочуname
и когда ты хочешьage
что о?
Судя по приведенному выше примеру кода, почему строка 1
useState
вернет строкуname
, а строка 3 возвращает числоage
Шерстяная ткань? В конце концов, похоже, мы просто дважды назвали это «непримечательно».useState
Вот и все.
Ответ "время".useState
Время вызова определяет результат, то есть первыйuseState
"сохранено"name
, и "спас" во второй разage
положение дел.
// Class Component 中通过字面量声明与更新 State,无一致性问题
this.setState({
name: 'xiaoming', // State 字面量 `name`,`age`
age: 18,
})
React просто и грубо использует «последовательность», чтобы решить все это (структура данных, стоящая за ним, представляет собой связанный список), что также приводит к строгим требованиям хуков к времени вызова. То есть, чтобы избежать всех ветвящихся структур, нельзя пускать хуки "иногда".
// ❌ 典型错误
if (some) {
const [name, setName] = useState('xiaoming')
}
Это требование полностью зависит от опыта разработчика или Lint, и с точки зрения общей сторонней библиотеки, такого родаДизайн API, требующий синхронизации вызововвстречается крайне редко и очень нелогично.
Идеальная упаковка API должна быть для разработчиковминимальная когнитивная нагрузкаиз. Это как обертка чистой функцииadd()
, независимо от того, где находится разработчикокружающая обстановкапозвони, как глубокоИерархиязвонок, какой звоноквыбор времени, пока входящие параметры соответствуют требованиям, он может работать нормально, просто и чисто.
function add(a: number, b: number) {
return a + b
}
function outer() {
const m = 123;
setTimeout(() => {
request('xx').then((n) => {
const result = add(m, n) // 符合直觉的调用:无环境要求
})
}, 1e3)
}
Можно сказать, что «React действительно не может заставить хуки не требовать окружения», но он не может отрицать странность этого метода.
Аналогичная ситуация вredux-saga
Есть и такие, что разработчики могут легко написать следующий «интуитивный» код, и проблем с «увидеть» не возникает.
import { call } from 'redux-saga/effects'
function* fetch() {
setTimeout(function* () {
const user = yield call(fetchUser)
console.log('hi', user) // 不会执行到这儿
}, 1e3)
}
yield call()
Вызов его в генераторе кажется действительно «разумным». Но по факту,function*
Требуется среда исполнения Generator, иcall
также требуетсяredux-saga
среда выполнения. При двойных требованиях код примера, естественно, не может работать нормально.
«Исключения» UseRef
Исходя из первоначального значения,useRef
По сути, это эпоха Class Component.React.createRef()
эквивалентная замена.
официальная документацияПример кода в самом начале в , иллюстрирует это (показан ниже, сокращенно):
function TextInputWithFocusButton() {
const inputEl = useRef(null);
return (
<input ref={inputEl} type="text" />
);
}
Но из-за особой реализации его часто используют не по назначению.
В исходном коде React HooksuseRef
Объекты инициализируются только в эпоху монтирования, а эпоха обновления возвращает результат эпохи монтирования (memoizedState
). Это означает, что в течение полного жизненного циклаuseRef
Зарезервированные ссылки никогда не меняются.
И эта функция делает его крючкомзакрытие спаситель.
"В беде используйРеф!"(useRef
Случаев злоупотребления много, поэтому в этой статье я не буду их повторять)
Выполнение каждой функции имеет соответствующую область видимости.this
Ссылка — это контекст, который соединяет все области действия (конечно, при условии, что они относятся к одному классу).
class Runner {
runCount = 0
run() {
console.log('run')
this.runCount += 1
}
xrun() {
this.run()
this.run()
this.run()
}
output() {
this.xrun()
// 即便是「间接调用」`run`,这里「仍然」能获取 `run` 的执行信息
console.log(this.runCount) // 3
}
}
В React Hooks каждый рендер определяется состоянием в данный момент, а контекст обновляется, когда рендеринг завершается. Элегантный рендеринг пользовательского интерфейса, чистый и аккуратный.
ноuseRef
Насколько это противоречит первоначальному замыслу дизайнера,useRef
Он может охватывать Scope, сгенерированный несколькими Renders, что может сохранить выполненную логику рендеринга, но также сделать отображаемый контекст не выпущенным.Бесконечная мощностьНо такжеДелать зло.
А если сказатьthis
Ссылка является наиболее важным побочным эффектом объектной ориентации.useRef
То же самое справедливо. С этой точки зрения, имеяuseRef
Написанному функциональному компоненту суждено быть трудным для достижения «функциональности».
будьте осторожны
Дефектный жизненный цикл
когда построен
Существует также большая «ошибка» между компонентом класса и компонентом функции. Компонент класса создается только один раз, а затем только выполняется.render()
, в то время как функциональный компонент постоянно выполняет сам себя.
Это приводит к тому, что в функциональном компоненте фактически отсутствуют соответствующиеconstructor
при построении. Конечно, если у вас есть способ выполнить определенную часть логики в функции только один раз, вы также можете смоделировать ее.constructor
.
// 比如使用 useRef 来构造
function useConstructor(callback) {
const init = useRef(true)
if (init.current) {
callback()
init.current = false
}
}
С точки зрения жизненного цикла,
constructor
не может быть похожимuseEffect
, если фактическое время рендеринга узла больше, между ними будет большая разница во времени.
Другими словами, API-интерфейсы жизненного цикла компонента класса и функционального компонента не соответствуют точно один к одному, что очень подвержено ошибкам.
Дизайн грязного использованияЭффект
в пониманииuseEffect
После основного использования , а также понимания его буквального значения (прислушиваясь к побочным эффектам), вы примете его за эквивалент Watcher.
useEffect(() => {
// watch 到 `a` 的变化
doSomething4A()
}, [a])
Но вскоре вы увидите, что что-то не так, если переменнаяa
Если повторный рендеринг не запущен, мониторинг не вступит в силу. Другими словами, он должен фактически использоваться для отслеживания изменений состояния, т.е.useStateEffect
. но параметрdeps
но нетограничить только вводСостояние. Если бы не какое-то специальное действие, трудно не подумать, что это конструктивный недостаток.
const [a] = useState(0)
const [b] = useState(0)
useEffect(() => {
// 假定此处为 `a` 的监听
}, [a])
useEffect(() => {
// 假定此处为 `b` 的监听
// 实际即便 `b` 未变化也并未监听 `a`,但此处仍然因为会因为 `a` 变化而执行
}, [b, Date.now()]) // 因为 Date.now() 每次都是新的值
useStateEffect
понимания нет, потому чтоuseEffect
На самом деле, он также отвечает за мониторинг монтирования, вам нужно использовать «пустые зависимости», чтобы различать монтирование и обновление.
useEffect(onMount, [])
Чем больше возможностей поддерживает один API, тем более запутанным будет его дизайн. Сложные функции не только проверяют память разработчика, но и сложны для понимания, и более склонны к сбоям из-за неправильного понимания.
useCallback
Проблема с производительностью?
В Class Component мы часто связываем функции вthis
на, держи егоуникальная ссылка, чтобы уменьшить ненужный повторный рендеринг дочерних компонентов.
class App {
constructor() {
// 方法一
this.onClick = this.onClick.bind(this)
}
onClick() {
console.log('I am `onClick`')
}
// 方法二
onChange = () => {}
render() {
return (
<Sub onClick={this.onClick} onChange={this.onChange} />
)
}
}
Соответствующая схема в функциональном компоненте:useCallback
:
// ✅ 有效优化
function App() {
const onClick = useCallback(() => {
console.log('I am `onClick`')
}, [])
return (<Sub onClick={onClick} />)
}
// ❌ 错误示范,`onClick` 在每次 Render 中都是全新的,<Sub> 会因此重渲染
function App() {
// ... some states
const onClick = () => {
console.log('I am `onClick`')
}
return (<Sub onClick={onClick} />)
}
useCallback
Можно сохранить ссылку на функцию при нескольких повторных рендерингах,2
OkonClick
Также всегда одно и то же, что позволяет избежать дочерних компонентов<Sub>
повторный рендеринг.
useCallback
Исходный код на самом деле очень прост:
Сохраняется только период монтированияcallback
и его зависимый массив
В течение периода обновления, если зависимые массивы непротиворечивы, будет возвращен последний из них.callback
КстатиuseMemo
на самом деле это связано сuseCallback
Разница всего в одном шаге в INVOKE:
Бесконечные матрешки✓
по сравнению с неиспользованнымuseCallback
проблемы с производительностью, настоящая проблемаuseCallback
Приносит проблемы со ссылочной зависимостью.
// 当你决定引入 `useCallback` 来解决重复渲染问题
function App() {
// 请求 A 所需要的参数
const [a1, setA1] = useState('')
const [a2, setA2] = useState('')
// 请求 B 所需要的参数
const [b1, setB1] = useState('')
const [b2, setB2] = useState('')
// 请求 A,并处理返回结果
const reqA = useCallback(() => {
requestA(a1, a2)
}, [a1, a2])
// 请求 A、B,并处理返回结果
const reqB = useCallback(() => {
reqA() // `reqA`的引用始终是最开始的那个,
requestB(b1, b2) // 当`a1`,`a2`变化后`reqB`中的`reqA`其实是过时的。
}, [b1, b2]) // 当然,把`reqA`加到`reqB`的依赖数组里不就好了?
// 但你在调用`reqA`这个函数的时候,
// 你怎么知道「应该」要加到依赖数组里呢?
return (
<>
<Comp onClick={reqA}></Comp>
<Comp onClick={reqB}></Comp>
</>
)
}
Как видно из приведенного выше примера, когдаuseCallback
Когда зависимости существовали раньше, их справочное обслуживание также было сложным. Будьте осторожны при вызове функции, вам нужно учитывать, имеет ли онаустаревшая ссылкаЕсли он опущен и не добавлен в массив зависимостей, это вызовет ошибку.
Use-Universal
Период ста цветов Хуков породил множество библиотек инструментов, толькоahooks
Есть 62 настраиваемых хука, это действительно «все идет».use
"~ Действительно ли необходимо инкапсулировать так много хуков? Или нам действительно нужно так много хуков?
Разумная упаковка?
Хотя в документации React официально рекомендуется инкапсулировать пользовательские хуки для улучшения возможности повторного использования логики. Но я думаю, что это зависит от ситуации, не все жизненные циклы необходимо инкапсулировать в хуки.
// 1. 封装前
function App() {
useEffect(() => { // `useEffect` 参数不能是 async function
(async () => {
await Promise.all([fetchA(), fetchB()])
await postC()
})()
}, [])
return (<div>123</div>)
}
// --------------------------------------------------
// 2. 自定义 Hooks
function App() {
useABC()
return (<div>123</div>)
}
function useABC() {
useEffect(() => {
(async () => {
await Promise.all([fetchA(), fetchB()])
await postC()
})()
}, [])
}
// --------------------------------------------------
// 3. 传统封装
function App() {
useEffect(() => {
requestABC()
}, [])
return (<div>123</div>)
}
async function requestABC() {
await Promise.all([fetchA(), fetchB()])
await postC()
}
В приведенном выше коде логика жизненного цикла инкапсулирована как HookuseABC
Вместо этого он объединяет обратные вызовы жизненного цикла и снижает возможность повторного использования. Даже если наш пакет не содержит никаких хуков, это всего лишь слой пакета, когда он вызывается.useEffect
Вот и все, это не слишком большая проблема, и заставляет эту логику работать и вне хуков.
Если используется в пользовательских хукахuseEffect
а такжеuseState
Общее количество раз не более 2. Вам стоит серьезно задуматься о необходимости этого хука и можно ли от него отказаться.
Проще говоря, хуки либо «прикрепляются к жизненному циклу», либо «обрабатывают состояние», иначе они не нужны.
повторные звонки
«Нелогичным» в вызове Hook является то, что он будет вызываться непрерывно с повторным рендерингом, что требует от разработчиков Hookповторные звонкиЕсть определенные ожидания.
Как и в приведенном выше примере, на инкапсуляцию запроса легко положиться.useEffect
, ведь его можно определить по жизненному циклуЗапросы не повторяются.
function useFetchUser(userInfo) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(userInfo).then(setUser)
}, [])
return user
}
но,useEffect
Это действительно подходит? Если это времяDidMount
, время выполнения все равно относительно позднее, в конце концов, если структура рендеринга сложная и уровень слишком глубокий,DidMount
будет очень поздно.
Например,ul
2000 представлены вli
:
function App() {
const start = Date.now()
useEffect(() => {
console.log('elapsed:', Date.now() - start, 'ms')
}, [])
return (
<ul>
{Array.from({ length: 2e3 }).map((_, i) => (<li key={i}>{i}</li>))}
</ul>
)
}
// output
// elapsed: 242 ms
Это не зависит от жизненного цикла, но используетуправляемый государствомШерстяная ткань? Кажется хорошей идеей, и кажется разумным обновить данные, если состояние изменится.
useEffect(() => {
fetch(userInfo).then(setUser)
}, [userInfo]) // 请求参数变化时,重新获取数据
Но время первоначального исполнения все же не идеально, илиDidMount
.
let start = 0
let f = false
function App() {
const [id, setId] = useState('123')
const renderStart = Date.now()
useEffect(() => {
const now = Date.now()
console.log('elapsed from start:', now - start, 'ms')
console.log('elapsed from render:', now - renderStart, 'ms')
}, [id]) // 此处监听 `id` 的变化
if (!f) {
f = true
start = Date.now()
setTimeout(() => {
setId('456')
}, 10)
}
return null
}
// output
// elapsed from start: 57 ms
// elapsed from render: 57 ms
// elapsed from start: 67 ms
// elapsed from render: 1 ms
Вот почему вышеизложенноеuseEffect
Дизайн сбивает с толку. Когда вы думаете о нем как о State Watcher, он на самом деле подразумевает, что «первое выполнениеDidMount
логика. буквальноEffect
Слушай, эта логика - побочный эффект. . .
управляемый государствомПомимо тайминга вызова, на самом деле есть и другие проблемы с инкапсуляцией:
function App() {
const user = useFetchUser({ // 乍一看似乎没什么问题
name: 'zhang',
age: 20,
})
return (<div>{user?.name}</div>)
}
На самом деле, повторный рендеринг компонента приведет к пересчету параметров запроса -> объект, объявленный литералом, каждый раз совершенно новый ->useFetchUser
Так что продолжайте запрашивать -> запрос изменяет состояние в хукеuser
-> Внешние компоненты<App>
Перерендерить.
Это бесконечный цикл!
Конечно, вы можете использоватьImmutable
решить проблему повторных запросов одного и того же параметра.
useEffect(() => {
// xxxx
}, [ Immutable.Map(userInfo) ])
Но в целом инкапсуляция хуков выходит далеко за рамки изменения организации вашего кода. Например, при выполнении запросов данных вы можете встать на путь, управляемый состоянием, и в то же время вам придется решать новые проблемы, связанные с управлением состоянием.
Для Миксина?
На самом деле, способность микина не подключается к эксклюзивным, мы можем использовать декоратор для инкапсуляции механизма микин.То есть Хукс не может полагаться на способность Миксина преодолевать общественное мнение.
const HelloMixin = {
componentDidMount() {
console.log('Hello,')
}
}
function mixin(Mixin) {
return function (constructor) {
return class extends constructor {
componentDidMount() {
Mixin.componentDidMount()
super.componentDidMount()
}
}
}
}
@mixin(HelloMixin)
class Test extends React.PureComponent {
componentDidMount() {
console.log('I am Test')
}
render() {
return null
}
}
render(<Test />) // output: Hello, \n I am Test
Тем не менее, хуки более пригодны для сборки и их легко вкладывать друг в друга. Но нужно опасаться Крюков с более глубокими слоями: очень вероятно, что в уголке, о котором вы не знаете, таится скрытая опасность.useEffect
.
резюме
- В этой статье не утверждается, что Class Component отказывается использовать React Hooks.Вместо этого я надеюсь глубже понять хуки, сравнив их в деталях.
- Странность React Hooks — основной камень преткновения.
- До появления хуков функциональный компонент был маленьким, надежным, но ограниченным по функциональности. Хуки наделяют функциональный компонент возможностями состояния и обеспечивают жизненный цикл, что позволяет использовать функциональный компонент в больших масштабах.
- «Элегантность» Hooks исходит из намека на функциональность, но
useRef
Злоупотребление делает Хукса далеко не «элегантным». - Есть еще много проблем с масштабной реализацией React Hooks, как из-за понимания семантики, так и из-за необходимости инкапсуляции.
- Внедрять инновации непросто, и я надеюсь, что в будущем у React будет лучший дизайн.