Guten Tag, Freunde!
Ich möchte Ihnen einige Einblicke in die Funktionsweise von React geben, nämlich Annahmen darüber, warum Hooks nicht in ifs, Schleifen, regulären Funktionen usw. verwendet werden können. Und können sie wirklich nicht so verwendet werden?
Die Frage ist: Warum können Haken nur auf der obersten Ebene verwendet werden? Hier ist, was die offizielle Dokumentation darüber sagt.
Beginnen wir mit den Regeln für die Verwendung von Hooks .
Verwenden Sie Haken nur auf der obersten Ebene (hervorgehoben die wichtigsten Punkte, auf die Sie achten sollten):
„Rufen Sie keine Hooks in Schleifen, Bedingungen oder verschachtelten Funktionen auf. Verwenden Sie Hooks stattdessen immer nur innerhalb der React-Funktionen, bevor Sie einen Wert von ihnen zurückgeben. Diese Regel stellt sicher, dass die Hooks bei jedem Rendern der Komponente in derselben Reihenfolge aufgerufen werden . Dadurch kann React den Hook-Status zwischen mehreren Aufrufen von useState und useEffect ordnungsgemäß beibehalten. (Wenn Sie interessiert sind, finden Sie unten eine ausführliche Erklärung.) "
Wir sind interessiert, siehe unten.
Erläuterung (Beispiele der Kürze halber weggelassen):
"… React useState? : React .… , React . , ?… . React , useState. React , persistForm, , . , , , , .… .… , ..."
Klar? Ja, irgendwie nicht sehr viel. Was meinst du mit "React hängt von der Reihenfolge ab, in der Hooks aufgerufen werden"? Wie macht er das? Was ist das für eine Art innerer Zustand? Was sind die Fehler, die durch das Fehlen eines Hooks beim erneuten Rendern verursacht werden? Sind diese Fehler für das Funktionieren der Anwendung kritisch?
Gibt es noch etwas in der Dokumentation dazu? Es gibt einen speziellen Abschnitt "Haken: Antworten auf Fragen" . Dort finden wir folgendes.
Wie bindet React Hook-Aufrufe an eine Komponente?
«React , .… , . JavaScript-, . , useState(), ( ) . useState() .»
Schon etwas. Eine interne Liste von Speicherplätzen, die Komponenten zugeordnet sind und einige Daten enthalten. Der Hook liest den Wert der aktuellen Zelle und bewegt den Zeiger zur nächsten. An welche Datenstruktur erinnert Sie das? Vielleicht sprechen wir über eine verknüpfte (verknüpfte) Liste .
Wenn dies tatsächlich der Fall ist, sieht die Folge von Hooks, die React beim ersten Rendern generiert, folgendermaßen aus (stellen Sie sich vor, die Rechtecke sind Hooks, jeder Hook enthält einen Zeiger auf den nächsten):
Großartig, wir haben eine Arbeitshypothese, die mehr oder weniger vernünftig aussieht. Wie überprüfen wir das? Eine Hypothese ist eine Hypothese, aber ich möchte Fakten. Und für die Fakten müssen Sie zu GitHub gehen, zum React-Quell- Repository .
Denken Sie nicht, dass ich mich sofort für einen so verzweifelten Schritt entschieden habe. Natürlich habe ich mich zuerst auf der Suche nach Antworten auf meine Fragen an das allwissende Google gewandt. Folgendes haben wir gefunden:
- Unter Stapelüberlauf finden Sie eine Frage dazu, wie React Hooks bestimmen, zu welcher Komponente sie gehören.
- Artikel "Das erste Eintauchen in den Quellcode von Hooks (Grundlagen für zukünftige Artikel)" über Habré
- Vorschläge zur Verwendung von Hooks in der if-Anweisung auf GitHub
- Sie können auch einige Informationen aus den TypsScript-Typdefinitionen für React abrufen . Hier sind die relevanten Auszüge
Alle diese Quellen beziehen sich auf die React-Quellen. Ich musste ein wenig in ihnen graben. Also die These und das Beispiel von "useState".
Der useState () und andere Hooks sind in ReactHooks.js implementiert :
export function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher()
return dispatcher.useState(initialState)
}
Ein Dispatcher wird verwendet, um useState () (und andere Hooks) aufzurufen. Am Anfang derselben Datei sehen wir Folgendes:
import ReactCurrentDispatcher from './ReactCurrentDispatcher'
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current
return ((dispatcher: any): Dispatcher)
}
Der Dispatcher, der zum Aufrufen von useState () (und anderen Hooks) verwendet wird, ist der Wert der Eigenschaft "current" des Objekts "ReactCurrentDispatcher", das aus ReactCurrentDispatcher.js importiert wird :
import type { Dispatcher } from 'react-reconciler/src/ReactInternalTypes'
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher)
}
export default ReactCurrentDispatcher
ReactCurrentDispatcher ist ein leeres Objekt mit einer "aktuellen" Eigenschaft. Dies bedeutet, dass es woanders initialisiert wird. Aber wo genau? Hinweis: Importe vom Typ "Dispatcher" zeigen an, dass der aktuelle Dispatcher etwas mit React-Interna zu tun hat. In der Tat finden wir dies in ReactFiberHooks.new.js (die Nummer im Kommentar ist die Zeilennummer):
// 118
const { ReactCurrentDispatcher, ReactCurrentBatchConfig } = ReactSharedInternals
In ReactSharedInternals.js stoßen wir jedoch auf "geheime interne Daten, die zur Verwendung ausgelöst werden könnten":
const ReactSharedInternals =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
export default ReactSharedInternals
Ist das alles? Ist unsere Suche zu Ende gegangen, bevor sie beginnen kann? Nicht wirklich. Wir werden die Details der internen Implementierung von React nicht kennen, aber wir brauchen sie nicht, um zu verstehen, wie React mit Hooks umgeht. Zurück in ReactFiberHooks.new.js:
// 405
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate
Der Dispatcher, der zum Aufrufen der Hooks verwendet wird, besteht aus zwei verschiedenen Dispatchern - HooksDispatcherOnMount (beim Mounten) und HooksDispatcherOnUpdate (beim Aktualisieren, erneutes Rendern).
// 2086
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
// -
}
// 2111
const HooksDispatcherOnUpdate: Dispatcher = {
useState: updateState,
// -
}
Die Mount / Update-Trennung wird auf Hook-Ebene beibehalten.
function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
//
const hook = mountWorkInProgressHook()
//
if (typeof initialState === 'function') {
initialState = initialState()
}
//
//
hook.memoizedState = hook.baseState = initialState
//
//
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any)
})
// - (setState)
const dispatch: Dispatch<
BasicStateAction<S>
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue
): any))
// , ,
return [hook.memoizedState, dispatch]
}
// 1266
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any))
}
Die Funktion "updateReducer" wird verwendet, um den Status zu aktualisieren. Wir sagen also, dass useState intern useReducer verwendet oder dass useReducer eine untergeordnete Implementierung von useState ist.
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
// , (!)
const hook = updateWorkInProgressHook()
//
const queue = hook.queue
//
queue.lastRenderedReducer = reducer
const current: Hook = (currentHook: any)
// , ,
let baseQueue = current.baseQueue
//
if (baseQueue !== null) {
const first = baseQueue.next
let newState = current.baseState
let newBaseState = null
let newBaseQueueFirst = null
let newBaseQueueLast = null
let update = first
do {
//
} while (update !== null && update !== first)
//
hook.memoizedState = newState
hook.baseState = newBaseState
hook.baseQueue = newBaseQueueLast
//
queue.lastRenderedState = newState
}
//
const dispatch: Dispatch<A> = (queue.dispatch: any)
//
return [hook.memoizedState, dispatch]
}
Bisher haben wir nur gesehen, wie die Haken selbst funktionieren. Wo ist die Liste? Hinweis: Mount- / Update-Hooks werden mit den Funktionen "mountWorkInProgressHook" bzw. "updateWorkInProgressHook" erstellt.
// 592
function mountWorkInProgressHook(): Hook {
//
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
// (?!)
next: null
}
// workInProgressHook null, ,
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
// ,
workInProgressHook = workInProgressHook.next = hook
}
return workInProgressHook
}
// 613
function updateWorkInProgressHook(): Hook {
// ,
// , (current hook), (. ), workInProgressHook ,
//
// , ,
let nextCurrentHook: null | Hook
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate
if (current !== null) {
nextCurrentHook = current.memoizedState
} else {
nextCurrentHook = null
}
} else {
nextCurrentHook = currentHook.next
}
let nextWorkInProgressHook: null | Hook
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState
} else {
nextWorkInProgressHook = workInProgressHook.next
}
if (nextWorkInProgressHook !== null) {
// workInProgressHook
workInProgressHook = nextWorkInProgressHook
nextWorkInProgressHook = workInProgressHook.next
currentHook = nextCurrentHook
} else {
//
// , ,
// , , ,
// , ?
// , "" ?
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.'
)
currentHook = nextCurrentHook
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
}
// workInProgressHook null, ,
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook
} else {
//
workInProgressHook = workInProgressHook.next = newHook
}
}
return workInProgressHook
}
Ich glaube, unsere Hypothese, dass eine verknüpfte Liste zur Kontrolle von Hooks verwendet wird, wurde bestätigt. Wir haben herausgefunden, dass jeder Hook eine "next" -Eigenschaft hat, deren Wert eine Verknüpfung zum nächsten Hook ist. Hier ist eine gute Illustration dieser Liste aus dem obigen Artikel:
Für diejenigen unter Ihnen, die sich fragen, wie die einfachste JavaScript-Implementierung einer verknüpften Einwegliste aussieht:
Ein bisschen Code
class Node {
constructor(data, next = null) {
this.data = data
this.next = next
}
}
class LinkedList {
constructor() {
this.head = null
}
insertHead(data) {
this.head = new Node(data, this.head)
}
size() {
let counter = 0
let node = this.head
while (node) {
counter++
node = node.next
}
return counter
}
getHead() {
return this.head
}
getTail() {
if (!this.head) return null
let node = this.head
while (node) {
if (!node.next) return node
node = node.next
}
}
clear() {
this.head = null
}
removeHead() {
if (!this.head) return
this.head = this.head.next
}
removeTail() {
if (!this.head) return
if (!this.head.next) {
this.head = null
return
}
let prev = this.head
let node = this.head.next
while (node.next) {
prev = node
node = node.next
}
prev.next = null
}
insertTail(data) {
const last = this.getTail()
if (last) last.next = new Node(data)
else this.head = new Node(data)
}
getAt(index) {
let counter = 0
let node = this.head
while (node) {
if (counter === index) return node
counter++
node = node.next
}
return null
}
removeAt(index) {
if (!this.head) return
if (index === 0) {
this.head = this.head.next
return
}
const prev = this.getAt(index - 1)
if (!prev || !prev.next) return
prev.next = prev.next.next
}
insertAt(index, data) {
if (!this.head) {
this.head = new Node(data)
return
}
const prev = this.getAt(index - 1) || this.getTail()
const node = new Node(data, prev.next)
prev.next = node
}
forEach(fn) {
let node = this.head
let index = 0
while (node) {
fn(node, index)
node = node.next
index++
}
}
*[Symbol.iterator]() {
let node = this.head
while (node) {
yield node
node = node.next
}
}
}
//
const chain = new LinkedList()
chain.insertHead(1)
console.log(
chain.head.data, // 1
chain.size(), // 1
chain.getHead().data // 1
)
chain.insertHead(2)
console.log(chain.getTail().data) // 1
chain.clear()
console.log(chain.size()) // 0
chain.insertHead(1)
chain.insertHead(2)
chain.removeHead()
console.log(chain.size()) // 1
chain.removeTail()
console.log(chain.size()) // 0
chain.insertTail(1)
console.log(chain.getTail().data) // 1
chain.insertHead(2)
console.log(chain.getAt(0).data) // 2
chain.removeAt(0)
console.log(chain.size()) // 1
chain.insertAt(0, 2)
console.log(chain.getAt(1).data) // 2
chain.forEach((node, index) => (node.data = node.data + index))
console.log(chain.getTail().data) // 3
for (const node of chain) node.data = node.data + 1
console.log(chain.getHead().data) // 2
//
function middle(list) {
let one = list.head
let two = list.head
while (two.next && two.next.next) {
one = one.next
two = two.next.next
}
return one
}
chain.clear()
chain.insertHead(1)
chain.insertHead(2)
chain.insertHead(3)
console.log(middle(chain).data) // 2
//
function circular(list) {
let one = list.head
let two = list.head
while (two.next && two.next.next) {
one = one.next
two = two.next.next
if (two === one) return true
}
return false
}
chain.head.next.next.next = chain.head
console.log(circular(chain)) // true
Es stellt sich heraus, dass updateWorkInProgressHook () beim erneuten Rendern mit weniger (oder mehr) Hooks einen Hook zurückgibt, der nicht mit seiner Position in der vorherigen Liste übereinstimmt, d. H. In der neuen Liste fehlt ein Knoten (oder ein zusätzlicher Knoten wird angezeigt). Und in Zukunft wird der falsche gespeicherte Zustand verwendet, um den neuen Zustand zu berechnen. Natürlich ist dies ein ernstes Problem, aber wie kritisch ist es? Weiß React nicht, wie man die Liste der Hooks im laufenden Betrieb neu erstellt? Und gibt es eine Möglichkeit, bedingte Hooks zu implementieren? Lassen Sie uns das herausfinden.
Ja, bevor wir von der Quelle gehen, werden wir nach einem Linter suchen, der die Regeln für die Verwendung von Hooks erzwingt. RulesOfHooks.js :
if (isDirectlyInsideComponentOrHook) {
if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
const message =
`React Hook "${context.getSource(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' +
'same order in every component render.' +
(possiblyHasEarlyReturn
? ' Did you accidentally call a React Hook after an' + ' early return?'
: '')
context.report({ node: hook, message })
}
}
Ich werde nicht näher darauf eingehen, wie der Unterschied zwischen der Anzahl der Haken bestimmt wird. Und so definieren Sie, dass eine Funktion ein Haken ist:
function isHookName(s) {
return /^use[A-Z0-9].*$/.test(s)
}
function isHook(node) {
if (node.type === 'Identifier') {
return isHookName(node.name)
} else if (
node.type === 'MemberExpression' &&
!node.computed &&
isHook(node.property)
) {
const obj = node.object
const isPascalCaseNameSpace = /^[A-Z].*/
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name)
} else {
return false
}
}
Lassen Sie uns eine Komponente skizzieren, in der die bedingte Verwendung von Hooks stattfindet, und sehen, was beim Rendern passiert.
import { useEffect, useState } from 'react'
//
function useText() {
const [text, setText] = useState('')
useEffect(() => {
const id = setTimeout(() => {
setText('Hello')
const _id = setTimeout(() => {
setText((text) => text + ' World')
clearTimeout(_id)
}, 1000)
}, 1000)
return () => {
clearTimeout(id)
}
}, [])
return text
}
//
function useCount() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount((count) => count + 1)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
return count
}
// ,
const Content = ({ active }) => <p>{active ? useText() : useCount()}</p>
function ConditionalHook() {
const [active, setActive] = useState(false)
return (
<>
<button onClick={() => setActive(!active)}> </button>
<Content active={active} />
</>
)
}
export default ConditionalHook
Im obigen Beispiel haben wir zwei benutzerdefinierte Hooks - useText () und useCount (). Wir versuchen, diesen oder jenen Hook abhängig vom Status der "aktiven" Variablen zu verwenden. Machen. Wir erhalten die Fehlermeldung "React Hook 'useText' wird bedingt aufgerufen. React Hooks müssen in jedem Komponenten-Rendering in genau derselben Reihenfolge aufgerufen werden ", was bedeutet, dass die Hooks bei jedem Rendering in derselben Reihenfolge aufgerufen werden müssen.
Vielleicht geht es nicht so sehr um React, sondern um ESLint. Versuchen wir es zu deaktivieren. Fügen Sie dazu am Anfang der Datei / * eslint-disable * / hinzu. Die Inhaltskomponente wird jetzt gerendert, aber das Wechseln zwischen Hooks funktioniert nicht. Es ist also doch Reagieren. Was kannst du noch tun?
Was ist, wenn wir benutzerdefinierte Hooks zu regulären Funktionen machen? Versuch:
function getText() {
// ...
}
function getCount() {
// ...
}
const Content = ({ active }) => <p>{active ? getText() : getCount()}</p>
Das Ergebnis ist das gleiche. Die Komponente wird mit getCount () gerendert, es ist jedoch nicht möglich, zwischen Funktionen zu wechseln. Übrigens, ohne / * eslint-disable * / erhalten wir den Fehler "React Hook" useState "wird in der Funktion" getText "aufgerufen, die weder eine React-Funktionskomponente noch eine benutzerdefinierte React Hook-Funktion ist. React-Komponentennamen müssen mit einem Großbuchstaben beginnen ", der besagt, dass der Hook innerhalb einer Funktion aufgerufen wird, die weder eine Komponente noch ein benutzerdefinierter Hook ist. Dieser Fehler enthält einen Hinweis.
Was ist, wenn wir unsere Funktionskomponenten herstellen?
function Text() {
// ...
}
function Count() {
// ...
}
const Content = ({ active }) => <p>{active ? <Text /> : <Count />}</p>
Jetzt funktioniert alles wie erwartet, auch bei eingeschaltetem Linter. Dies liegt daran, dass wir das bedingte Rendern der Komponenten tatsächlich implementiert haben. Offensichtlich verwendet React einen anderen Mechanismus, um das bedingte Rendern von Komponenten zu implementieren. Warum konnte dieser Mechanismus nicht auf Haken angewendet werden?
Lassen Sie uns noch ein Experiment machen. Wir wissen, dass beim Rendern einer Liste von Elementen jedem Element ein "Schlüssel" -Attribut hinzugefügt wird, mit dem React den Status der Liste verfolgen kann. Was ist, wenn wir dieses Attribut in unserem Beispiel verwenden?
function useText() {
// ...
}
function useCount() {
// ...
}
const Content = ({ active }) => <p>{active ? useText() : useCount()}</p>
function ConditionalHook() {
const [active, setActive] = useState(false)
return (
<>
<button onClick={() => setActive(!active)}> </button>
{/* key */}
<Content key={active} active={active} />
</>
)
}
Wir bekommen einen Fehler mit dem Linter. Ohne Linter ... funktioniert alles! Aber warum? Möglicherweise betrachtet React Content with useText () und Content with useCount () als zwei verschiedene Komponenten und rendert die Komponenten bedingt basierend auf dem aktiven Status. Wie dem auch sei, wir haben eine Problemumgehung gefunden. Ein anderes Beispiel:
import { useEffect, useState } from 'react'
const getNum = (min = 100, max = 1000) =>
~~(min + Math.random() * (max + 1 - min))
//
function useNum() {
const [num, setNum] = useState(getNum())
useEffect(() => {
const id = setInterval(() => setNum(getNum()), 1000)
return () => clearInterval(id)
}, [])
return num
}
// -
function NumWrapper({ setNum }) {
const num = useNum()
useEffect(() => {
setNum(num)
}, [setNum, num])
return null
}
function ConditionalHook2() {
const [active, setActive] = useState(false)
const [num, setNum] = useState(0)
return (
<>
<h3> ? <br /> , </h3>
<button onClick={() => setActive(!active)}> </button>
<p>{active && num}</p>
{active && <NumWrapper setNum={setNum} />}
</>
)
}
export default ConditionalHook2
Im obigen Beispiel haben wir einen benutzerdefinierten Hook "useNum", der jede Sekunde eine zufällige Ganzzahl im Bereich von 100 bis 1000 zurückgibt. Wir wickeln sie in die Komponente "NumWrapper" ein, die nichts zurückgibt (genauer gesagt, sie gibt null zurück ), aber ... aufgrund der Verwendung von setNum aus der übergeordneten Komponente wird der Status ausgelöst. Tatsächlich haben wir das bedingte Rendern der Komponente erneut implementiert. Dies zeigt jedoch, dass es auf Wunsch immer noch möglich ist, eine bedingte Verwendung von Haken zu erreichen.
Der Beispielcode ist hier .
Sandkasten:
Fassen wir zusammen. React verwendet eine verknüpfte Liste, um Hooks zu verwalten. Jeder (aktuelle) Hook enthält einen Zeiger auf den nächsten Hook oder null (in der Eigenschaft "next"). Aus diesem Grund ist es wichtig, die Reihenfolge einzuhalten, in der die Hooks bei jedem Render aufgerufen werden.
Obwohl Sie durch bedingtes Rendern von Komponenten eine bedingte Verwendung von Hooks erreichen können, sollten Sie dies nicht tun: Die Folgen können unvorhersehbar sein.
Noch ein paar Beobachtungen zu den React-Quellen: Klassen werden praktisch nicht verwendet, und Funktionen und ihre Zusammensetzung sind so einfach wie möglich (selbst der ternäre Operator wird selten verwendet); Die Namen der Funktionen und Variablen sind recht informativ, obwohl aufgrund der großen Anzahl von Variablen die Präfixe "base", "current" usw. verwendet werden müssen, was zu Verwirrung führt, jedoch angesichts der Größe der Codebasis ist diese Situation ganz natürlich; Es gibt detaillierte Kommentare, einschließlich TODO.
Zu den Rechten der Eigenwerbung: Für diejenigen, die die Tools für die Entwicklung moderner Webanwendungen (React, Express, Mongoose, GraphQL usw.) lernen oder besser verstehen möchten, empfehle ich einen Blick auf dieses Repository .
Hoffe du fandest es interessant. Konstruktive Kommentare in den Kommentaren sind willkommen. Vielen Dank für Ihre Aufmerksamkeit und einen schönen Tag.