React Hooks Code summary document read by ByteDance @Idealism

During my internship, I was in touch with most projects related to React. In the process of using hooks API, I often encountered some problems. Sometimes I understood the HOOKS API, and sometimes I was confused about why it happened. Is also some personal understanding of the experience to share it. The hooks source code may be slightly different from version to version, so we don’t need to care about specific variable names, just the overall flow and data structure. Here is the text:

Hooks problem to be fixed

Every new technology comes along to solve an existing problem. Before we get into the source code, let’s look at what the hooks fix:

  1. It is difficult to reuse state logic between Class Component components. React uses render props and higher-order components, which are hard to understand and have too much nesting, creating “nested hell”.

  2. Complex Class components get harder to understand: The life cycle functions of a class component are riddled with state logic and side effects that are hard to reuse and sporadic, such as fetching data in componentDidMount and componentDidUpdate, However, there may be a lot of other logic in componentDidMount as well, which makes component development more and more bloated, and the logic is obviously clustered in various lifecycle functions, making React development “lifecycle oriented programming.”

  3. Hard to understand Class Component questions, such as:

    • This pointer problem
    • Component precompilation techniques encounter optimization failures in class cases
    • Class does not compress well
    • Class is unstable under thermal loading

The React team hopes that the V16 hook will solve this problem. However, there are some difficulties in implementing the Hook:

  • The Class Component can permanently store instance state, whereas the Functional Component can’t, because state is reassigned to 0 every time the function is re-executed

  • Each Class Component instance has a member Function this.setState to change its state. Function Component is just a Function and does not have this.setState. Or some other way to do it.

  • There will be multiple hook update calls in a component, and we don’t want each update to produce a render, such as the following:

    setAge(18)
    setAge(19)
    setAge(20)  // Only care about the value calculated this time
    Copy the code

    We only care about the value computed by the last update.

To address the above difficulties, the React team designed the core logic of the Hook architecture: Use closures, two linked lists (a component’s hook call list, and each hook object has a queue of updates), and pass-through dispatch source code, as discussed in more detail below.

Before we dive into the source code, let’s take a look at the data structure of a few core objects so that we don’t get confused when we look at the source code.

The structure of a hook object

type Hook = {
  memoizedState: any,   // Final status value since last update
  queue: UpdateQueue, // Update the queue
  next, // Next hook object
};
Copy the code

What does the next pointer above do?

A hook can be called multiple times in a Functional Component, as follows:

let [name, setName] = useState(' ')
let [age, setAge] = useState(0)
Copy the code

Each time a hook method is called, a hook object is generated. The above code calls useState twice to generate two hooks, which are concatenated into a linked list using the next pointer, as shown below:

Update the data structure of the object

type Update = {
  expirationTime: ExpirationTime,// Expiration time
  action: A,// Modify the action
  eagerReducer: ((S, A) => S) | null.// Next reducer
  eagerState: S | null.// Next state
  next: Update; | null.// Next update
};
Copy the code

Update objects record information about data changes. You’ll notice that there is a next pointer and multiple Update objects are concatenated into a linked list structure.

The data structure of the queue object

React updates form a list, which is recorded in the last attribute of the queue. Different versions of React have different variable names. Newer versions of React are pending. Note that since we need to insert the update object and then iterate through the list at the end, the update is a circular list, with last pointing to the last update and next being the first

type queue = {
  last: Update| null.// Record the first update object
  dispatch,							// Records the dispatch method to be deconstructed to the user
  lastRenderedReducer,
  lastRenderedState,
};
Copy the code

React source code, the above three core data structure: hook, queue, update have reference concatenation, as shown in the following figure:

Ok, with the hook, queue, update three core data structure understanding, we can look at the source.

useState

Let’s start with the basic useState source code.

Most hooks in React are divided into two phases: mount phase during initial initialization and update phase.

UseState is no exception, corresponding to two methods:

  1. mountState
  2. updateState

mountState

Let’s look at what is done in the mountState method first, and then look at the source code to make it clearer.

  1. To generate ahookObject and mount tofiberThe object’smemoizedStateIn the linked list to which the
  2. generatehookThe object’smemoizedStateProperty to record updated values; generatehookThe object’squeueProperty, which is the initialized Update list
  3. generatedispatch Method is returned to the user.dispatchThat’s the second parameter we got for deconstruction

Detailed source code analysis is as follows:

// mountState() is called in the mount phase
function mountState(initialState) {
  1. Generate a hook object
  var hook = mountWorkInProgressHook();
  // hook.memoizedState is used to record the latest values from the last update. The next update starts with the latest value, not the original value. Set to the initial value during the mount phase
  hook.memoizedState = hook.baseState = initialState;
  Queue.pending indicates the first update in the list. // Pending indicates the first update in the list. Queue. dispatch Records the dispatches deconstructed to the user
  hook.queue = {
    pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  // Fiber and hook are one-to-many relationships recorded in the dispatch method through bind
  // 3. Call.bind() to preinject fiber and hook. Queue.
  hook.queue.dispatch = dispatchAction.bind(null, fiber, hook.queue);

  return [hook.memoizedState, hook.queue.dispatch];
}
Copy the code

Note that the dispatch method is generated by dispatchaction.bind () and then mounted to the queue property of the hook object.

Each time a user invokes the Hook API during initialization, a unique and magical Dispatch method is generated.

Why is it amazing? Because bind() is magic. Bind () can also pre-inject arguments in addition to changing the direction of this.

This line of dispatchAction.bind(null, fiber, hook. Queue) binds this to null and creates a new method. The new method pre-injects fiber (the current Fiber object) and hook. Queue. When we call setName(‘aaa’) in our business code, the actual call and input is dispatch(Fiber, hook.queue, ‘AAA ‘).

That is, a functionalComponent can call hook methods multiple times, resulting in a one-to-many relationship between the fiber object and the hook object. This relationship is recorded in the newly generated function by pre-injecting arguments through bind(). It can also be described as a closure, because the newly generated function keeps references to fiber and hook variables, so it will not be destroyed by the JS engine garbage collection. The update information is logged on the Hook object to implement data persistence in the Functional Component.

The above code was originally regenerated as a hook object by calling the mountWorkInProgressHook() method. Let’s see what the mountWorkInProgressHook method does.

mountWorkInProgressHook

function mountWorkInProgressHook() {
  // 1. Create a hook object
  var hook = {
    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null
  };

  // 2. Mount the new hook object to the linked list
  if (workInProgressHook === null) {
    // If there is no current hook list, the memoizedState property points to the hook
    fiber.memoizedState = workInProgressHook = hook;
  } else {
    // If a hook list already exists, concatenate it through the next pointer
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}
Copy the code

There is also an memoizedState for The Hook object. There is also an memoizedState for the Hook object.

The memoizedState property of the hook object records the value since the last update and is a concrete value.

The memoizedState property of a Fiber object has different meanings for different types of components. In Functional Component, Fiber’s memoizedState property is used to record the hook list, pointing to the first hook object, The hook object above. In Class Component, memoizedState is used to record the data state of the Class Component.

This code is all the logic of mountUpdate. At the end of the code, we return the user a deconstructed Dispatch method to update the data, and when the user calls a method like setName, we enter the updateState phase. Let’s see what happens behind the updateState phase.

updateState

Each time we execute the Dispatch method, we create an update object that records the update. The update object has the following structure:

type Update = {
  expirationTime: ExpirationTime,// Expiration time
  suspenseConfig: null | SuspenseConfig,
  action: A,// Modify the action
  eagerReducer: ((S, A) => S) | null.// Next reducer
  eagerState: S | null.// Next state
  next: Update< S, A> |null.// Next update
}
Copy the code

The created Update object is mounted to the update list Queue.

What is an update linked list queue?

React uses the data structure of a linked list. The next pointer on each update connects update objects generated by the same hook in sequence. When the second Update object is inserted, the next of the second update object points to the head node.

So what exactly does Update Estate do? Here are the dispatchAction methods:

// The arguments fiber and queue are prepassed in through bind
// Action is the value that the user actually passes through
function dispatchAction(fiber,queue,action) {    
   	// Generate an update object
  	const update = {   
      action,
      next: null};// Add the update object to the circular list
    const last = queue.last;    
    if (last === null) {      
        // The list is empty, the current update is first, and the loop is kept
        update.next = update;    
    } else {      
        const first = last.next;      
        if(first ! = =null) {        
        // Insert a new update object after the latest update object
            update.next = first;      
        }      
        last.next = update;    
    }    
    // Keep the table header on the latest update object
    queue.last = update;   
    // Perform scheduling tasks
    scheduleWork(); 
}
Copy the code

The above code does three things:

  • Generate an Update object
  • Add the Update object to the circular linked list
  • Call scheduleWork() for scheduling

ScheduleWork () then enters the update process of the React scheduling algorithm, which is beyond the scope of this article.

useEffect

useEffectThe use of is also divided intomountandupdate.mountThe main stage is to mount Effect in two places, one ishooksChain, the other is throughpushEffecttheuseEffectAll the collectedupdateQueueOn the linked list, and then execute after the refresh is completeupdateQueueThe function. In the update phase, this is basically the same, except that a dePS judgment is added. If the DEPS does not change, the tag does not need to be updated, and the function is not executed during the update ue

// react-reconciler/src/ReactFiberHooks.js
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null.) :void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps,
  );
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) :void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.effectTag |= fiberEffectTag;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    undefined,
    nextDeps,
  );
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      constfirstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; }}return effect;
}
Copy the code

UseMemo and useCallback

The mount procedure retrieves and stores the initial value, while the update procedure executes a new function to retrieve the new value or replace the value with the new value based on its shallow compare. In essence, they take advantage of context switching. A function or variable that existed in the previous context and, if dePS changes, uses or executes the function in the current context.

Function is(x: any, y: any) {return ((x === y && (x! == 0 || 1 / x === 1 / y)) || (x ! == x && y ! == y) ); } for (let i = 0; i<prevDeps.length &&i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return trueCopy the code

The value of the useMemo is cached at mount time. If the DEPS does not change, the function is not updated and the value is not updated. UseCallback is the same with useCallback, but it is a little bit difficult to understand. I have not understood why the variables in the function are not updated when I use it. Memorized functions only hold the values of the variables in the corresponding state. When function is re-executed, references to variables will not change. Context is switched after the DEPS update. Here’s a little example to help you understand

/ / useMemo related function mountMemo < T > (nextCreate: () = > T, deps: Array < mixed > | void | null,) : T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState ! == null) { // Assume these are defined. If they're not, areHookInputsEqual will warn. if (nextDeps ! == null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } / / useCallback related function mountCallback < T > (deps callback: T: Array < mixed > | void | null) : T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; } function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState ! == null) { if (nextDeps ! == null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; }Copy the code

Emmmm is an example of a function called in the previous context after a context switch. The variable inside that function still refers to the previous variable. The variable with the same name is not overwritten

Let obj = {} function area() {let b = 666 const test = () => {console.log(b)} obj. Test = test Function area2() {let b = 999 obj.test()} area2() // 666Copy the code

useRef

As you can see from the example above, the function Component will re-execute the function to switch to the new context when updating, so if you want to hold the original value, you need to hold the value in Fiber’s memorizeState and retrieve it from Fiber when using it. So useRef API, the source code is very simple, I don’t want to repeat here.

function mountRef<T> (initialValue: T) :{|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T> (initialValue: T) :{|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
Copy the code

Overall structure drawing

Join us