1
头图

Preface

Today we come to an unusual advanced React article. In this article, we use some unusual phenomena to analyze the causes and find the results through the process of investigating, so as to understand React, walk into the world of React, and unveil the veil of React. , I am convinced that a can be used better.

I admit that this name may be a bit of a headline. The inspiration comes from a column called "Into Science" on CCTV when I was a child. It introduced various supernatural and supernatural phenomena every day, and it was very magical. When the secret was finally revealed, it turned out to be a variety of pediatrics. Question, I think it’s funny now 😂😂. But the nature of these React'supernatural' phenomena I introduced today is not pediatric. After each phenomenon, the React operating mechanism and design principles are revealed. (The react version we are talking about is 16.13.1 )

src=http___n.sinaimg.cn_sinacn_w640h360_20180113_9984-fyqrewh6822097.jpg&refer=http___n.sinaimg.jpg

Okay, let’s not talk too much nonsense, my big detectives, are you ready? Let’s start today's journey of revealing the secrets.

Case 1: The component is repeatedly mounted inexplicably

Received a report

A previous classmate encountered a weird situation. He hoped componentDidUpdate executed. The source of the component update comes from the change props But the parent component changed to props found that the view was rendered, but componentDidUpdate not executed. What is even more weird is that componentDidMount executed. code show as below:

// TODO: 重复挂载
class Index extends React.Component{
   componentDidMount(){
     console.log('组件初始化挂载')
   }
   componentDidUpdate(){
     console.log('组件更新')
     /* 想要做一些事情 */
   }
   render(){
      return <div>《React进阶实践指南》  👍 { this.props.number } +   </div>
   }
}

effect of

didupdate.gif

componentDidUpdate not executed and componentDidMount executed, indicating that the component did not update the logic , but went and repeatedly mounted .

Check one by one

The child component is at a loss, and we don't find the reason at all, we have to start with the parent component. Let's take a look at 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) } >点赞</button>
   </div>
}

Found some clues from the parent component. In the parent component, first use BoxStyle as a container component, add styles, and render our child component Index , but each time we combine the container components to form a new component NewIndex , the real mount is NewIndex , the truth is out of the question.

Precautions

The essence of this situation is that every render process, a new component is formed. For new components, React's processing logic is to directly uninstall the old components and remount the new components. Therefore, we should pay attention to a problem in the development process. Is:

  • For functional components, do not declare and render new components in their function execution context, so that each function update will cause the component to be repeatedly mounted.
  • For class components, do not render function, otherwise the subcomponents will be repeatedly mounted.

Case 2: The source of the incident e.target is strangely missing

Emergencies

The pseudonym (Xiao Ming) had a whim to write a controlled component on a black and windy night. What is 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="请输入用户名?" onChange={ this.handerChange.bind(this) }  />
    </div>
  }
}

input value by state in value property control, Xiao Ming wanted by handerChange change value value, but he expects setTimeout complete update. But when he wants to change the input value, unexpected things happen.

event.jpg

The console error is shown above. Cannot read property 'value' of null means that e.target is null . Event source target how can I say nothing?

Clue tracking

Upon receipt of this case, we first troubleshoot the problem, then we first handerChange direct printing e.target , as follows:

event1.jpg

It seems that the first investigation is not handerChange , and then we setTimeout and find:

event2.jpg

Sure enough, it was setTimeout . Why did the event source e.target in setTimeout First of all, the event source is definitely not missing inexplicably. It is certain that the bottom layer of React has done some additional processing on the event source. First of all, we know that React uses the event synthesis mechanism, that is, the bound onChange is not the real bound change event handerChange bound by Xiao Ming is not a real event handling function either. So in other words, the bottom layer of React helps us deal with the event source. All this may be the only way for us to find clues from the React source code. After investigating 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 is the main function that all events must pass through in the legacy batchedEventUpdates is the logic for processing batch updates, which will execute our real event processing functions. We have nativeEvent in the event principle chapter that true native event object of event . targetInst is e.target corresponding fiber object. The event source we obtained in handerChange is the event source synthesized by React. Then know when and how the event source is synthesized? This may be helpful for solving the case.

In the event principle chapter, we will introduce the event plug-in mechanism used by React. For example, our onClick event corresponds to SimpleEventPlugin . Then Xiaoming writes onChange also has a special ChangeEventPlugin event plug-in. These plug-ins have a vital role to synthesize our event source object 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, // 组件实例
            nativeEvent, // 原生的事件源 e
            target,      // 原生的e.target
     );
     accumulateTwoPhaseListeners(event); // 这个函数按照冒泡捕获逻辑处理真正的事件函数,也就是  handerChange 事件
     return event; // 
   }   
}

We see that the e in the event source handerChange of the synthetic event is created by SyntheticEvent.getPooled 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,);
}

Fanwai: In the chapter on the event system, the article's thoughts on the event pool are hurried and general. This section will supplement the thoughts on the event pool in detail. <br/>

getPooled introduces the real concept of event pool, it mainly does two things:

  • Determine whether there are free event sources in the event pool, and if there are any event sources to be reused.
  • If not, create a new event source object by way of new SyntheticEvent Then SyntheticEvent is the constructor to create the event source object, let's study it together.
const EventInterface = {
  type: null,
  target: null,
  currentTarget: function() {
    return null;
  },
  eventPhase: null,
  ...
};
function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
  this.dispatchConfig = dispatchConfig; 
  this._targetInst = targetInst;    // 组件对应fiber。
  this.nativeEvent = nativeEvent;   // 原生事件源。
  this._dispatchListeners = null;   // 存放所有的事件监听器函数。
  for (const propName in Interface) {
      if (propName === 'target') {
        this.target = nativeEventTarget; // 我们真正打印的 target 是在这里
      } else {
        this[propName] = nativeEvent[propName];
      }
  }
}
SyntheticEvent.prototype.preventDefault = function (){ /* .... */ }     /* 组件浏览器默认行为 */
SyntheticEvent.prototype.stopPropagation = function () { /* .... */  }  /* 阻止事件冒泡 */

SyntheticEvent.prototype.destructor = function (){ /* 情况事件源对象*/
      for (const propName in Interface) {
           this[propName] = null
      }
    this.dispatchConfig = null;
    this._targetInst = null;
    this.nativeEvent = null;
}
const EVENT_POOL_SIZE = 10; /* 最大事件池数量 */
SyntheticEvent.eventPool = [] /* 绑定事件池 */
SyntheticEvent.release=function (){ /* 清空事件源对象,如果没有超过事件池上限,那么放回事件池 */
    const EventConstructor = this; 
    event.destructor();
    if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
       EventConstructor.eventPool.push(event);
    }
}

After I refined this piece of code, the truth gradually emerged. Let’s take a look at what SyntheticEvent did:

  • First give some initialized variables nativeEvent and so on. Then follow the EventInterface rule to copy the attributes on the native event source React event source. Then an important thing is that the e.target we printed is this.target, which is bound to the real e.target->nativeEventTarget
  • Then the React event source binds its own blocking default behavior preventDefault , blocking bubbling stopPropagation and other methods. But here is a key method, destructor , This function nulls React's own event source object. Then we finally found the answer. The high probability of our event source e.target disappearing is because destructor and destructor were triggered in release , and then the event source was put into the event pool, waiting for the next reuse.

Now all the spearheads are directed at release , release be triggered?

legacy-events/SyntheticEvent.js
function executeDispatchesAndRelease(){
    event.constructor.release(event);
}

When the React event system finishes executing all _dispatchListeners , it will trigger this method executeDispatchesAndRelease release the current event source.

The truth is revealed

Back to the problem encountered by Xiaoming, we mentioned above that React will finally empty the event source synchronously and put it into the event pool, because setTimeout is executed asynchronously, and the event source object has been reset and released during execution. , So we print e.target = null , so far, the truth of the case is revealed.

Through this case, we understand some concepts of React event pool:

  • The React event system has unique synthetic events, its own event source, and processing logic for some special situations, such as bubbling logic.
  • In order to prevent each event from creating an event source object and wasting performance, React introduces the event pool concept . Every user event will take an e from the event pool. If not, create one, then assign the event source, and wait until After the event is executed, reset the event source and put it back into the event pool to achieve reuse.

represented by a flow chart:

eventloop.jpg

Case 3: True and False React

Crime scene

This is what happened to the author. When I was developing a React project, I uploaded some encapsulated custom Hooks to the company’s private package management platform for logic reuse. When developing another React project, Download the company's package and use it inside the component. code show as below:

function Index({classes, onSubmit, isUpgrade}) {
   /* useFormQueryChange 是笔者写好的自定义hooks,并上传到私有库,主要是用于对表单控件的统一管理  */
  const {setFormItem, reset, formData} = useFormQueryChange()
  React.useEffect(() => {
    if (isUpgrade)  reset()
  }, [ isUpgrade ])
  return <form
    className={classes.bootstrapRoot}
    autoComplete='off'
  >
    <div className='btnbox' >
       { /* 这里是业务逻辑,已经省略 */ }
    </div>
  </form>
}

useFormQueryChange is a custom 060bc414f20221 written by the author and uploaded to the private library. It is mainly used for the unified management of form controls. hooks The error content is as follows:

hooks.jpg

Check one by one

We follow the content of React's error report to troubleshoot the problems one by one:

  • The first possible error reason is You might have mismatching versions of React and the renderer (such as React DOM) , which means that the React and React Dom inconsistent, causing this situation, but the React and React Dom in our project are both v16.13.1 , so we rule out this suspicion.
  • The second possible error reason You might be breaking the Rules of Hooks means that you broke the Hooks rule. This situation is also impossible, because the author's code does not break the hoos rule. So also rule out suspicion.
  • The third possible error reason You might have more than one copy of React in the same app means that there may be multiple Reacts in the same application. So far, all the suspects point to the third one. First of all, the custom hooks we referenced, will there be a React inside?

I follow the above tips to troubleshoot custom hooks corresponding node_modules Sure enough there is another React, is this false React (let's call them false React) out of the ghost. As we in the 160bc414f2033d Hooks Principle article, React Hooks initialized with ReactCurrentDispatcher.current in the component, and different hooks objects are assigned in the component update stage. After the update is completed, ContextOnlyDispatcher assigned. If the hooks under this object are called, the above error will be reported, so it is explained. this error is because of our project, the React introduced in the execution context is the React of the project itself, but the custom Hooks refer to ContextOnlyDispatcher

Next I saw package.json in the component library,

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

It turns out that React is used as dependencies so when downloading custom Hooks , React downloaded 060bc414f2039e again. So how to solve this problem. React for packaging component library, library hooks, can not be used dependencies , because it is the current dependencies is downloaded to the custom hooks dependent library following node_modules in. Instead, you should use peerDependencies , use peerDependencies , customize hooks then find related dependencies, you will go to node_modules of our project to find it, which can fundamentally solve this problem.
So we changed

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

It solves this problem perfectly.

Clear the fog

This question makes us understand the following:

  • For some hooks libraries, component libraries, and their own dependencies, they already exist in the project, so declare them peerDependencies
  • In the development process, different versions of the same dependency are likely to be used. For example, the project introduces a dependency of version A, and the component library introduces a dependency of version B. So how to deal with this situation. A resolutions configuration item is provided in the package.json resolutions , so that it will not cause the problem caused by the dependency of multiple versions of the project.

Project package.json writes like this

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

In this way, regardless of the dependencies in the project or the dependencies in other libraries, a unified version will be used, which fundamentally solves the problem of multiple versions.

Case 4: PureComponet/memo function failure problem

Case description

When developing React, we wanted to use PureComponent for performance optimization and adjust component rendering, but after writing a piece of code, we found that the PureComponent actually failed. The specific code is as follows:


class Index extends React.PureComponent{
   render(){
     console.log('组件渲染')
     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:

  • For the Index component, only the props and type changes in name cause the component to render. But the actual situation is like this:

Click the button effect:

purecomponent.gif

Come to the bottom

Why does this happen? Let's check the Index component again and find that there is a changeType Index component. Is this the reason? Let's analyze, first of all status updates in the parent component Home on, Home component updates occur every time a new changeName , so Index of PureComponent each meeting shallow comparison , found props in changeName not equal each time, So it was updated, giving us the intuitive feeling that it is invalid.

So how to solve this problem, React hooks provided useCallback , can props cache incoming callback function, we have to change it Home code.

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

effect:

pureComponent1.gif

This solves the problem fundamentally. Use useCallback to changeName function. Every time the Home component is executed, as long as the useCallback in deps has not changed, the changeName still points to the original function, so the PureComponent will find the same changeName , 414f205f7 and 060bc414f205f7. The component is not rendered, so far the case has been solved.

Keep going

When developing functional components + class components, if you use React.memo React.PureComponent such as 060bc414f20627, pay attention to the way to bind events to these components. If it is a functional component, you want to continue to maintain the pure component rendering control feature , Then please use useCallback , useMemo and other apis. If it is a class component, please do not use arrow functions to bind events. Arrow functions will also cause failure.

The above mentioned a shallow comparison shallowEqual , then we focus on analyzing how PureComponent is shallowEqual , and then we are going to delve into the mystery of shallowEqual Then it starts with the update of the class rental price.

react-reconciler/src/ReactFiberClassComponent.js
function updateClassInstance(){
    const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );
    return shouldUpdate
}

I simplified updateClassInstance here, and only kept the part related to PureComponent . updateClassInstance is mainly used to execute the life cycle, update the state, and determine whether the component is re-rendered. The returned shouldUpdate used to determine whether the current class component is rendered. checkHasForceUpdateAfterProcessing checks whether the source of the update is the same as forceUpdate. If it is the forceUpdate component, it will definitely be updated. checkShouldComponentUpdate checks whether the component is rendered. Let's look at the logic of this function next.

function checkShouldComponentUpdate(){
    /* 这里会执行类组件的生命周期 shouldComponentUpdate */
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    /* 这里判断组件是否是 PureComponent 纯组件,如果是纯组件那么会调用 shallowEqual 浅比较  */
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return (
        !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
        );
    }
}

checkShouldComponentUpdate has two vital functions:

  • The first is if the class component life cycle shouldComponentUpdate , will perform lifecycle shouldComponentUpdate , to determine whether the component is rendered.
  • If it is found to be a pure component PureComponent , it will compare the new and old props and state see if they are equal. If they are equal, the component will not be updated. isPureReactComponent is our PureComponent , which proves to be a pure component.

Next is the key point shallowEqual , take props as an example, let's take a look.

shared/shallowEqual
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) { // is可以 理解成  objA === objB 那么返回相等
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;  
  } // 如果新老props有一个不为对象,或者不存在,那么直接返回false

  const keysA = Object.keys(objA); // 老props / 老state key组成的数组
  const keysB = Object.keys(objB); // 新props / 新state key组成的数组

  if (keysA.length !== keysB.length) { // 说明props增加或者减少,那么直接返回不想等
    return false;
  }

  for (let i = 0; i < keysA.length; i++) { // 遍历老的props ,发现新的props没有,或者新老props不同等,那么返回不更新组件。
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true; //默认返回相等
}

shallowEqual process of 060bc414f2077a is like this. shallowEqual returns true it proves that they are equal, and then the component is not updated; if it returns false prove that you do not want to wait, then the component is updated. is can be understood as ===

  • In the first step, directly pass === to judge whether they are equal, if they are equal, then return true . Normally, as long as calling React.createElement will recreate props , props are not equal.
  • In the second step, if one of the new and old props is not an object, or does not exist, then directly return to false .
  • The third step is to determine whether the props , key , etc. do not want to wait, indicating that props has increased or decreased, then directly return to false .
  • The fourth step is to traverse the old props and find that the new props does not correspond to it, or the new and old props different, then return to false .
  • By default, true returned.

This is the shallowEqual , and the code is very simple. Interested students can take a look.

Case 5: useState updates the same State, and the function component is executed twice

Received a report

This question is actually very hanging. You may not have noticed it. What caught my attention was a question asked by a friend of the Nuggets. The question is as follows:

EBF05CF6-E088-4DE0-A0CD-57E04AB29BBF.jpg

First of all thank the report carefully dig Friends, I React-Hooks principle mentioned too, for updated components methods in the component useState and class components setState have some differences, useState the source code if you encounter twice The same state will prevent the component from updating by default, but setState is not set in PureComponent in the class component, the same state will be updated twice.

Let’s review how hooks prevents component updates.

react-reconciler/src/ReactFiberHooks.js -> dispatchAction
if (is(eagerState, currentState)) { 
     return
}
scheduleUpdateOnFiber(fiber, expirationTime); // 调度更新

If a determination on state -> currentState , and this time state -> eagerState equal, direct return prevent components scheduleUpdate schedule update. So we think that if useState triggers the same state twice, then the component can only be updated once, but is this really the case? .

Investigation

demo the clues provided by this digger, we began to write 060bc414f20971 for verification.

const Index = () => {
  const [ number , setNumber  ] = useState(0)
  console.log('组件渲染',number)
  return <div className="page" >
    <div className="content" >
       <span>{ number }</span><br/>
       <button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
       <button onClick={ () => setNumber(2) } >将number设置成2</button><br/>
       <button onClick={ () => setNumber(3) } >将number设置成3</button>
    </div>
  </div>
}
export default class Home extends React.Component{
  render(){
    return <Index />
  }
}

As in the demo above, there are three buttons. We expect to click on each button continuously, and the component will be rendered only once, so we started the experiment:

effect:

demo1.gif

Sure enough, we changed number setNumber . Each time the button is clicked continuously, the component will be updated twice. According to our normal understanding, each time the number , it will only be rendered once, but why is it executed twice?

You may get into trouble at the beginning and don't know how to solve the case, but we hooks in the 060bc414f209ee principle that each function component uses the fiber object of the hooks information. So we can only find clues fiber

Follow the vine

So how do you find fiber object functions corresponding components of it, along which the parent function components Home start, because we can from class components Home find the corresponding fiber object, and then according to child find components function pointer Index corresponding fiber . Just do it, we will transform the above code into the following:

const Index = ({ consoleFiber }) => {
  const [ number , setNumber  ] = useState(0)
  useEffect(()=>{  
      console.log(number)
      consoleFiber() // 每次fiber更新后,打印 fiber 检测 fiber变化
  })
  return <div className="page" >
    <div className="content" >
       <span>{ number }</span><br/>
       <button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
    </div>
  </div>
}
export default class Home extends React.Component{
  consoleChildrenFiber(){
     console.log(this._reactInternalFiber.child) /* 用来打印函数组件 Index 对应的fiber */
  }
  render(){
    return <Index consoleFiber={ this.consoleChildrenFiber.bind(this) }  />
  }
}

We focus on these attributes on the fiber, which is very helpful to solve the case

  • Index fiber on memoizedState property, react hooks principle of the article mentioned, the function components with memoizedState save all hooks information.
  • Index fiber on alternate property
  • Index fiber attribute on alternate memoizedState . Isn't it very confusing? I will reveal what it is soon.
  • Index on assembly useState in number .

First, let's talk about what the alternate pointer refers to?

Speaking of alternate we should start with the fiber architecture design. Each React element node uses two fiber trees to save the state, one tree to save the current state, and one tree to save the last state. Two fiber trees point to each other alternate It is the well-known double-buffered .

Initial printing

renderings:

fiber1.jpg

initialization of 160bc414f20b7a is completed for the first render, let’s take a look at these states on the fiber tree

The first print result is as follows,

  • fiber on memoizedState in baseState = 0 i.e. initialization useState value.
  • fiber on alternate is null .
  • Index on assembly number is 0.

Initialization process: First, for the first initialization of the component, it will be reconciled and rendered to form a fiber tree (we referred to as tree A ). A tree of alternate property null .

Click setNumber(1) for the first time

We clicked for the first time and found that the component was rendered, and then we printed the result as follows:

fiber2.jpg

  • On the tree A memoizedState in ** baseState = 0 .
  • alternate on tree A points to another fiber (we call it tree B here).
  • Index on assembly number 1.

Next we print memoizedState

fiber3.jpg

It turns out that the tree B memoizedState on baseState = 1 .

It is concluded that the updated state is on tree B, and the baseState on tree A is still the previous 0.

Let's make a bold guess about the update process: when the rendering is updated for the first time, since alternate does not exist in tree A, we directly copy a copy of tree A as workInProgress (we call it tree B here) and all the updates are in In the current tree B, so the baseState will be updated to 1, and then use the current tree B for rendering. After the end, tree A and tree B point to each other alternate Tree B is used as the current tree for the next operation.

Click setNumber(1) for the second time

second time, the components are also rendered, and then we print the fiber object, the effect is as follows:

fiber4.jpg

  • memoizedState in baseState on the fiber object is updated to 1.

Then we print alternate in baseState is also updated to 1.

fiber5.jpg

After the second click, both tree A and tree B are updated to the latest baseState = 1

First, we analyze the process: when we clicked for the second time, the state in tree A was not updated to the latest state, and the component was updated again. Next, the tree A pointed to alternate current tree (tree B) will workInProgress . At this time, the baseState on tree A is finally updated to 1, which explains why the above two baseStates are both equal to 1. Next, the component rendering is complete. Tree A serves as the new current tree.

In our second printing, print out the actual tree after alternating B, A and tree B tree so alternately as the date for rendering workInProgress on trees and a buffer state for the next rendering current tree.

The third click (more than three)

Then the third click component is not rendered, it is easy to explain, the third click the last time baseState = 1 and setNumber(1) in tree B are equal, and the return logic is directly gone.

Unravel the mystery (what have we learned)

  • Double buffering tree: React uses workInProgress tree (tree built in memory) and current (rendering tree) to implement the update logic. workInProgress printed by our console.log are all fiber trees with 060bc414f20e95 in memory. The double cache is built in memory. In the next rendering, the cache tree is directly used as the next rendering tree, and the previous rendering tree is used as the cache tree. This can prevent the loss of update status with only one tree. Speed up the replacement and update dom
  • Update mechanism: In an update, the alternate current tree is first obtained as the current workInProgress . After rendering, the workInProgress tree becomes a current tree. We use the above tree A and tree B and the saved baseState model to explain the update mechanism more vividly.

We use a flowchart to describe the entire process.

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

This case has been solved. Through this easily overlooked case, we have learned the double buffering and update mechanism.

Case six: useEffect to modify the DOM element causes a weird flash

curious coincidence

Xiao Ming (pseudonym) encountered a strange Dom flash phenomenon when dynamically mounting components. Let's take a look at the phenomenon first.

flash phenomenon:

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进阶实践指南 》</div>
    </div>
}

export default function Home({ offset = '300px' }){
   const [ isRender , setRender ] = React.useState(false)
   return <div>
       { isRender && <Index offset={offset}  /> }
       <button onClick={ ()=>setRender(true) } > 挂载</button>
   </div>
}
  • isRender dynamically load Index in the parent component, and click the button to control Index rendering.
  • The dynamic offset Index offset . And by the controlling useRef acquired native dom changing the offset directly, so that the draw block slide. However, the flash phenomenon shown in the above picture has appeared, which is very unfriendly, so why does it cause this problem?

understand deeper

It is preliminary judged that the problem of this flash should be useEffect . Why do you say that, because the componentDidMount , but this phenomenon does not occur. So why useEffect will cause this situation, we can only follow the vine to find the execution timing of useEffect callback

useEffect ,useLayoutEffect , componentDidMount executed at the commit stage. We know that React has a effectList store different effect . Because React has different execution logic and timing effect Let's take a look at what type of effect useEffect is defined.

react-reconciler/src/ReactFiberHooks.js
function mountEffect(create, deps){
  return mountEffectImpl(
    UpdateEffect | PassiveEffect, // PassiveEffect 
    HookPassive,
    create,
    deps,
  );
}

The information of this function is as follows:

  • useEffect is assigned PassiveEffect type effect .
  • The function for Xiaoming to change the location of the native dom is create .

So when is the create function executed, and how does React handle PassiveEffect ? This is the key to solving the case. Write it down and let's take a look at how React handles PassiveEffect .

react-reconciler/src/ReactFiberCommitWork.js
function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        /*  异步调度 - PassiveEffect */
        scheduleCallback(NormalPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

In commitBeforeMutationEffects function, asynchronously scheduling flushPassiveEffects method, flushPassiveEffects method, for React hooks performs commitPassiveHookEffects , then executes commitHookEffectListMount .

function commitHookEffectListMount(){
     if (lastEffect !== null) {
          effect.destroy = create(); /* 执行useEffect中饿 */
     }
}

In commitHookEffectListMount , the create function will be called. The position we add to the dom element will take effect.

So the question is, what does asynchronous scheduling do? React's asynchronous scheduling, in order to prevent the execution of some tasks from delaying the browser's drawing, and causing the phenomenon of jams, React uses asynchronous scheduling to process some tasks with low priority, which means that the browser can only perform these tasks in idle time. Asynchronous tasks. Asynchronous tasks are executed on different platforms and different browsers in different ways. Let setTimeout consider the effect as 060bc414f21190.

Sunny after rain

Through the above we find useEffect first argument create , the way the use of asynchronous call, then flashed on a good understanding, at the click of a button assembly for the first time the rendering process, the first execution of the function components render , then commit replace real dom Node, and then the browser is drawn. At this point, the browser has drawn once, and then the browser has free time to perform asynchronous tasks, so it executes create , and modifies the position information of the element. Because the element has been drawn last time, a position is modified at this time, so I feel flashed As a result, the case has been solved. ,

So how do we solve the phenomenon of flashing, that is, React.useLayoutEffect , useLayoutEffect of create are executed synchronously, so the browser draws once and directly updates the latest position.

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

to sum up

What have we learned in this section?

React from the perspective of solving the case from the perspective of principle. Through these phenomena, we have learned some internal things of React. I summarized the above case.

  • Case one-understanding of some component rendering and component error timing statements
  • Case Two-Supplement to the actual event pool concept.
  • Case three-is the thinking and solution of React
  • Case four - to pay attention to memo / PureComponent binding events, and how to handle PureComponent logic, shallowEqual principles.
  • Case 5-Actually, it is an fiber double cache tree of 060bc414f212bd.
  • Case 6-is an explanation of the timing of execution useEffect create

Finally, give someone a rose and leave a lingering fragrance in your hand. Friends who think you have gained can give me likes, follow , and update the front-end super hard core articles one after another. Welcome to follow the author's public front-end Sharing , share hardcore articles for the first time


alien
36 声望3 粉丝