Preface
The interpretation of the source code is helpful to understand what Hooks has done. If you think useEffect is "magic", this article may help you.
This blog is limited in length. It only looks at useEffect and tries to be simple and clear. It will take you to the depth of React Hooks
Find the source code of Hook (you can jump directly)
First, we get the source code of react from Github, and then we can find the react folder in packages, where index.js is our entry.
The code is very simple, just two lines:
const React = require('./src/React'); module.exports = React.default || React;
So let's take a look at 'react/src/React'. There are a lot of codes. Let's simplify them:
import ReactVersion from 'shared/ReactVersion'; // ... import { useEffect, } from './ReactHooks'; const React = { useEffect }; //... export default React;
Well, now we know at least why Hooks is quoted in the following way:
import {useEffect} from 'react'
Let's move on to 'react / SRC / react hooks'.
ReactHooks file (can jump directly)
As we said before, we only look at useEffect, so we need to simplify the code as well.
And considering that some people are not familiar with TypeScript syntax, TypeScript syntax is also removed, so will the simplified code.
Now let's look at the simplified code:
import invariant from 'shared/invariant'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; // If the React version is wrong or the Hook is used incorrectly, an error will be reported // ... return dispatcher; } export function useEffect(create,inputs) { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, inputs); }
As you can see here, our useEffect is actually ReactCurrentDispatcher.current.useEffect.
React current dispatcher file (can jump directly)
Take a look at the react current dispatcher file, which is not simplified here:
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks'; const ReactCurrentDispatcher = { current: (null: null | Dispatcher), }; export default ReactCurrentDispatcher;
We found that his current type is null or Dispatcher, so we can easily guess that the source code of this thing is' react reconciler / SRC / react fiberhooks'.
ReactFiberHooks file
Thousands of lines of code, big header. But don't panic. We're not here to write react. Just look at the principle.
We have known before that useState is actually ReactCurrentDispatcher.current.useState.
Obviously, no matter what is listed separately, we just need to know who is assigned to it.
After simplifying the code and removing the development code distinguished by "DEV", we found that there are few values assigned to react current dispatcher.current in the whole file.
The only one that has nothing to do with exception determination is the code in the renderWithHooks function:
export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRenderExpirationTime ){ ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; let children = Component(props, secondArg); return children; }
We don't know what this code is for, but it must be used to render components.
Obviously, the values of ReactCurrentDispatcher.current can only be HooksDispatcherOnMount and HooksDispatcherOnUpdate.
It's clear that one of these is for load time and the other for update time.
Then we search the relevant codes:
const HooksDispatcherOnMount = { useEffect: mountEffect }; const HooksDispatcherOnUpdate = { useEffect: updateEffect };
That is to say, useEffect will call mounteeffect when the component is loaded, and updateEffect when the component is updated.
Let's move on to these two functions:
function mountEffect(create, deps) { return mountEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, ); } function updateEffect(create, deps) { return updateEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, ); }
Here, UpdateEffect and PassiveEffect are binary constants, which are operated by bit operation.
You don't need to know the specific meaning first, just know it's a constant.
Next let's look at the specific mountEffectImpl:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps){ const hook = mountWorkInProgressHook(); // useEffect does not pass dependency, so it is null const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.effectTag |= fiberEffectTag; // The memoizedState of the hook object at the end of the list is the return value of pushEffect hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); }
We see that the first line of code calls mountWorkInProgressHook to create a new hook object. Let's look at mountWorkInProgressHook:
function mountWorkInProgressHook() { const hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
Obviously, there is a linked list structure workInProgressHook. If the workInProgressHook linked list is null, the new hook object will be assigned to it. If it is not null, it will be added at the end of the linked list.
It is necessary to explain:
Hooks are stored as a linked list in the membered state of the fiber.
currentHook is the linked list of the current fiber.
workInProgressHook is a linked list to be added to work in progress fiber.
Then let's look at pushEffect:
function pushEffect(tag, create, destroy, deps) { // Create a new effect, which is obviously a linked list structure const effect = { tag, create, destroy, deps, // Circular next: null, }; // Get component update queue from currentlyRenderingFiber.updateQueue let componentUpdateQueue= currentlyRenderingFiber.updateQueue; // Determine whether the component update queue is empty. Every time renderWithHooks is called, the component update queue will be set to null // In this way, each time the component is update d, a new effect list will be created if (componentUpdateQueue === null) { // Create a component update queue if it is empty componentUpdateQueue = createFunctionComponentUpdateQueue(); // And assign it to currentlyRenderingFiber.updateQueue currentlyRenderingFiber.updateQueue = componentUpdateQueue; // The latest effect of component update queue is our new effect componentUpdateQueue.lastEffect = effect.next = effect; } else { // If the component update queue already exists, get its latest Effect const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { // If the latest effect is null, the latest effect in the component update queue is our new effect componentUpdateQueue.lastEffect = effect.next = effect; } else { // Otherwise, we add our effect to the end of the list structure, and his next is the first effect of the list structure // The effect list here is a closed loop const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
Let's take a look at the updateEffectImpl called during the update:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { // Here is updateWorkInProgressHook // workInProgressHook = workInProgressHook.next; // currentHook = currentHook.next; const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; // Compare whether there is any change between the values of two dependent arrays. If there is no change, set the flag bit to NoHookEffect if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(NoHookEffect, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); }
We can see that updateEffectImpl and mountEffectImpl are very similar. The most important thing is that we have to string the two functions to see what they have implemented.
Hook related data structure diagram
Here I draw a picture of myself, which is conducive to understanding:
The structure of this diagram is the structure of a component at a certain time.
In the figure, yellow is the Fiber node, green is the Hook node, and blue is the Effect node.
The Fiber node is actually our virtual DOM node. react will generate a Fiber node tree. Each component has a corresponding Fiber node on the Fiber tree.
Where currentlyRenderingFiber represents the node we are rendering from workInProgress, and current represents the node that has been rendered.
When the component is loaded, each useEffect is executed, and then a Hook list is created. The memoizedState field of workInProgress points to the Hook node at the end of the Hook list.
When building each Hook node, an Effect node will be constructed at the same time. Similarly, the memoizedState field of the Hook node points to the corresponding Effect node.
Each Effect node will be connected to form a linked list, and then the updateQueue field of workInProgress points to the end Effect node of the Effect linked list.
When the component is updated, the dependency array of the Effect pointed to by currentHook will be compared with the new dependency array in turn. If it is the same, set the effectTag of the Effect node to NoHookEffect.
However, whether or not the value in the dependent array changes, an Effect node will be constructed as the value of the memoizedState field of the Hook node.
Then when you are ready to render, you will directly find the lastEffect of the updateQueue of the Fiber node, that is, directly point to the end Effect node of the Effect chain.
Because the effect list is closed-loop, here we find the first effect through the next of lastEffect.
Then loop through the effect list. When the effect tag is NoHookEffect, no operation will be performed. Otherwise, the destroy operation of the effect will be performed first, and then the create operation will be performed.
Yes, you are right. In summary, as long as the dependency changes after each update, the uninstall function of useEffect will be executed, and then the first parameter, create function, will be executed.
This part of the code is far away:
function commitHookEffectList( unmountTag, mountTag, finishedWork, ) { const updateQueue = finishedWork.updateQueue; let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & unmountTag) !== NoHookEffect) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } if ((effect.tag & mountTag) !== NoHookEffect) { // Mount const create = effect.create; effect.destroy = create(); } effect = effect.next; } while (effect !== firstEffect); } }
You may not understand the bit operation here, because the value of NoHookEffect is 0, so as long as effect.tag is set to NoHookEffect, then
effect.tag & unmountTag
It must be no hookeffect.
We still remember that when the values of the dependency array remain unchanged, we set the effectTag of the Effect node to NoHookEffect.
At this time, the operation of destroy Effect node first and then Effect function create will never be performed.
If the value of effect.tag is not NoHookEffect, you need at least one bit of effect.tag and unmountag to perform destroy.
Let's see that both mountEffectImpl and updateEffectImpl pass by default: UnmountPassive | MountPassive, that is to say, effect.tag is UnmountPassive | MountPassive.
Obviously, the purpose of this design is to execute the create function when mountTag is MountPassive, and the destroy function when mountTag is unmountative passive.
Only the following places will do this Passive operation:
export function commitPassiveHookEffects(finishedWork: Fiber): void { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Chunk: { commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); commitHookEffectList(NoHookEffect, MountPassive, finishedWork); break; } default: break; } } }
The meaning here is very obvious. First, traverse the effect list once, destroy every hook whose dependency has changed, and then traverse the effect list once again, execute the create function for every hook whose dependency has changed.
That is to say, every time, according to the call sequence of useEffect, all the unload functions of useEffect will be executed first, and then all the create functions of useEffect will be executed.
And commitPassiveHookEffects is the only function that flushpassive effects can finally call.
Each time React detects a data change, flushpassive effects executes.
This is true of both props and state changes.
So if you really need to simulate a life cycle like the previous componentDidMount and componentWillUnmount, you'd better use a separate Effect:
useEffect(()=>{ // Logic at load time return ()=>{ // Unloading logic } },[])
Here, [] is used as the dependency array because the dependency will not change. That is to say, the loading logic is only executed once when loading, and the unloading logic is executed once when unloading.
When you do not rely on arrays, each render will perform a load and unload.
summary
I hope this article of the blogger has some harvest for you, and I also want to talk about my feelings in reading the source code here.
Reading this part of the source code, in fact, is more like feeling the elephant in the blind.
In order to pursue efficiency, we need to avoid some more complex things, such as the Fiber node tree we mentioned, and for example, the use effect to replace the effect is not synchronous with the specific implementation effect.
Otherwise, it will take a lot of time to read a lot of things. I'm afraid that I really need to write a series.
The source code reading is a little difficult. Fortunately, the key points are annotated. In the process of reading, you can also gain a lot of things.
Blogger is not the developer of react. If there are any omissions and misunderstandings in the process of interpretation, I hope you can criticize and correct them.