The introduction of React Hooks has completely changed the way React components are written, enabling functional components to manage state and side effects. This article will delve into the internal implementation of React Hooks, especially the commonly used Hooks useEffect, useRef, useMemo and useCallback.
Internal mechanism of Effect Hook
Effect Hook (mainly useEffect and useLayoutEffect) in React is an important tool for handling component side effects. Their implementation involves multiple stages of React's rendering and submission process. Let’s take a closer look at how they work.
The difference between useEffect and useLayoutEffect
Although the APIs of useEffect and useLayoutEffect look similar, there are important differences in their execution timing:
* useEffect: executed asynchronously after rendering is completed
* useLayoutEffect: executed synchronously after DOM changes
This difference is reflected in their internal implementation:
function mountEffect(create, deps) {
return mountEffectImpl(UpdateEffect | PassiveEffect, HookPassive, create, deps)
}
function mountLayoutEffect(create, deps) {
return mountEffectImpl(UpdateEffect, HookLayout, create, deps)
}
```
The key difference is the flags passed in: useEffect uses the PassiveEffect flag, while useLayoutEffect does not. This determines how they are processed during the commit phase.
### Effect creation process
When the component is first rendered, Effect is created through the mountEffectImpl function:
```javascript
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps)
}
```
This function performs the following key tasks:
1. Create a new hook and add it to the fiber's hook list
2. Set the flags of the fiber, which determines how to handle the fiber during the commit phase.
3. Call pushEffect to create an effect object and add it to the effect list
### Effect execution process
The execution of Effect is divided into three main stages, corresponding to the three sub-stages of the commit stage:
1. Before mutation (commitBeforeMutationEffects)
2. Mutation (commitMutationEffects)
3. Layout (commitLayoutEffects)
#### Before Mutation stage
At this stage, React prepares for the execution of useEffect:
```javascript
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const flags = nextEffect.flags
if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects()
return null
})
}
}
nextEffect = nextEffect.nextEffect
}
}
```
Here, React schedules an asynchronous task to execute useEffect. That's why useEffect executes asynchronously after rendering.
#### Mutation stage
At this stage, React handles useLayoutEffect’s cleanup function:
```javascript
function commitMutationEffects(root, renderPriorityLevel) {
while (nextEffect !== null) {
const flags = nextEffect.flags
if (flags & Update) {
const current = nextEffect.alternate
commitWork(current, nextEffect)
}
nextEffect = nextEffect.nextEffect
}
}
```
This process is synchronous and occurs before the DOM is updated, ensuring that old side effects are cleaned up before new ones are executed.
Why useLayoutEffect performs cleanup function here?
* Make sure to clean up old side effects before updating the DOM to prevent potential memory leaks.
* Prepare a clean environment for new side effects.
#### Layout stage
In the Layout phase, React executes the callback function of useLayoutEffect:
```javascript
function commitLayoutEffects(root, committedLanes) {
while (nextEffect !== null) {
const flags = nextEffect.flags
if (flags & (Update | Callback)) {
const current = nextEffect.alternate
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes)
}
if (flags & Ref) {
commitAttachRef(nextEffect)
}
nextEffect = nextEffect.nextEffect
}
}
```
useLayoutEffect's callback is executed synchronously here, while useEffect's callback is still waiting for asynchronous execution.
### Asynchronous execution of useEffect
The actual execution of useEffect occurs in the previously scheduled asynchronous task. When the browser has idle time, the previously scheduled asynchronous task (flushPassiveEffects) will be executed. This function will loop through all pending effects and execute them:
```javascript
function flushPassiveEffects() {
if (rootWithPendingPassiveEffects !== null) {
const root = rootWithPendingPassiveEffects
// First execute all cleanup functions
flushPassiveEffectsImpl()
// Then execute the new effect
flushPassiveEffectsImpl()
}
}
```
This function first executes the cleanup functions of all effects to be cleaned, and then executes all new effect callbacks.
## 2\. Implementation principle of Ref Hook
The implementation of useRef is relatively simple, but its role is very important:
```javascript
function mountRef(initialValue) {
const hook = mountWorkInProgressHook()
const ref = { current: initialValue }
hook.memoizedState = ref
return ref
}
function updateRef() {
const hook = updateWorkInProgressHook()
return hook.memoizedState
}
```
The core idea of useRef is to create a mutable reference that remains unchanged throughout the lifetime of the component. This is why we can use useRef to store DOM nodes or any mutable value without triggering component re-rendering.
During the construction and submission phases of the fiber tree, React will specially handle fibers with ref:
```javascript
function markRef(current, workInProgress) {
const ref = workInProgress.ref
if ((current === null && ref !== null) || (current !== null && current.ref !== ref)) {
workInProgress.flags |= Ref
}
}
```
This function will mark the fiber when the ref changes so that it can be processed during the commit phase.
In the commit phase, React will first remove the old ref and then set the new ref:
```javascript
function commitAttachRef(finishedWork) {
const ref = finishedWork.ref
if (ref !== null) {
const instance = finishedWork.stateNode
let instanceToUse
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance)
break
default:
instanceToUse = instance
}
if (typeof ref === 'function') {
ref(instanceToUse)
} else {
ref.current = instanceToUse
}
}
}
```
OK, I understand you want to add something about shallow comparison dependencies in section 3. We could extend this section to explain in detail how React performs dependency comparisons, and how this affects the performance optimization of useMemo and useCallback. The following is the modified content:
## 3\. Implementation and dependency comparison mechanism of useMemo and useCallback
The implementations of useMemo and useCallback are very similar, and they are both used for performance optimization. The core of these two Hooks is to compare dependencies to decide whether the value needs to be recalculated or the function re-created. Let's take a closer look at their implementation, especially the mechanisms that rely on comparisons:
```javascript
function mountMemo(nextCreate, deps) {
const hook = mountWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
const nextValue = nextCreate()
hook.memoizedState = [nextValue, nextDeps]
return nextValue
}
function updateMemo(nextCreate, deps) {
const hook = updateWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
const prevState = hook.memoizedState
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]
}
}
}
const nextValue = nextCreate()
hook.memoizedState = [nextValue, nextDeps]
return nextValue
}
```
### Depend on comparison mechanism
React uses the `areHookInputsEqual` function to do shallow comparisons of dependencies. The implementation of this function is roughly as follows:
```javascript
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) {
return false
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue
}
return false
}
return true
}
```
This function has the following key points:
1. Shallow comparison: only compares the reference of each element in the dependent array, without performing deep comparison.
2. [Use Object.is](http://xn--Object-vt9i248w.is): This method compares whether two values are the same. It is similar to the `===` operator, but can correctly handle NaN and -0 situations.
3. Length-sensitive: If the lengths of the old and new dependency arrays are different, false will be returned.
### The impact of shallow comparison
The shallow comparison mechanism has an important impact on the use of useMemo and useCallback:
1. Basic type dependence: For basic types such as numbers, strings, and Boolean values, shallow comparison can effectively detect changes.
2. Object and array dependencies: Since only references are compared, if you pass in an object or array that is newly created every time it is rendered, it will be regarded as a dependency change even if the content does not change. For example:
```javascript
useMemo(() => expensiveCalculation(a, b), [{ a, b }]) // Not recommended
```
Here, a new object is created for each rendering, causing the memo to become invalid. Should be changed to:
```javascript
useMemo(() => expensiveCalculation(a, b), [a, b]) // Recommended
```
3. Functional dependencies: Similarly, if a dependency contains an inline function, a new function reference will be created with each rendering, causing optimization to fail.
### Best Practices
In order to take full advantage of the optimization effects of useMemo and useCallback:
1. Only include necessary dependencies: only include values that the calculation or callback function really depends on.
2. Use basic type dependencies: Use basic types such as numbers and strings as dependencies as much as possible.
3. Stable references: For object or function dependencies, consider using useMemo or useCallback to stabilize their references.
4. Consider using useRef: For mutable values that don’t need to trigger re-rendering, consider using useRef instead of making it a dependency.
## Summary
The implementation of React Hooks involves complex internal mechanisms, including the construction, update and submission process of the fiber tree.
Key points include:
1. Execution timing and optimization mechanism of Effect Hooks (useEffect and useLayoutEffect)
2. The non-triggering re-rendering feature of Ref Hook (useRef) and its application in DOM operations
3. Dependency comparison and caching mechanism of Memo Hooks (useMemo and useCallback)
Through these mechanisms, React achieves the ability to manage state and side effects in functional components while maintaining good performance and development experience. An in-depth understanding of these implementation details will help us better use Hooks in actual development and write higher-quality React code.