Understand React's common hooks source code by implementing your own useState

(1) Implement a simple useState

overall data structure

insert image description here

need

function App() {
    
    
  const [age, updateAge] = useState(0);
  const [num,updateNum] = useState(5);
  console.log(`${
      
      isMount ? 'mount' : 'update'} age: `,age,'num',num)
  return {
    
    
    click() {
    
    
      updateAge(num => num + 1);
      updateAge(num => num + 8);
      updateNum(num => num + 5);
    }
  }
}

We observe the above code, and the main considerations for implementing the code are:
1. Calling ReactDOM.render will generate an update of mount, and the age and num returned by useState are initialValue (ie 0 and 5).
2. Clicking on the click event will generate an update of update, each time Call updateAge or updateNum to update the component App, and the num/age returned by useState is the updated result

what is update

The update is the following data structure:

const update = {
    
    
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

update data structure

How are these updates put together?
The answer is: they will form a circular one-way linked list.
Call updateNum, updateAge actually calls dispatchAction.bind(null, hook.queue), let's understand this function first:

function dispatchAction(queue, action) {
    
    
  const update = {
    
    
    // 更新执行的函数
    action,
    // 与同一个Hook的其他更新形成链表
    next: null
  }
  if (queue.pending === null) {
    
    
    update.next = update;
  } else {
    
    
    // 环状单向链表: 第一个update的next指向第二个update,第二个update的next指向第三个update,....,最后一个update指向第一个update
    // 1.新的update的next指向第一个update
    update.next = queue.pending.next;
    // 2.最后一个update的next指向新的update
    queue.pending.next = update;
  }
  // queue.pending始终指向最后一个插入的update
  queue.pending = update;
  schedule();
}

insert image description here

How state is saved

The update object generated by the update will be saved in the queue of the corresponding hook. For functional components, where is the hook stored?
In the fiber corresponding to FunctionComponent.

// App组件对应的fiber对象
const fiber = {
    
    
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null,
  // 指向App函数
  stateNode: App
};

Hook data structure

hook = {
    
    
  // 保存update的queue,即上文介绍的queue
  queue: {
    
    
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

insert image description here
Overall idea:
1. When mounting, create a hook in the useState function, and use the workInProgressHook pointer (pointing to the previous hook) to connect this hook with the previous hook to form a one-way acyclic linked list of hooks. Returns [initialState, dispatchAction.bind(null, hook.queue)].

2. When updateAge/updateNum is called, it is calling dispatchAction.bind(null, hook.queue)
to combine the update object and the previous update objects into a circular one-way linked list. And simulate React to start scheduling updates.
The useState function will be executed in the scheduling update, and the update result baseState will be calculated according to its corresponding hook object and its ring-shaped one-way chain of hooks, returning [baseState, dispatchAction.bind(null, hook.queue)];

overall code

let workInProgressHook;
// 通过workInProgressHook变量指向当前正在工作的hook。
let isMount = true;
// 首次render时是mount


const fiber = {
    
    
  memoizedState: null,
  stateNode: App
};

function schedule() {
    
    
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  const app = fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
  return app;
}

 // 1.创建update
 // 2.环状单向链表操作
 // 3.模拟React开始调度更新
function dispatchAction(queue, action) {
    
    
  const update = {
    
    
    // 更新执行的函数
    action,
    // 与同一个Hook的其他更新形成链表
    next: null
  }
  if (queue.pending === null) {
    
    
    update.next = update;
  } else {
    
    
    // 环状单向链表: 第一个update的next指向第二个update,第二个update的next指向第三个update,....,最后一个update指向第一个update
    // 1.新的update的next指向第一个update
    update.next = queue.pending.next;
    // 2.最后一个update的next指向新的update
    queue.pending.next = update;
  }
  // queue.pending始终指向最后一个插入的update
  queue.pending = update;
  schedule();
}

function useState(initialState) {
    
    
  let hook;
  if (isMount) {
    
    
    hook = {
    
    
      queue: {
    
    
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    if (!fiber.memoizedState) {
    
    
      fiber.memoizedState = hook;
    } else {
    
    
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    
    
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    
    
    let firstUpdate = hook.queue.pending.next;
   do {
    
    
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while(firstUpdate !== hook.queue.pending.next)
  }
  hook.memoizedState = initialState;
  return [baseState, dispatchAction.bind(null, hook.queue)];
}

function App() {
    
    
  const [age, updateAge] = useState(0);
  const [num,updateNum] = useState(5);
  console.log(`${
      
      isMount ? 'mount' : 'update'} age: `,age,'num',num)
  return {
    
    
    click() {
    
    
      updateAge(num => num + 1);
      updateAge(num => num + 8);
      updateNum(num => num + 5);
    }
  }
}

const myApp = schedule();
myApp.click()

(2) Principle Analysis

In the source code, most of our Hooks have two definitions:

// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
    
    
  useEffect: mountEffect,
  useReducer: mountReducer,
  useState: mountState,
 // 其他Hooks
};

// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
    
    
  useEffect: updateEffect,
  useReducer: updateReducer,
  useState: updateState,
  // 其他Hooks
};

It can be seen from this that the logic of our Hooks is different in the Mount phase and the Update phase. They are two different definitions in the Mount phase and the Update phase.

1.useState

1.1 mountState

Let's first look at the implementation of mountState.

// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
    
    
  // 1.获取当前的Hook节点,同时将当前Hook添加到Hook链表中
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    
    
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  // 2.声明一个链表来存放更新
  const queue = (hook.queue = {
    
    
    last: null,
    dispatch: null,
    lastRenderedReducer,
    lastRenderedState,
  });
  // 3.返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  // 4.返回当前状态和修改状态的方法 
  return [hook.memoizedState, dispatch];
}

Hook type definition

// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
    
    
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,  // 指向下一个Hook
};

How are these Hooks nodes connected in series using the linked list data structure? The relevant logic is in the mountWorkInProgressHook method called by the Hooks function in each specific mount stage (workInProgressHook is connected in series as a pointer):

// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
    
    
  const hook: Hook = {
    
    
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    
    
    // 当前workInProgressHook链表为空的话,
    // 将当前Hook作为第一个Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    
    
    // 否则将当前Hook添加到Hook链表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

In the mount phase, whenever we call the Hooks method, such as useState, mountState will call mountWorkInProgressHook to create a Hook node and add it to the Hooks list. Like our example:

const [age, setAge] = useState(initialAge);
  const [name, setName] = useState(initialName);
  useEffect(() => {
    
    })

Then in the mount phase, a singly linked list like the following figure will be produced: Both
insert image description here
useState and useReducer use a queue linked list to store each update. So that the later update phase can return the latest state. Every time we call the dispatchAction method, a new update object will be formed and added to the queue list, and this is a circular list. You can take a look at the implementation of the dispatchAction method:

// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action,) {
    
    
    const update = {
    
    
      action,
      next: null,
    };
    // 将update对象添加到循环链表中
    const last = queue.last;
    if (last === null) {
    
    
      // 链表为空,将当前更新作为第一个,并保持循环
      update.next = update;
    } else {
    
    
      const first = last.next;
      if (first !== null) {
    
    
        // 在最新的update对象后面插入新的update对象
        update.next = first;
      }
      last.next = update;
    }
    // 将表头保持在最新的update对象上
    queue.last = update;
   // 进行调度工作
    scheduleWork();
}

That is, every time we execute the dispatchAction method, such as setAge or setName. An update object that saves the update information will be created and added to the update list queue. Then each Hooks node will have its own queue. For example, suppose we executed the following statements:

setAge(19);
setAge(20);
setAge(21);

Then our Hooks linked list will become like this:
insert image description here

On the Hooks node, as shown in the figure above, all historical update operations are stored through a linked list. So that in the update phase, the latest values ​​can be obtained and returned to us through these updates. This is why after the first call to useState or useReducer, every update returns the latest value.
The Hooks linked list built by the component will be mounted on the memoizedState of the FiberNode node.

insert image description here

1.2 updateState

Then let's take a look at the update phase, that is, to see how our useState or useReducer uses the existing information to return us the latest and most correct value. Let's take a look at the code of useState in the update phase, which is updateState:

// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
    
    
  return updateReducer(basicStateReducer, initialState);
}

It can be seen that the bottom layer of updateState is actually updateReducer, because when we call useState, the reducer is not passed in, so a basicStateReducer will be passed in by default. Let's take a look at this basicStateReducer first:

// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
    
    
  return typeof action === 'function' ? action(state) : action;
} 

When using useState(action), action is usually a value, not a method. So what baseStateReducer has to do is to return this action. Let's continue to look at the logic of updateReducer:

// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
    
    
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新列表的表头
  const last = queue.last;

  // 获取最早的那个update对象
  first = last !== null ? last.next : null;

  if (first !== null) {
    
    
    let newState;
    let update = first;
    do {
    
    
      // 执行每一次更新,去更新状态
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}

In the update phase, that is, the second and third time of our component. . When executing useState or useReducer, it will traverse the circular list of update objects, execute each update to calculate the latest state and return it, so as to ensure that we can get the latest state every time we refresh the component. The reducer of useState is baseStateReducer, because the incoming update.action is a value, so it will directly return update.action, and the reducer of useReducer is a user-defined reducer, so it will be calculated step by step according to the incoming action and the newState obtained in each cycle out the latest status.
insert image description here

2.useReducer

const initialState = {
    
    age: 0, name: 'Dan'};

function reducer(state, action) {
    
    
  switch (action.type) {
    
    
    case 'increment':
      return {
    
    ...state, age: state.age + action.age};
    case 'decrement':
      return {
    
    ...state, age: state.age - action.age};
    default:
      throw new Error();
  }
}
function PersionInfo() {
    
    
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Age: {
    
    state.age}, Name: {
    
    state.name}
      <button onClick={
    
    () => dispatch({
    
    type: 'decrement', age: 1})}>-</button>
      <button onClick={
    
    () => dispatch({
    
    type: 'increment', age: 1})}>+</button>
    </>
  );
}

2.1 Supplement mountReducer

mountReducer, you will find that it is almost the same as mountState, but the initialization logic of the state is slightly different. After all, useState is actually a castrated version of useReducer

// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init) {
    
    
  // 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
  const hook = mountWorkInProgressHook();
  let initialState;
  // 初始化
  if (init !== undefined) {
    
    
    initialState = init(initialArg);
  } else {
    
    
    initialState = initialArg ;
  }
  hook.memoizedState = hook.baseState = initialState;
  // 存放更新对象的链表
  const queue = (hook.queue = {
    
    
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  // 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
 // 返回状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}

3. Summary

3.1 How does React manage Hooks?

React manages Hooks through a single-linked list,
and adds Hook nodes to the linked list in sequence according to the execution order of Hooks.

3.2 How do useState and useReducer return the latest value each time they are rendered?

Each Hook node remembers all the update operations through the circular linked list.
In the update phase, it will execute all the update operations in the update circular linked list in turn, and finally get the latest state and return

3.3 Why can't useState be placed in a conditional statement?

This is because React manages Hooks through a singly linked list.
In the update phase, each time useState is called, the updated result will be calculated according to its corresponding hook object and its update circular one-way chain.
So how to get the corresponding hook object? We use a pointer workInProgressHook to obtain it sequentially along the hooks linked list. Every time useState is called, the linked list will execute next and move backward one step, and workInProgressHook will point to the hook corresponding to the next useState The object can be directly used for calculation in the next useState.
If useState is written in the conditional judgment, assuming that the conditional judgment is not true, and the useState method inside is not executed, the value of all subsequent useState hook objects will be offset, resulting in an exception.
insert image description here

4.useEffect

function PersionInfo () {
    
    
  const [age, setAge] = useState(18);
  useEffect(() =>{
    
    
      console.log(age)
  }, [age])

 const [name, setName] = useState('Dan');
 useEffect(() =>{
    
    
      console.log(name)
  }, [name])
  return (
    <>
      ...
    </>
  );
}

When the PersionInfo component is rendered for the first time, it will output the age and name on the console. In each update of the subsequent components, if the value of the deps dependency in useEffect changes, the corresponding state will also be output on the console. At the same time The cleanup function (if any) will be executed when unmounting

How is it implemented in React? In fact, it is very simple. An updateQueue is used to store all the effects in the FiberNode, and then all the effects that need to be executed are executed sequentially after each rendering. useEffect is also divided into mountEffect and updateEffect

insert image description here

4.1 mountEffect

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑

function mountEffect( create,deps,) {
    
    
  return mountEffectImpl(
    create,
    deps,
  );
}

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    
    
  // 获取当前Hook,并把当前Hook添加到Hook链表
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将当前effect保存到对应的Hook节点的memoizedState属性上,
  // 以及添加到fiberNode的updateQueue上
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
    
    
  const effect: Effect = {
    
    
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // componentUpdateQueue 会被挂载到fiberNode的updateQueue上
  if (componentUpdateQueue === null) {
    
    
    // 如果当前Queue为空,将当前effect作为第一个节点
    componentUpdateQueue = createFunctionComponentUpdateQueue();
   // 保持循环
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    
    
    // 否则,添加到当前的Queue链表中
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
    
    
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
    
    
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // componentUpdateQueue.lastEffect永远指向最后一个插入的effect
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect; 
}

It can be seen that in the mount phase, what useEffect does is to add its own effect to the componentUpdateQueue. This componentUpdateQueue will be assigned to the updateQueue of fiberNode in the renderWithHooks method.

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
    
    
   const renderedWork = currentlyRenderingFiber;
   renderedWork.updateQueue = componentUpdateQueue;
}

That is, in the mount phase, all our effects are mounted on the fiberNode in the form of a linked list. Then after the component is rendered, React will execute all the methods in updateQueue.

insert image description here

4.2 updateEffect

The update phase is similar to the mount phase, except that this time the effect's dependency deps will be considered. If there is no change in the dependency of the updated effect, it will be marked as NoHookEffect, and finally the execution of the effect will be skipped in the commit phase.

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑

function updateEffect(create,deps){
    
    
  return updateEffectImpl(
    create,
    deps,
  );
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
    
    
  // 获取当前Hook节点,并把它添加到Hook链表
  const hook = updateWorkInProgressHook();
  // 依赖 
  const nextDeps = deps === undefined ? null : deps;
  // 清除函数
  let destroy = undefined;
  
  // 将当前effect保存到对应的Hook节点的memoizedState属性上,
  // 以及添加到fiberNode的updateQueue上
  if (currentHook !== null) {
    
    
    // 拿到前一次渲染该Hook节点的effect
    // hook对象上memoizedState存储了effect,effect.deps是前一次渲染时候的依赖
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
    
    
      const prevDeps = prevEffect.deps;
      // 对比deps依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
    
    
        // 如果依赖没有变化,就会打上NoHookEffect tag,在commit阶段会跳过此
        // effect的执行
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        return;
      }
    }
  }
 
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}

The commit phase skips the execution of the effect of NoHookEffect.

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 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
    
    
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
    
    
        // Mount 阶段执行所有tag !== NoHookEffect的effect.create,
        // 我们的清除函数(如果有)会被返回给destroy属性,等到unmount的时候执行
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

insert image description here

5. Summarize what useEffect does?

1. There will be an updateQueue linked list in the FiberNdoe node to store all the effects that need to be executed for this rendering.
2. The mountEffect and updateEffect stages will mount the effect to the updateQueue.
3. In the updateEffect stage, the effect whose deps has not changed will be marked with the NoHookEffect tag, and the effect will be skipped in the commit stage.

6. Complete diagraminsert image description here

References

1. React technology secret
2. React hooks source code analysis

Guess you like

Origin blog.csdn.net/m0_57307213/article/details/126990343