The principle of react-hooks

浅夏晴空
中文

I. Introduction

This article mainly react-hooks , from the perspective of principle and source code, and began to analyze the react-hooks . I believe that after this article, the hooks problems during the interview will be solved. The actual react-hooks is not so difficult to understand. It sounds very cool . In fact, the functional component solves a technical solution that does not have state , life cycle, and logic cannot reuse .

Hook is a new feature of React 16.8. It allows you to use state and other React features without writing classes.

The old rules, 🤔️🤔️🤔️ we started today’s discussion with questions ( can answer the last few, you can try it yourself, master ):

  • 1 When the function context of the stateless component is executed every time, react hooks record the state of 0608e782ad2376?
  • 2 What is the use of react-hooks to record the order of hooks Ask another way! Why can't you declare hooks conditional statement? hooks statement at the top of the component?
  • 3 function function components useState , and class class components setState What is the difference?
  • 4 How does react hooks , which is inside the function component?
  • 5 useEffect , useMemo , why useRef can access the latest changed value without dependency injection?
  • 6 useMemo cache the value? How to apply it to optimize performance?
  • 7 Why useState passed twice is the same, and the function component is not updated?
  • ...

图片

If you read this article carefully, all these problems will be solved.

The essential difference between function component and class component

In explanation react-hooks before principle, we want to deepen their understanding about, function class components and assemblies in the end what is the difference , ado, we look at two code fragments.

class Index extends React.Component<any,any>{
    constructor(props){
        super(props)
        this.state={
            number:0
        }
    }
    handerClick=()=>{
       for(let i = 0 ;i<5;i++){
           setTimeout(()=>{
               this.setState({ number:this.state.number+1 })
               console.log(this.state.number)
           },1000)
       }
    }

    render(){
        return <div>
            <button onClick={ this.handerClick } >num++</button>
        </div>
    }
}

Print the result?

Let's take a look at the function component:

function Index(){
    const [ num ,setNumber ] = React.useState(0)
    const handerClick=()=>{
        for(let i=0; i<5;i++ ){
           setTimeout(() => {
                setNumber(num+1)
                console.log(num)
           }, 1000)
        }
    }
    return <button onClick={ handerClick } >{ num }</button>
}

Print the result?

------------Announce the answer-------------

In the first example 🌰 prints the result: 1 2 3 4 5

In the second example 🌰 prints the result: 0 0 0 0 0

The real problem is to deceive, we have to analyze together, first class components, due to the implementation of setState not in react execution on the execution context of normal function, but setTimeout performed, batch updates condition is destroyed. I won’t talk about the principle here, so you can get the changed state .

But in stateless components, it doesn't seem to take effect. The reason is simple. In the class state, an instantiated class to maintain various states in the function component, there is no state to save this information. Every time the function context is executed, all variables and constants are Re-declare, complete the execution, and then be recycled by the garbage mechanism. So as above, no matter setTimeout executed, it is executed in the current function context. At this time, num = 0 will not change, and then setNumber will be executed. After the function component is executed again, num change.

Therefore, for the class component, we only need to instantiate it once, and the state state of the component is saved in the instance. For each update, you only need to call the render method. But in the function component, each update is a new function execution. In order to save some state, execute some side-effect hooks, react-hooks came into being to help record the state of the component and handle some additional side effects.

Second first acquaintance: lift the veil of hooks

1 What happened when we introduced hooks?

We start with the introduction of hooks , taking useState as an example, when we write this from the project:

import { useState } from 'react'

So we go to useState to see which god it is?

react/src/ReactHooks.js

useState

export function useState(initialState){
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useState() implementation of equal dispatcher.useState(initialState) there is the introduction of a dispatcher , we look at resolveDispatcher done?

resolveDispatcher

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current
  return dispatcher
}

ReactCurrentDispatcher

react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
  current: null,
};

We see that ReactCurrentDispatcher.current initialized as null , and then there is no more text. We can only write down ReactCurrentDispatcher for the time being. See ReactCurrentDispatcher is used?

2 Start the creation, starting with the function execution of stateless components

To fully understand hooks , we must start from its roots. When we introduced hooks , we ended up with a ReactCurrentDispatcher . The clues are all broken, so we can only start from the execution of the function component.

renderWithHooks execution function

When is the function component executed?

react-reconciler/src/ReactFiberBeginWork.js

function component initialization:

renderWithHooks(
    null,                // current Fiber
    workInProgress,      // workInProgress Fiber
    Component,           // 函数组件本身
    props,               // props
    context,             // 上下文
    renderExpirationTime,// 渲染 ExpirationTime
);

For initialization, there is no current tree. After a component update is completed, the current workInProgress tree will be assigned to the current tree.

function component update:

renderWithHooks(
    current,
    workInProgress,
    render,
    nextProps,
    context,
    renderExpirationTime,
);

We can see from the top, renderWithHooks function that is called call function component functions main function. Let's focus on what renderWithHooks done?

renderWithHooks react-reconciler/src/ReactFiberHooks.js

export function renderWithHooks(
  current,
  workInProgress,
  Component,
  props,
  secondArg,
  nextRenderExpirationTime,
) {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.expirationTime = NoWork;

  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

  let children = Component(props, secondArg);

  if (workInProgress.expirationTime === renderExpirationTime) { 
       // ....这里的逻辑我们先放一放
  }

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null
  workInProgressHook = null;

  didScheduleRenderPhaseUpdate = false;

  return children;
}

all components perform the function, here are methods , first of all we should understand a few gratitude, which we understand to follow useState is helpful.

current fiber tree: When a rendering is completed, a current tree will be generated, and current will be replaced with a real Dom tree commit

workInProgress fiber tree fiber tree to be reconciled and rendered. Once the new component is updated, a current will be copied as workInProgress . After the update is completed, the current workInProgress tree will be assigned to the current tree.

workInProgress.memoizedState : In the class component, memoizedState stores the state information, and in the function component, can be disclosed in advance, and memoizedState is stored in the form of a linked list of information hooks

workInProgress.expirationTime : react uses different expirationTime to determine the priority of the update.

currentHook : It can be understood that hooks node pointed to by the current

workInProgressHook : Understand hooks node pointed to by the workInProgress

renderWithHooks main function of the function:

memoizedState and updateQueue workInProgress tree that will be reconciled and rendered. Why do this? Because in the execution of the next function component, the new hooks information should be mounted on these two attributes, and then in the component commit stage , Replace the workInProgress tree with the current tree, and replace the real DOM element node. And save hooks information in the current

Then according to whether the current function component is rendered for the first time, assign ReactCurrentDispatcher.current different from hooks , and finally associate it with ReactCurrentDispatcher For the first rendering of the component, the HooksDispatcherOnMount hooks object is used. For the function component that needs to be updated after rendering, it is the HooksDispatcherOnUpdate object, so the two differences are current by whether memoizedState (hook information) is on the 0608e782ad3dc0 tree. If current does not exist, it proves to be the first rendering of the function component.

Next, calls Component(props, secondArg); execute our function component. Our function component is actually executed here. Then, the hooks we wrote is executed in turn, and the hooks information is sequentially saved to the workInProgress tree. As for how it is stored, we will talk about it shortly.

Next, it is also very important to assign ContextOnlyDispatcher to ReactCurrentDispatcher.current . Since js is single-threaded, that is to say, we are not in the function component. The hooks calls are all ContextOnlyDispatcher objects on the hooks object. 0608e782ad3eba, let's see what ContextOnlyDispatcher is .

const ContextOnlyDispatcher = {
    useState:throwInvalidHookError
}
function throwInvalidHookError() {
  invariant(
    false,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
}

It turns out that react-hooks uses this function component to perform the assignment of different hooks objects to determine whether the hooks is inside the function component, catching and throwing an exception.

Finally, reset some variables such as currentHook , currentlyRenderingFiber , workInProgressHook etc.

3 different hooks objects

hooks objects are called in the first rendering component and update component of the function. Let's take a look at HooksDispatcherOnMount and HooksDispatcherOnUpdate .

first rendering (I only show the commonly used hooks ):

const HooksDispatcherOnMount = {
  useCallback: mountCallback,
  useEffect: mountEffect,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
};

update component:

const HooksDispatcherOnUpdate = {
  useCallback: updateCallback,
  useEffect: updateEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState
};

It seems that for the first rendering component and the updated component, react-hooks uses two sets of Api . The second and third parts of this article will focus on the connection between the two.

We use a flowchart to describe the entire process:

图片 17AC0A26-745A-4FD8-B91B-7CADB717234C.jpg

Three hooks are initialized, what will happen to the hooks we write

This article will focus on the four focus hooks expansion, they are responsible for component updates useState , responsible for the implementation of side effects useEffect , responsible for the preservation of data useRef , responsible for caching optimization useMemo , as for useCallback , useReducer , useLayoutEffect principle and that four key hooks compare They are similar, so I won’t explain them one by one.

Let's write a component first, and use the above four main hooks :

Please remember the following code snippets, the following explanation will be expanded with the following code snippets

import React , { useEffect , useState , useRef , useMemo  } from 'react'
function Index(){
    const [ number , setNumber ] = useState(0)
    const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[])
    const curRef  = useRef(null)
    useEffect(()=>{
       console.log(curRef.current)
    },[])
    return <div ref={ curRef } >
        hello,world { number } 
        { DivDemo }
        <button onClick={() => setNumber(number+1) } >number++</button>
     </div>
}

Next, let's study together what we wrote above the four hooks will eventually become?

1 mountWorkInProgressHook

When component initialization, every hooks execution, such as useState() , useRef() , will call mountWorkInProgressHook , mountWorkInProgressHook in the end do what to write, let us work together to analyze:

react-reconciler/src/ReactFiberHooks.js \-> mountWorkInProgressHook
function mountWorkInProgressHook() {
  const hook: Hook = {
    memoizedState: null,  // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountWorkInProgressHook function does very simple things. First, each time a hooks function is hook object is generated, which stores the current hook information, and then each hooks is connected in a linked list and assigned to workInProgress of memoizedState . It also confirms the above-mentioned, the function component uses memoizedState store the hooks linked list.

As for what information is retained in the hook Let me introduce them separately:

memoizedState : in useState save state information | useEffect save the effect objects | useMemo stored value is cached and deps | useRef saved is ref object.

baseQueue : usestate and useReducer save the latest update queue.

baseState : usestate and useReducer , in one update, the latest state value generated.

queue : save the queue to be updated pendingQueue , update function dispatch and other information.

next : Point to the next hooks object.

Then when our function component is executed, the four hooks and workInProgress will be in the relationship as shown in the figure.

图片 shunxu.jpg

After knowing each hooks relationship, we should understand why we can't declare hooks conditional statement.

We use a picture to show what happens if we declare in a conditional statement.

If we put one of the above demo , useRef into the conditional statement,

 let curRef  = null
 if(isFisrt){
  curRef = useRef(null)
 }

图片 hoo11.jpg

because once declared in a conditional statement hooks , the next component update function, hooks chain structure, will be destroyed, current tree memoizedState cache hooks information, and current workInProgress inconsistency, if related to the read state other operations, occurs abnormal.

The above describes what hooks uses to prove uniqueness. The answer is through the order of the hooks And why can’t you declare hooks in the conditional statement. Next, we will follow the four directions to introduce what happened during initialization?

2 Initialize useState -> mountState

mountState

function mountState(
  initialState
){
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // 如果 useState 第一个参数为函数,执行函数得到state
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,  // 带更新的
    dispatch: null, // 负责更新函数
    lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
    lastRenderedState: initialState, // 最后一次得到的 state
  });

  const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数
    null,
    currentlyRenderingFiber,
    queue,
  )))
  return [hook.memoizedState, dispatch];
}

mountState done in the end, it will first get initialized state , assign it to mountWorkInProgressHook generated hook object memoizedState and baseState property, and then create a queue objects, which holds responsible for updating the information.

Let me talk about here, in the absence of the state assembly, useState and useReducer method of triggering function updates are dispatchAction , useState , can be seen as a simplified version of useReducer , as for dispatchAction how to update state , updated components, we then studied down dispatchAction .

Before we study must first figure out dispatchAction what is?

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
)
const [ number , setNumber ] = useState(0)

dispatchAction is setNumber , dispatchAction the first parameter and the second parameter, has been bind to change currentlyRenderingFiber and queue , our incoming parameter is the third argument action

dispatchAction stateless component update mechanism

As the main function of the update, let’s take a look at it. I dispatchAction , streamlined, streamlined,

function dispatchAction(fiber, queue, action) {

  // 计算 expirationTime 过程略过。
  /* 创建一个update */
  const update= {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  }
  /* 把创建的update */
  const pending = queue.pending;
  if (pending === null) {  // 证明第一次更新
    update.next = update;
  } else { // 不是第一次更新
    update.next = pending.next;
    pending.next = update;
  }
  
  queue.pending = update;
  const alternate = fiber.alternate;
  /* 判断当前是否在渲染阶段 */
  if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else { /* 当前函数组件对应fiber没有处于调和渲染阶段 ,那么获取最新state , 执行更新 */
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState = queue.lastRenderedState; /* 上一次的state */
          const eagerState = lastRenderedReducer(currentState, action); /**/
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) { 
            return
          }
        } 
      }
    }
    scheduleUpdateOnFiber(fiber, expirationTime);
  }
}

Whether component class calls setState , components or functions dispatchAction , will have a update objects, which records information on the update, then this update placed to be updated pending queue, dispatchAction second step is to determine the current function component Whether the fiber object is in the rendering stage, if it is in the rendering stage, then we do not need to update the current function components, just update the current update expirationTime .

If the current fiber not in the update phase. Then get the latest state by calling lastRenderedReducer , and compare it with the last currentState . If they are equal, then exit. This confirms why useState , when the two values are equal, the component does not render. This mechanism and Component in mode setState is quite different.

If the two times state not equal, then call scheduleUpdateOnFiber schedule rendering current fiber , scheduleUpdateOnFiber is the main function react

We initialization mountState * and * stateless component update mechanism . Next, let’s look at other 1608e782ad5462 hooks What are the operations done by the 1608e782ad5463 initialization?

3 Initialize useEffect -> mountEffect

The above mentioned the fiber object memoizedState stateless component to save the linked list formed by the hooks So what information is stored in updateQueue ? We will find the answer in the process of useEffect When we call useEffect mountEffect method will be called when the component is rendered for the first time. What exactly does this method do?

mountEffect

function mountEffect(
  create,
  deps,
) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, 
    create, // useEffect 第一次参数,就是副作用函数
    undefined,
    nextDeps, // useEffect 第二次参数,deps
  );
}

Each hooks initialization creates a hook object, and then hook the memoizedState save the current effect hook information.

There are two memoizedState Don’t confuse everyone, I will remind you again

  • workInProgress / current tree memoizedState contains the current function of each component hooks list form.
  • Each hooks on memoizedState save the current hooks information, different kinds of hooks of memoizedState different content. The above method finally executed a pushEffect , let's take a look at what pushEffect did?

pushEffect creates an effect object and mounts updateQueue

function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null,
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue
  if (componentUpdateQueue === null) { // 如果是第一个 useEffect
    componentUpdateQueue = {  lastEffect: null  }
    currentlyRenderingFiber.updateQueue = componentUpdateQueue
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {  // 存在多个effect
    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;
    }
  }
  return effect;
}

This paragraph is actually very simple. First, create a effect , and determine if the component is rendered for the first time, then create componentUpdateQueue , which is workInProgress of updateQueue . Then put effect into updateQueue .

Suppose we write this in a functional component:

useEffect(()=>{
    console.log(1)
},[ props.a ])
useEffect(()=>{
    console.log(2)
},[])
useEffect(()=>{
    console.log(3)
},[])

Finally workInProgress.updateQueue will be saved in this form:

图片 7B8889E7-05B3-4BC4-870A-0D4C1CDF6981.jpg

Extension: effectList

effect list can be understood as a effectTag side effects list. It is a singly linked list structure composed of the fiber node and the pointer nextEffect , which also includes the first node firstEffect , and the last node lastEffect . React uses a depth-first search algorithm. When traversing the fiber render fiber with side effects is filtered out, and finally a effect list linked list with only side effects is constructed. In commit stage, React get effect list the data, by traversing effect list , and according to each effect node effectTag type, performing each effect , so that the corresponding DOM execution tree changes.

4 Initialize useMemo -> mountMemo

I don’t know if you useMemo is too complicated. Compared to other useState , useEffect etc., its logic is actually very simple.

function mountMemo(nextCreate,deps){
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Initializing useMemo is to create a hook , and then execute useMemo to get the value that needs to be cached, then deps , and assign it to the current hook memoizedState . There is no complicated logic overall.

5 Initialize useRef -> mountRef

For useRef initialization processing, it seems to be even simpler, let's take a look together:

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

mountRef initialization is simply to create a ref objects, object current property to hold the value of initialization, and finally memoizedState save ref , to complete the operation.

6 Summary of mounted stage hooks

We summarize the initialization phase, react-hooks do things in a first rendering component function execution context process, each react-hooks execution, will have a hook object and form a linked list structure, bound workInProgress of memoizedState on the property, Then the state on react-hooks is bound to the memoizedState attribute of the hooks For effect side-effect hooks, they will be bound to workInProgress.updateQueue , wait until the commit stage, dom tree construction is completed, and execute each effect side-effect hook.

Four hooks update stage

The above introduces the first rendering function component, react-hooks initialization does, then we analyze it,

For the update phase, it means that the workInProgress tree has been assigned to the current tree last time. memoizedState , which stores hooks information, already exists on the current tree. The react processing logic for hooks is similar to the fiber

For a function component updates, when executed again hooks time functions, such as useState(0) , first from current of hooks found in the current workInProgressHook , corresponding currentHooks , then copy currentHooks to workInProgressHook , next hooks time function execution, the The latest status is updated to workInProgressHook to ensure that the hooks status is not lost.

So every time the function component is updated, every time the react-hooks function is executed, there needs to be a function to do the above operation, this function is updateWorkInProgressHook , let's look at this updateWorkInProgressHook together.

1 updateWorkInProgressHook

function updateWorkInProgressHook() {
  let nextCurrentHook;
  if (currentHook === null) {  /* 如果 currentHook = null 证明它是第一个hooks */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else { /* 不是第一个hooks,那么指向下一个 hooks */
    nextCurrentHook = currentHook.next;
  }
  let nextWorkInProgressHook
  if (workInProgressHook === null) {  //第一次执行hooks
    // 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else { 
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) { 
      /* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
    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,
    };
    if (workInProgressHook === null) { // 如果是第一个hooks
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else { // 重新更新 hook
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

The logic of this paragraph is roughly like this:

  • First, if it is the first time to execute the hooks function, then take out memoizedState current tree, which is the old hooks .
  • Then declare variables nextWorkInProgressHook , there should be worth noting that, under normal circumstances, once renderWithHooks execution, workInProgress on memoizedState will be left blank, hooks perform functions order, nextWorkInProgressHook should have been as null , then under what circumstances nextWorkInProgressHook not null , that is, when renderWithHooks execution of 0608e782ad6019, the function component was executed many times, which is the logic renderWithHooks
  if (workInProgress.expirationTime === renderExpirationTime) { 
       // ....这里的逻辑我们先放一放
  }

The logic here is actually to determine that if the current function component is still in the rendering priority after the current function component is executed, it means that the function component has a new update task, then the function component is executed cyclically. This causes the above-mentioned situation where nextWorkInProgressHook not null

  • Finally, copy the current of hooks and assign it to workInProgressHook , which is used to update a new round of hooks status.

Next, let's take a look at the four types of hooks . In a component update, we did those operations separately.

2 updateState

useState

function updateReducer(
  reducer,
  initialArg,
  init,
){
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current = currentHook;
  let baseQueue = current.baseQueue;
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
     // 这里省略... 第一步:将 pending  queue 合并到 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 {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) { //优先级不足
        const clone  = {
          expirationTime: update.expirationTime,
          ...
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {  //此更新确实具有足够的优先级。
        if (newBaseQueueLast !== null) {
          const clone= {
            expirationTime: Sync, 
             ...
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        /* 得到新的 state */
        newState = reducer(newState, action);
      }
      update = update.next;
    } while (update !== null && update !== first);
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  const dispatch = queue.dispatch
  return [hook.memoizedState, dispatch];
}

This paragraph looks very complicated, let us slowly understand it, first merge the last updated pending queue into basequeue , why do we want to do this, for example, we write this in the click event again,

function Index(){
   const [ number ,setNumber ] = useState(0)
   const handerClick = ()=>{
    //    setNumber(1)
    //    setNumber(2)
    //    setNumber(3)
       setNumber(state=>state+1)
       // 获取上次 state = 1 
       setNumber(state=>state+1)
       // 获取上次 state = 2
       setNumber(state=>state+1)
   }
   console.log(number) // 3 
   return <div>
       <div>{ number }</div>
       <button onClick={ ()=> handerClick() } >点击</button>
   </div>
}

Click the button, print 3

Three setNumber generated update will for the time being into pending queue , the next component function execution time, three times update be merged into baseQueue . The structure is as follows:

图片 setState.jpg

Next will present useState or useReduer corresponding hooks on baseState and baseQueue update to the latest state. Will cycle baseQueue of update , copy update , update expirationTime , for there is enough priority update (above three setNumber produced update have sufficient priority), we want to get the latest state state. , Will execute each useState action . Get the latest state .

update state

图片 sset1.jpg

There are two questions here🤔️:

  • Question 1: action n’t it enough to execute the last 0608e782ad67c6?

Answer: The reason is simple. The logic of useReducer similar to useState If the first parameter is a function, it will reference the update generated by the state needs to be called cyclically, each update of reducer , if setNumber(2) is the case of 0608e782ad6858, then only use the updated value, then setNumber(state=>state+1) Enter the last state get the latest state .

  • Question 2: Under what circumstances will there be insufficient priority ( updateExpirationTime < renderExpirationTime )?

Answer: This situation usually occurs when we call setNumber and call scheduleUpdateOnFiber render the current component, and a new update is generated, so the final execution of reducer update state task is handed over to the next update.

3 updateEffect

function updateEffect(create, deps): void {
  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;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookEffectTag, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber.effectTag |= fiberEffectTag
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}

useEffect do is very simple, judge twice deps equal, if not need to perform this update instructions are equal, then a direct call pushEffect , where attention effect label, hookEffectTag , if not equal, then update effect , and assigned to hook.memoizedState , here The label is HookHasEffect | hookEffectTag , and then in the commit stage, react will use the label to determine whether to execute the current effect function.

4 updateMemo

function updateMemo(
  nextCreate,
  deps,
) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
  const prevState = hook.memoizedState; 
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1]; // 之前保存的 deps 值
      if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

In the component update process, we execute the useMemo function. What we do is actually very simple. It is to determine whether the two deps are equal. If you don’t want to wait and prove that the dependency changes, then execute useMemo to get the new value Then re-assign the value to hook.memoizedState . If the equality proves that there is no dependency change, then the cached value is directly obtained.

But here is one point. It is worth noting that nextCreate() executed, if usestate is referenced, the variable will be referenced and cannot be recycled by the garbage collection mechanism. This is the principle of closure. Then the accessed attribute may not be the latest value, so you need to The referenced value is added to the dependency dep array. Every time dep changed and re-executed, there will be no problems.

Warm tips: Many students said useMemo , what scene is it used, and will it be counterproductive? Through the analysis of the source code principle, I can clearly say that it can basically be used with confidence. To put it bluntly, it can be customized. To change the cache, the stored value is just a value.

5 updateRef

function updateRef(initialValue){
  const hook = updateWorkInProgressHook()
  return hook.memoizedState
}

The function component updates useRef to do a simpler thing, that is, it returns the cached value, that is, no matter how the function component is executed, how many times it is executed, hook.memoizedState memory points to an object, so it explains why useEffect , useMemo , useRef not Dependency injection is needed to access the latest changed value.

One click event update

图片 91A72028-3A38-4491-9375-0895F420B7CD.jpg

Five summary

Above, we have function component to function component update rendering , two-dimensional decomposition explained the react-hooks , mastered the react-hooks and internal operating mechanism, which will help us in our work, and better use react-hooks .

阅读 564

大前端技术栈
大前端技术汇总
avatar
浅夏晴空
前端开发工程师

基于大前端端技术的一些探索反思总结及讨论

4.6k 声望
3.5k 粉丝
0 条评论
你知道吗?

avatar
浅夏晴空
前端开发工程师

基于大前端端技术的一些探索反思总结及讨论

4.6k 声望
3.5k 粉丝
宣传栏