Неэлегантные хуки React

внешний интерфейс React.js
Неэлегантные хуки React

Автор - 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что о?

Судя по приведенному выше примеру кода, почему строка 1useStateвернет строку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Зарезервированные ссылки никогда не меняются.

image.png

image.png

И эта функция делает его крючкомзакрытие спаситель.

"В беде используйРеф!"(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Можно сохранить ссылку на функцию при нескольких повторных рендерингах,2OkonClickТакже всегда одно и то же, что позволяет избежать дочерних компонентов<Sub>повторный рендеринг.

useCallbackИсходный код на самом деле очень прост:

image.png

Сохраняется только период монтированияcallbackи его зависимый массив

image.png

В течение периода обновления, если зависимые массивы непротиворечивы, будет возвращен последний из них.callback

КстатиuseMemoна самом деле это связано сuseCallbackРазница всего в одном шаге в INVOKE:

image.png

Бесконечные матрешки

по сравнению с неиспользованным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будет очень поздно.

Например,ul2000 представлены в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 будет лучший дизайн.