Probe into six weird phenomena of react
Front end GOGO 2021-06-04 11:00:04

Preface

Today we have an unusual React Advanced articles , In this paper, we will discuss some Unusual The phenomenon , Analyze the reasons with the process of investigation , Find the result , So as to know React, Walk into React The world of , uncover React The veil of , I'm convinced that the , A deeper understanding of , In order to better use .

I admit that the name may be a bit of a headline party , It's inspired by a CCTV program called 《 Into Science 》 Column of , Introduce all kinds of supernatural phenomena every day , It's amazing , At the end of the day, it turned out to be all kinds of pediatric problems , Now I think it's funny . But what I'm introducing today React ' Psychic ' The essence of phenomenon is not pediatrics , Every phenomenon reveals React Operating mechanism and Design principle .( We're talking about react The version is 16.13.1

 picture

well , I don't say much nonsense , My great detectives ,are you ready ? Let's start today's journey of revealing secrets .

Case 1 : The component is repeatedly mounted for no reason

Received a report

A former classmate came across a strange situation , He wants to update the components ,componentDidUpdate Do what you want to do after execution , Component updates come from the parent component passing props Changes . But the parent component changes props Find view rendering , however componentDidUpdate No implementation , What's more weird is componentDidMount perform . The code is as follows :

// TODO:  Repeat mount 
class Index extends React.Component{
   componentDidMount(){
     console.log(' Component initialization mount ')
   }
   componentDidUpdate(){
     console.log(' Component update ')
     /*  Want to do something  */
   }
   render(){
      return <div>《React Advanced practice guide 》   { this.props.number } +   </div>
   }
}

The effect is as follows

 picture
didupdate.gif

componentDidUpdate No implementation ,componentDidMount perform , Explain that the component is basically No update logic , It is Let's go. Repeat the mount .

Check one by one

The subassemblies are confused , No reason at all , We have to start with the parent component . Let's see how the parent component is written .

const BoxStyle = ({ children })=><div className='card' >{ children }</div>

export default function Home(){
   const [ number , setNumber ] = useState(0)
   const NewIndex = () => <BoxStyle><Index number={number}  /></BoxStyle>
   return <div>
      <NewIndex  />
      <button onClick={ ()=>setNumber(number+1) } > give the thumbs-up </button>
   </div>

}

Some clues are found in the parent component . In parent component , First, through BoxStyle As a container component , Add the style , Render our subcomponents Index, But each time a new component is formed by combining container components NewIndex, What's really mounted is NewIndex, The truth .

matters needing attention

The nature of the situation , It's every time render In the process , They all form a new component , For new components ,React The processing logic is to unload the old components directly , Remount the new component , So in the process of our development , Pay attention to one problem, that is :

  • For function components , Don't declare new components and render them in their function execution context , In this way, each function update will cause the component to mount repeatedly .
  • For class components , Not in render Function , Do the same thing as above , Otherwise, the child components will be mounted repeatedly .

Case two : Event source e.target disappear mysteriously

Unexpected cases

alias ( Xiao Ming ) On a dark and windy night , Whim write a controlled component . What's written is as follows :

export default class EventDemo extends React.Component{
  constructor(props){
    super(props)
    this.state={
        value:''
    }
  }
  handerChange(e){
    setTimeout(()=>{
       this.setState({
         value:e.target.value
       })
    },0)
  }
  render(){
    return <div>
      <input placeholder=" Please enter a user name ?" onChange={ this.handerChange.bind(this) }  />
    </div>

  }
}

input The value of is affected by state in value Attribute control , Xiao Ming wants to pass handerChange change value value , But he expects to be in setTimeout Complete the update in . When he wants to change input It's time , Something unexpected happened .

 picture
event.jpg

The console reported an error as shown above .Cannot read property 'value' of null That is to say e.target by null. Event source target How to say no, No ?

Lead tracking

After receiving this case , Let's check the problem first , So let's start with handerChange Print directly e.target, as follows :

 picture
event1.jpg

It seems that the first step is not handerChange Why , And then we went on to setTimeout I found that :

 picture
event2.jpg

Sure enough setTimeout Why , Why? setTimeout Event sources in e.target Just disappeared ? First , The source of the incident must not have disappeared somehow , sure React The bottom layer does some extra processing to the event source , First we know that React It's using Event synthesis Mechanism , That's bound onChange Not really bound change event , It's bound by Xiao Ming handerChange It's not really an event handler . So in other words React The bottom layer helps us deal with the source of events . It's all possible that only we can learn from React Find clues in the source code . After checking the source code , I found a very suspicious clue .

react-dom/src/events/DOMLegacyEventPluginSystem.js


function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
    const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
    batchedEventUpdates(handleTopLevel, bookKeeping);
}

dispatchEventForLegacyPluginEventSystem yes legacy In mode , The main function through which all events must pass ,batchedEventUpdates It's the logic of batch update , It will execute our real event handling functions , We talked about it in the principle of events chapter nativeEvent Namely Truly native event objects event.targetInst Namely e.target Corresponding fiber object . We are handerChange The event source is React Synthetic event sources , So know when the event source is , How to be synthesized ? It may help to solve the case .

In principle of events, we will introduce React Using event plug-in mechanism , Like our onClick The event corresponds to SimpleEventPlugin, So Xiao Ming wrote onChange There are also special ChangeEventPlugin Event plug-ins , One of the most important functions of these plug-ins is to synthesize our event source objects e, So let's take a look at ChangeEventPlugin.

react-dom/src/events/ChangeEventPlugin.js

const ChangeEventPlugin ={
   eventTypes: eventTypes,
   extractEvents:function(){
        const event = SyntheticEvent.getPooled(
            eventTypes.change,
            inst, //  Component instance
            nativeEvent, //  Native event sources  e
            target,      //  Native e.target
     );
     accumulateTwoPhaseListeners(event); //  This function handles real event functions in accordance with bubble capture logic , That is to say   handerChange  event
     return event; // 
   }   
}

We see the event source of the composite event handerChange Medium e, Namely SyntheticEvent.getPooled created . So this is the key to solving the case .

legacy-events/SyntheticEvent.js

SyntheticEvent.getPooled = function(){
    const EventConstructor = this//  SyntheticEvent
    if (EventConstructor.eventPool.length) {
    const instance = EventConstructor.eventPool.pop();
    EventConstructor.call(instance,dispatchConfig,targetInst,nativeEvent,nativeInst,);
    return instance;
  }
  return new EventConstructor(dispatchConfig,targetInst,nativeEvent,nativeInst,);
}

Off the coast : In the event system Chapter , The concept of event pool in this article , I'm in a hurry , general , This section of this article will supplement the event pool in detail Concept .

getPooled The real concept of event pool is introduced , It does two main things :

  • Determine if there are free event sources in the event pool , If there is an event source reuse .
  • without , adopt new SyntheticEvent Create a new event source object in the same way . that SyntheticEvent Is to create the constructor of the event source object , Let's study it together .
const EventInterface = {
  typenull,
  targetnull,
  currentTargetfunction({
    return null;
  },
  eventPhasenull,
  ...
};
function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
  this.dispatchConfig = dispatchConfig; 
  this._targetInst = targetInst;    //  Component correspondence fiber.
  this.nativeEvent = nativeEvent;   //  Native event sources .
  this._dispatchListeners = null;   //  Store all the event listener functions .
  for (const propName in Interface) {
      if (propName === 'target') {
        this.target = nativeEventTarget; //  What we actually print  target  It's here
      } else {
        this[propName] = nativeEvent[propName];
      }
  }
}
SyntheticEvent.prototype.preventDefault = function ()/* .... */ }     /*  Component browser default behavior  */
SyntheticEvent.prototype.stopPropagation = function (/* .... */  }  /*  Stop the event from bubbling  */

SyntheticEvent.prototype.destructor = function ()/*  Situation event source object */
      for (const propName in Interface) {
           this[propName] = null
      }
    this.dispatchConfig = null;
    this._targetInst = null;
    this.nativeEvent = null;
}
const EVENT_POOL_SIZE = 10/*  Maximum number of event pools  */
SyntheticEvent.eventPool = [] /*  Binding event pool  */
SyntheticEvent.release=function ()/*  Clear the event source object , If the event pool upper limit is not exceeded , So put it back in the event pool  */
    const EventConstructor = this
    event.destructor();
    if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
       EventConstructor.eventPool.push(event);
    }
}

After I refine this piece of code , The truth gradually came to the surface , Let's take a look first SyntheticEvent What did you do :

  • First, give some initialization variables nativeEvent etc. . And then according to EventInterface Rules Native event sources Properties on , Make a copy for React Event source . And then one of the most important things is that we print e.target Namely this.target, When the event source is initialized, the real e.target->nativeEventTarget

  • then React Event source , Bind your own block default behavior preventDefault, To prevent a bubble stopPropagation Other methods . But there's a key approach here, which is destructor, This function is empty React Your own event source object . So we finally got the answer , Our source of events e.target That's why the probability of disappearance is so high destructor,destructor stay release Is triggered , Then put the event source into the event pool , Wait for the next reuse .

Now all the spearheads are pointing at release, that release When did it trigger ?

legacy-events/SyntheticEvent.js

function executeDispatchesAndRelease(){
    event.constructor.release(event);
}

When React The event system performs all of the _dispatchListeners, Will trigger this method executeDispatchesAndRelease Release the current event source .

The truth

Back to Xiao Ming's problem , We said above ,React Finally, the event source will be set to null synchronously , And then put it in the event pool , because setTimeout It's asynchronous execution , When executing, the event source object has been reset and the event pool will be released , So we print e.target = null, Only this and nothing more , The truth of the case came out .

Through this case, we understand that React Some concepts of event pooling :

  • React The event system has unique composite events , It has its own source of events , And there are some special cases of processing logic , For example, bubble logic and so on .
  • React To prevent the creation of an event source object for each event , Waste performance , So we introduced The concept of event pool , Each user event will take one from the event pool e, without , Just create one , The event source is then assigned , After the event is executed , Reset event source , Put it back in the event pool , To reuse .

Show... With a flow chart :

 picture
eventloop.jpg

Case three : True and false React

crime scene

This is what happened to me , In development React Project time , For logic reuse , I put some packaged custom Hooks Upload to the company's private package On the management platform , Developing another React When the project is , Download the company package , Used inside the component . The code is as follows :

function Index({classes, onSubmit, isUpgrade}{
   /* useFormQueryChange  It's my custom hooks, And upload it to a private library , It is mainly used for the unified management of form controls   */
  const {setFormItem, reset, formData} = useFormQueryChange()
  React.useEffect(() => {
    if (isUpgrade)  reset()
  }, [ isUpgrade ])
  return <form
    className={classes.bootstrapRoot}
    autoComplete='off'
  >

    <div className='btnbox' >
       { /*  Here is the business logic , Has been omitted  */ }
    </div>
  </form>

}

useFormQueryChange It's my custom hooks , And upload it to a private library , It is mainly used for the unified management of form controls , I didn't expect that the introduction would be popular . The error is as follows :

 picture
hooks.jpg

Check one by one

We are in accordance with the React The content of the error report , Check the problem one by one :

  • The first possible cause of error You might have mismatching versions of React and the renderer (such as React DOM), intend React and React Dom Version inconsistency , To cause this , But in our project React and React Dom All are v16.13.1, So get rid of this .

  • The second possible cause of error You might be breaking the Rules of Hooks It means you broke Hooks The rules , It's also impossible , Because there is no damage in my code hoos The behavior of rules . So the suspicion is also ruled out .

  • The third possible cause of error You might have more than one copy of React in the same app It means in the same application , There may be more than one React. So far, all the suspects point to the third , First, we refer to the custom hooks, Will there be another one inside React Well ?

According to the above tips, I found the custom hooks Corresponding node_modules There's another one in it React, This is it false React( Let's call it fake React) Ghost . We are Hooks principle As mentioned in the article ,React Hooks use ReactCurrentDispatcher.current In component initialization , The component update phase gives different hooks object , After the update, give ContextOnlyDispatcher, If you call the hooks, You'll report the above error , That explains This mistake is because our project , Introduced by execution context React It's the project itself React, But custom Hooks The quotation is false React Hooks Medium ContextOnlyDispatcher

Next, I see in the component library package.json in ,

"dependencies": {
  "react""^16.13.1",
  "react-dom""^16.13.1"
},

Turned out to be React As dependencies So download custom Hooks When , hold React I downloaded it again . So how to solve this problem . For packaging React Component library ,hooks library , Out-of-service dependencies, Because it will be in the current dependencies Download to custom for dependencies hooks Under the library node_modules in . Instead, we should use peerDependencies, Use peerDependencies, Customize hooks We'll go to our project if we find related dependencies node_modules In looking for , Can fundamentally solve this problem . So we change it like this

"peerDependencies": {
    "react"">=16.8",
    "react-dom"">=16.8",
},

It solved the problem perfectly .

Poke the fog away

This question makes us understand as follows :

  • For some hooks library , Component library , Its own dependence , Already in the project , So use peerDependencies Statement .

  • In the process of development , It's possible to use different versions of the same dependency , For example, the project introduced A Version dependency , The component library introduces B Version dependency . So how to deal with this situation . stay package.json The document provides a resolutions Configuration items can solve this problem , stay resolutions Lock the same import version in , This will not cause the problem caused by the existence of multiple versions of project dependencies .

project package.json That's how it's written

{
  "resolutions": {
    "react""16.13.1",
    "react-dom""16.13.1"
  },
}

This way, regardless of the dependencies in the project , Or other library dependencies , Will use the same version , Fundamentally solve the problem of multiple versions .

Case four :PureComponet/memo Functional failure problems

Description of the case

stay React When developing , But we want to use PureComponent Do performance optimization , Adjust component rendering , But after writing a piece of code , Find out PureComponent The function failed , The specific code is as follows :


class Index extends React.PureComponent{
   render(){
     console.log(' Component rendering ')
     const { name , type } = this.props
     return <div>
       hello , my name is { name }
       let us learn { type }
     </div>

   }
}

export default function Home (){
   const [ number , setNumber  ] = React.useState(0)
   const [ type , setType ] = React.useState('react')
   const changeName = (name) => {
       setType(name)
   }
   return <div>
       <span>{ number }</span><br/>
       <button onClick={ ()=> setNumber(number + 1) } >change number</button>
       <Index type={type}  changeType={ changeName } name="alien"  />
   </div>

}

We had expected :

  • about Index Components , Only props in name and type change , To make components render . But the reality is that :

Click on the button effect :

 picture
purecomponent.gif

when the water subsides , the rocks emerge

Why does this happen ? Let's check again Index Components , Find out Index There is one on the component changeType, So is it the reason for this ? Let's analyze , First, the state update is in the parent component Home On ,Home Each component update produces a new changeName, therefore Index Of PureComponent Every time Shallow comparison , Find out props Medium changeName It's not equal every time , So it's updated , It gives us an intuitive sense that it's not working .

So how to solve this problem ,React hooks Provided in useCallback, It can be done to props The incoming callback function is cached , Let's change it Home Code .

const changeName = React.useCallback((name) => {
    setType(name)
},[])

effect :

 picture
pureComponent1.gif

In this way, the problem is fundamentally solved , use useCallback Yes changeName Function to cache , In every time Home Component execution , as long as useCallback in deps No change ,changeName The memory space also points to the original function , such PureComponent Shallow comparison will find that it is the same changeName, So you don't render components , So far, the case has been solved .

further

We use function components + Class component development , If used React.memo React.PureComponent etc. api, Note the way events are bound to these components , If it's a function component , So if you want to keep Component only rendering control features Words , Then please use useCallback,useMemo etc. api Handle , If it's a class component , Do not bind events with arrow functions , Arrow functions can also cause failures .

There is a shallow comparison mentioned above shallowEqual, Next let's focus on PureComponent How is it? shallowEqual, Now we're going to take a closer look at shallowEqual The secret of . Then we can start with the update of class rents .

react-reconciler/src/ReactFiberClassComponent.js

function updateClassInstance(){
    const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );
    return shouldUpdate
}

I'm here to simplify updateClassInstance, It's only about PureComponent Part of .updateClassInstance This function is mainly used to , Execution lifecycle , to update state, Determine whether the component is re rendered , Back to shouldUpdate Used to determine whether the current class component is rendered .checkHasForceUpdateAfterProcessing Check if the update source is the same as forceUpdate , If it is forceUpdate Components are bound to be updated ,checkShouldComponentUpdate Check that the component is rendered . Let's take a look at the logic of this function .

function checkShouldComponentUpdate(){
    /*  The life cycle of the class component is executed here  shouldComponentUpdate */
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    /*  Here we determine whether the component is  PureComponent  Pure components , If it's a pure component, it calls  shallowEqual  Shallow comparison   */
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return (
        !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
        );
    }
}

checkShouldComponentUpdate There are two crucial roles :

  • The first is if a class component has a lifecycle shouldComponentUpdate, Will execute the life cycle shouldComponentUpdate, Determine whether the component is rendered .
  • If it turns out to be a pure component PureComponent, It will be shallower and older props and state Whether it is equal or not , If equal , The component is not updated . isPureReactComponent It's that we use PureComponent The logo of , It turns out to be pure components .

Next is the point shallowEqual, With props As an example , Let's see .

shared/shallowEqual

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) { // is Sure   Comprehend   objA === objB  So return equal
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;  
  } //  If new and old props There's one that's not an object , Or it doesn't exist , Then go straight back false

  const keysA = Object.keys(objA); //  The old props /  The old state key Array of components
  const keysB = Object.keys(objB); //  new props /  new state key Array of components

  if (keysA.length !== keysB.length) { //  explain props Increase or decrease , So go straight back and don't want to wait
    return false;
  }

  for (let i = 0; i < keysA.length; i++) { //  Go through the old props , Discover new props No, , Or old and new props Different, etc , Then the return does not update the component .
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true// The default return is equal
}

shallowEqual The process goes like this ,shallowEqual return true Then prove equal , So don't update components ; If you return false I don't want to wait , So update the component .is We can understand it as ===

  • First step , Directly through === Judge whether it is equal , If equal , Then the return true. Normally, just call React.createElement Will recreate props, props It's all unequal .
  • The second step , If new and old props There's one that's not an object , Or it doesn't exist , Then go straight back false.
  • The third step , Judge the old and the new props, key I don't want to wait , explain props There is an increase or decrease in , Then go straight back false.
  • Step four , Go through the old props , Discover new props There's no corresponding , Or old and new props Different, etc , Then the return false.
  • Default return true.

This is it. shallowEqual Logic , The code is very simple . Interested students can have a look at .

Case five : useState Update the same State, Function components execute 2 Time

Received a report

It's a real problem , You may not notice , What caught my attention was a question from a friend of nuggets , Questions as follows :

 picture First of all, thank you very much for your report , I am here React-hooks principle As mentioned in , For the method function component of the update component useState And class components setState There's a difference ,useState In the source code, if you encounter the same two times state, By default, the component will be prevented from being updated , But in class components setState If not set PureComponent, Two times the same state Will update .

Let's review hooks How to prevent component updates in .

react-reconciler/src/ReactFiberHooks.js -> dispatchAction

if (is(eagerState, currentState)) { 
     return
}
scheduleUpdateOnFiber(fiber, expirationTime); //  Schedule updates

If we judge the last time state -> currentState , And this time state -> eagerState equal , Then it will be directly return Prevent components from scheduleUpdate Schedule updates . So we thought if we did it twice useState Trigger the same state, Then the component can only be updated once , But is it true ?.

Put on file an investigation

Follow the clues provided by this digger , So let's start writing demo To verify .

const Index = () => {
  const [ number , setNumber  ] = useState(0)
  console.log(' Component rendering ',number)
  return <div className="page" >
    <div className="content" >
       <span>{ number }</span><br/>
       <button onClick={ () => setNumber(1) } > take number Set to 1</button><br/>
       <button onClick={ () => setNumber(2) } > take number Set to 2</button><br/>
       <button onClick={ () => setNumber(3) } > take number Set to 3</button>
    </div>
  </div>

}
export default class Home extends React.Component{
  render(){
    return <Index />
  }
}

Above demo, Three buttons , We expect to click every button in a row , Components are rendered only once , So we started experimenting :

effect :

 picture
demo1.gif

Sure enough , We go through setNumber change number, Each time you click the button in succession , Components are updated 2 Time , According to our normal understanding , Every time you give number The same value , It'll only be rendered once , But why was it carried out 2 And then ?

It may be in trouble at first , I don't know how to solve the case , But we're thinking hooks In principle , Each function component uses the... Of the corresponding function component fiber Object to save hooks Information . So we can only from fiber Find a clue .

ultimately

So how to find the corresponding function component fiber What about objects? , This follows the parent of the function component Home Start with , Because we can start with class components Home Find the corresponding fiber object , And then according to child Pointer found function component Index Corresponding fiber. Do as you say , Let's transform the above code into something like this :

const Index = ({ consoleFiber }) => {
  const [ number , setNumber  ] = useState(0)
  useEffect(()=>{  
      console.log(number)
      consoleFiber() //  Every time fiber After the update , Print  fiber  testing  fiber change
  })
  return <div className="page" >
    <div className="content" >
       <span>{ number }</span><br/>
       <button onClick={ () => setNumber(1) } > take number Set to 1</button><br/>
    </div>
  </div>

}
export default class Home extends React.Component{
  consoleChildrenFiber(){
     console.log(this._reactInternalFiber.child) /*  Used to print function components  Index  Corresponding fiber */
  }
  render(){
    return <Index consoleFiber={ this.consoleChildrenFiber.bind(this) }  />
  }
}

We focus on fiber On these attributes , It's very helpful to solve the case

  • Index fiber Upper memoizedState attribute , react hooks In principle, the article talked about , Function components use memoizedState Save all hooks Information .
  • Index fiber Upper alternate attribute
  • Index fiber Upper alternate On the properties memoizedState attribute . Is it very winding , We'll find out what it is soon .
  • Index On the component useState Medium number.

First of all, let's talk about alternate What does the pointer refer to ?

Speaking of alternate It's from fiber Architecture design , Every React Element nodes , Use two fiber Tree save state , A tree holds the current state , A tree keeps the last state , Two fiber For trees alternate Point to each other . That's what we're familiar with Double buffering .

Initialize printing

design sketch :

 picture
fiber1.jpg

Initialization complete the first time render after , Let's see fiber These states on the tree

The first print result is as follows ,

  • fiber Upper memoizedState in baseState = 0 It's initialization useState Value .
  • fiber Upper alternate by null.
  • Index On the component number by 0.

Initialization flow : First, for the first initialization of a component , Will reconcile rendering to form a fiber Trees ( We It's called the tree for short A). Trees A Of alternate The attribute is null.

First click setNumber(1)

For the first time, we found component rendering , And then we print it out as follows :

 picture
fiber2.jpg
  • Trees A Upper memoizedState in ** baseState = 0.
  • Trees A Upper alternate Point to Another one fiber( We call it a tree here B).
  • Index On the component number by 1.

Next we print the tree B Upper memoizedState

 picture
fiber3.jpg

And we found that trees B On memoizedState Upper baseState = 1.

Come to the conclusion : The update status is all in the tree B On , And trees A Upper baseState Or before 0.

Let's make a bold guess at the update process : The first time you update the rendering , Because of the trees A in , non-existent alternate, So just copy a tree A As workInProgress( We call it here Trees B) All updates are in the current tree B In the middle of , therefore baseState It will be updated to 1, Then use the current Trees B Rendering . When it's over, the tree A And trees B adopt alternate Point to each other . Trees B As the next operation current Trees .

The second click setNumber(1)

Second printing , Components also render , Then we print fiber object , The effect is as follows :

 picture
fiber4.jpg
  • fiber On the object memoizedState in baseState Updated to 1.

And then we print it out alternate in baseState It's also updated to 1.

 picture
fiber5.jpg

After the second click , Trees A And trees B All updated to the latest baseState = 1

First, let's analyze the process : When we click the second time , It's through the last tree A Medium baseState = 0 and setNumber(1) Incoming 1 Comparison . So I found eagerState !== currentState , The component has been updated again . Next, I will use current Trees ( Trees B) Of alternate Pointing to the tree A As new workInProgress updated , Trees at this time A Upper baseState It's finally updated to 1 , This explains why the above two baseState All equal to 1. Next, the component rendering is complete . Trees A As a new current Trees .

In our second print , What's printed out is actually a post alternate tree B, Trees A And trees B So alternate as the latest state for rendering workInProgress Tree and cache last state for next rendering current Trees .

Click... For the third time ( There are many of them )

So the third click component doesn't render , That's a good explanation , Third click on the last tree B Medium baseState = 1 and setNumber(1) equal , So I went straight away return Logic .

Uncover the mystery ( What we learned )

  • Double buffered trees :React use workInProgress Trees ( Trees built in memory ) and current( Render tree ) To implement the update logic . We console.log Printed fiber It's all in memory workInProgress Of fiber Trees . A double cache is built in memory , In the next rendering , Directly use the cache tree as the next rendering tree , The last rendering tree is used as the cache tree , This can prevent the loss of updating state with only one tree , Speed up again dom Replacement and update of nodes .

  • Renewal mechanism : In an update , First of all, I will get current Treelike alternate As the present workInProgress, After rendering ,workInProgress The tree becomes current Trees . We use the tree above A And trees B And what has been saved baseState Model , To explain the renewal mechanism more vividly .hooks Medium useState Conduct state contrast , It's on the cache tree state And the latest state. All that explains why the same state, Function components execute 2 Time .

We use a flow chart to describe the whole process .

 picture
FFB125E7-6A34-4F44-BB6E-A11D598D0A01.jpg

This case has been solved , Through this easily overlooked case , We learned about double buffering and update mechanisms .

Case six :useEffect modify DOM Elements cause weird flashes

doings of ghosts and gods

Xiao Ming ( alias ) When dynamically mounting components , I met the supernatural Dom Flash phenomenon , Let's look at the phenomenon first .

Flash phenomenon :

 picture
effect.gif

Code :

function Index({ offset }){
    const card  = React.useRef(null)
    React.useEffect(()=>{
       card.current.style.left = offset
    },[])
    return <div className='box' >
        <div className='card custom' ref={card}   >《 React Advanced practice guide  》</div>
    </div>

}

export default function Home({ offset = '300px' }){
   const [ isRender , setRender ] = React.useState(false)
   return <div>
       { isRender && <Index offset={offset}  /> }
       <button onClick={ ()=>setRender(true) } >  mount </button>
   </div>

}
  • In the parent component, use isRender Dynamic loading Index, Click the button to control Index Rendering .
  • stay Index Accept the dynamic offset of offset. And by manipulating useRef Get the original dom Change the offset directly , Make the paddle slide . But there's a flash like the one above , Very unfriendly , So why is this a problem ?

Deepen understanding

It is preliminarily judged that the problem of this flash should be useEffect Caused by the , Why do you say that , Because the class component life cycle componentDidMount Write the same logic , But it's not going to happen . So why useEffect It's going to cause this , We can only find useEffect Of callback The timing of execution .

useEffect ,useLayoutEffect , componentDidMount The timing of execution is commit Stage execution . We know React There is one effectList Different storage effect. because React For different effect The logic and timing of execution are different . Let's see useEffect When defined , What kind of effect.

react-reconciler/src/ReactFiberHooks.js

function mountEffect(create, deps){
  return mountEffectImpl(
    UpdateEffect | PassiveEffect, // PassiveEffect 
    HookPassive,
    create,
    deps,
  );
}

The information for this function is as follows :

  • useEffect To be endowed with PassiveEffect Type of effect .
  • Xiao Ming changed the original dom Function of position , Namely create.

that create When did the function execute ,React How to deal with PassiveEffect What about , This is the key to solving the case . Let's take a look at it Next React How to deal with PassiveEffect.

react-reconciler/src/ReactFiberCommitWork.js

function commitBeforeMutationEffects({
  while (nextEffect !== null) {
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        /*   Asynchronous scheduling  - PassiveEffect */
        scheduleCallback(NormalPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

stay commitBeforeMutationEffects Function , It will be scheduled asynchronously flushPassiveEffects Method ,flushPassiveEffects In the method , about React hooks Will execute commitPassiveHookEffects, And then it will execute commitHookEffectListMount .

function commitHookEffectListMount(){
     if (lastEffect !== null) {
          effect.destroy = create(); /*  perform useEffect I'm hungry  */
     }
}

stay commitHookEffectListMount in ,create Function will be called . We give dom The position of the element will take effect .

So here comes the question , What does asynchronous scheduling do ?React Asynchronous scheduling of , In order to prevent some tasks from delaying the browser drawing , And causes the card frame phenomenon ,react For some low priority tasks , Use asynchronous scheduling to handle , That is to say, let the browser have free time to perform these asynchronous tasks , Asynchronous task execution on different platforms , Different browsers have different ways of implementation , Let's consider the effect and setTimeout equally .

after rain the sky looks blue

From the above we find that useEffect The first parameter of create, Asynchronous call mode adopted , So flash is easy to understand , During the first rendering of the button component , First, execute the function component render, then commit Replace the real dom node , And then the browser draws . At this point, the browser has drawn once , Then the browser has free time to perform asynchronous tasks , So it's implemented create, Changed the location information of the element , Because the last time the element was drawn , At this time, another position has been changed , So I feel the flash effect , This case has been solved .,

So how do we solve the flash phenomenon , That's it React.useLayoutEffect ,useLayoutEffect Of create It's synchronous , So the browser draws once , Directly updated the latest location .

  React.useLayoutEffect(()=>{
      card.current.style.left = offset
  },[])

summary   

What we learned in this section ?

From the angle of solving a case , From the point of view of principle React Some unexpected phenomena , Through these phenomena , We learned some React Something inside , I summarize the above cases ,

  • Case 1 - Understanding of some component rendering and component error timing statements
  • Case two - Supplement to the concept of actual event pool .
  • Case three - Is to introduce multiple versions of some component libraries React Thinking and solutions .
  • Case four - Pay attention to memo / PureComponent The binding event , And how to deal with it PureComponent Logic , shallowEqual Principle .
  • Case five - The actual is fiber Explanation of double cache tree .
  • Case six - It's right useEffect create The timing of execution .
 front end GoGoGo
front end GoGoGo
Committed to increasing front-end revenue . Provide in-depth interviews , The front end of promotion .
8 Original content
official account
Please bring the original link to reprint ,thank
Similar articles

2021-08-09

2021-08-09