preface

Most language or framework library use, when it comes to the advanced part, absolutely inseparable from the three:

1. Performance optimization 2. Component packaging 3. Language difficulties

This article will explain the advanced knowledge related to React from these three directions.

React Performance Optimization

SCU

React updates the parent component by default, while the child component updates unconditionally. In most cases, the state of the child component does not change and does not need to be updated. Is there any way to handle this? The answer is yes, you can use shouldComponentUpdate (SCU) to compare the state of the component to determine whether the component needs to be updated.

shouldComponentUpdate(nextProps, nextState) {
  if(nextProps.color ! = =this.props.color) {
    return true // Can render
  }
  if(nextState.count ! = =this.state.count) {
    return true // Can render
  }
  return false // Do not repeat rendering
}
Copy the code

By determining components

  • Current state and nextState after change
  • Current props and nextProps after the change

To determine whether the component itself is updated.

Let’s look at a simple example:

import React from "react";

class Scu extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0.content:"I'm ScuChild component"
    }
  }
  
  onIncrease = () = > {
    this.setState({
      count: this.state.count + 1})}render() {
    console.log("Scu, rendered ~");
    return <div>
      <ScuChild content={this.state.content} />
      <span>{this.state.count}</span>
      <button onClick={this.onIncrease}>increase</button>
    </div>}}class ScuChild extends React.Component{
  render() {
    console.log("ScuChild rendered ~")
    return <div>
      {this.props.content}
    </div>}}export default Scu
Copy the code

The parent component increases count by 1 each time it is clicked. In this simple example, the Scu component changes its own state each time, while the props. Content obtained by its child ScuChild never changes, but it is updated every time it is clicked, which is obviously unreasonable. But the React framework is designed that way, so we don’t have to do it ourselves.

Add to ScuChild component:

shouldComponentUpdate(nextProps, nextState) {
  // When the content is different, we update it, otherwise we don't
  returnnextProps.content ! = =this.props.content;
}
Copy the code

When content doesn’t change, it doesn’t update anymore.

PureComponent

ShouldComponentUpdate is too cumbersome to write every time. React provides the PureComponent base class to inherit from. It overrides the implementation of shouldComponentUpdate() with a shallow comparison of the current and previous props and state.

In other words, if the data is of reference type, only the same address will be compared, not whether the data stored at that address is identical.

This also explains why setState must use immutable values, and we need to make a shallow copy of the objects in state each time, rather than using push, POP and other methods to change objects or arrays directly.

setState({
  list:this.state.list.push(1)})Copy the code

If we setState like this, if its children receive the list to render, then the PureComponent will not render without changing the address of the list object, and will actually push a value into it.

class ScuChild extends React.PureComponent{
  
  render() {
    console.log("ScuChild rendered ~")
    return <div>
      {this.props.content}
    </div>}}Copy the code

Is it very convenient to write after modification?

Memo

The function component does not provide a shouldComponentUpdate lifecycle hook, which can be wrapped with the React.Memo function

function ScuChild2(props) {
  console.log("ScuChild2");
  return (
    <div>
      {props.content}
    </div>)}export const MemodScuChild = React.memo(ScuChild2);
Copy the code

The memo can also accept a comparison function:

export const MemodScuChild = React.memo(ScuChild2,(prevProps, nextProps) = > prevProps.content===nextProps.content);
Copy the code

Component lazy loading

Optimizing project performance through lazy loading is a common operation method. TC39 proposal has import() dynamic import method, which makes lazy loading very easy. React V16.6 releases lazy functions to make the React implementation easier to load lazily.

import React from 'react'

const Scu = React.lazy(() = > import('./Scu'));

class Lazy extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return <div>
            <p>Introduce a dynamic component</p>
            <hr />
            <React.Suspense fallback={<div>Loading...</div>} ><Sku />
            </React.Suspense>
        </div>
        // Refresh to see loading}}export default Lazy
Copy the code

The Scu component is the component written above, now introduced through Lazy loading of Lazy components.

There are two things to note:

  1. Components that require lazy loading must passexport defaultExport components;
  2. React.lazySuspenseThe technology does not support server-side rendering.

Analysis of lazy loading principle

Lazy function

export function lazy<T.R> (ctor: () => Thenable<T, R>) :LazyComponent<T> {
  return{?typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    _status: -1.// The status of the resource
    _result: null.// Store the resource to load the file
  };
}
Copy the code
const Scu = React.lazy(() = > import('./Scu')); Is equivalent toconst Scu = {
  ?typeof: REACT_LAZY_TYPE, // Indicates lazy loading type
  _ctor: () = > import('./Scu'),
  _status: -1.// Initialization status -1 == pending
  _result: null,}Copy the code

There’s another key function, readLazyComponentType, which is used to parse lazy loads in React:

export function readLazyComponentType<T> (lazyComponent: LazyComponent<T>) :T {
  const status = lazyComponent._status;
  const result = lazyComponent._result;
  switch (status) {
    case Resolved: {
      const Component: T = result;
      return Component;
    }
    case Rejected: {
      const error: mixed = result;
      throw error;
    }
    case Pending: {
      const thenable: Thenable<T, mixed> = result;
      throw thenable;
    }
    default: {
      lazyComponent._status = Pending;
      const ctor = lazyComponent._ctor;
      const thenable = ctor();
      thenable.then(
        moduleObject= > {
          if (lazyComponent._status === Pending) {
            constdefaultExport = moduleObject.default; lazyComponent._status = Resolved; lazyComponent._result = defaultExport; }},error= > {
          if(lazyComponent._status === Pending) { lazyComponent._status = Rejected; lazyComponent._result = error; }}); lazyComponent._result = thenable;throwthenable; }}}Copy the code

Source code:

First, switch (status) is executed by the component’s state. The default state is -1, so the defalut branch is used. Defalut has a core piece of code:

import('./Scu').then(
  moduleObject= > {
    if (lazyComponent._status === Pending) {
      const defaultExport = moduleObject.default; / / {1}
      lazyComponent._status = Resolved;  / / {2}
      lazyComponent._result = defaultExport; / / {3}}},error= > {
    if(lazyComponent._status === Pending) { lazyComponent._status = Rejected; lazyComponent._result = error; }});Copy the code
  • {1} gets the output object of the asynchronously loaded componentdefaultProperties, remember there’s a caveat up there? “Components that need lazy loading must go throughexport defaultExport component “, is not from sourceconst defaultExport = moduleObject.defaultYou can see why you should do that.
  • {2} changes its state to doneResolved 
  • {3} lazyComponent._resultProperty to the actual content of the component

Therefore, it can be analyzed that the core is still the import(“…”) proposed by ES2020. ) dynamic loading module scheme.

The rationale for webpack packaging import() :

  1. encounterimport()The loaded components are packaged into a separatechunk 
  2. When loading conditions need to be met, one is inserted dynamically<script src="chunk path"></script> 
  3. It fires when the load succeedsimport('./Scu').thenThe first function inside, and the second function fires when the load fails.

Principle of Suspense

When lazy components are used, react. Suspense must be used for wrapping. When lazy components have not finished loading, the custom loading state is displayed. Let’s take a look at Suspense pseudocode to understand how it works

import React from 'react'

class Suspense extends React.Component {
  state = {
    promise: null
  }

  componentDidCatch(e) {
    if (e instanceof Promise) {
      this.setState({
        promise: e
      }, () = > {
        e.then(() = > {
          this.setState({
            promise: null})})})}}render() {
    const { fallback, children } = this.props;
    const { promise } = this.state;
    return <>
      { promise ? fallback : children }
    </>}}Copy the code

Code explanation:

The core code is in the componentDidCatch lifecycle function, which catches any errors that occur in the subcomponent tree. The error type is then used to setState the promise’s value, and the final decision is what to render based on the promise’s value.

It’s perfectly understandable why readLazyComponentType throws Thenable, since Suspense is implemented by catching errors in Suspense. Display lazily loaded components when an error is detected, otherwise display loaded components.

React Component Encapsulation

When it comes to encapsulation, the first thing that comes to mind in JavaScript is the encapsulation of higher-order functions (such as callback methods). In React, we use a similar idea to encapsulate it, which is called higher-order component HOC.

HOC

A higher-order component is a function that takes a component as an argument and returns a new component.

We imagine a car assembly line, an empty shelf car, through one line is assembled on the chassis, and then through another line is assembled on the steering wheel… A pass. Abstract out the capability and add a capability to any component that doesn’t have that capability. This is the embodiment of higher-order components.

As I mentioned in the design patterns article, HOC is really just a decorative mask pattern, and it only takes a change to the configuration file to support the writing of the decorative mask pattern @hoc

In the world of programming we don’t have steering wheels, car audio mounts. But we will have our actual business also need to be assembled.

Implement higher-order components

1. Property broker

The function returns a component that we define ourselves, and then returns the component that we want to wrap in Render, so that we can proxy all the props that are passed in and decide how to render it. In effect, the higher-order component generated this way is the parent of the original component. Okay

function proxy(WrappedComponent) {
  return class extends React.Component {
    render() {
      return <WrappedComponent {. this.props} / >; }}}Copy the code

2. Reverse inheritance

Return a component that inherits the original component and calls render in render. Because it inherits from the original component, it can access the lifecycle, props, state, render, and so on of the original component, and it can manipulate more properties than the property proxy.

function inherit(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return super.render(); }}}Copy the code

What HOC can do

Combination of rendering
function CombProxy(WrappedComponent) {
  return class extends React.Component {
    render() {
      return (
        <>
          <div>{this.props.title}</div>
          <WrappedComponent {. this.props} / >
        </>)}}}class Child extends React.Component{
  render() {
    return (
      <div>The child components</div>)}}export default CombProxy(Child);
Copy the code

In this way, any component wrapped by the CombProxy component can have the ability to render the title, without the need to add this function separately.

Two-way data binding
import React from "react";

function proxyHoc(WrappedComponent){
  return class extends React.Component{
    constructor(props) {
      super(props);
      this.state = {value:""}
    }
    onChange = (event) = >{
      const {onChange} = this.props;

      this.setState({
        value: event.target.value
      },() = >{
        if(typeof onChange === 'function'){
          onChange(this.state.value); }})}render(){
      const newProps = {
        value: this.state.value,
        onChange: this.onChange
      }
      const props = Object.assign({},this.props,newProps);
      return <WrappedComponent {. props} / >}}}class HOC extends React.Component{
  render() {
    return <input {. this.props} / >}}export default proxyHoc(HOC);
Copy the code

Code explanation:

  • theinputForm elements are passed into higher-level components
  • Custom in higher-order componentsvalueState, andonChangeEvent, and passed toinputOn form elements
  • So the form elements are equivalent to<input onChange={... } value="..." >And the bidirectional binding logic is done in higher-order components.
Form validation

We continue the ability to validate the form based on the higher-order function above.

import React from "react";

function proxyHoc(WrappedComponent){
  return class extends React.Component{
    constructor(props) {
      super(props);
      this.state = {
        value:"".error:""
      }
    }
    onChange = (event) = >{
      const {onChange,validator} = this.props;

      if(validator && typeof validator.func === 'function') {if(validator.func(event.target.value)){
          this.setState({
            error : ""})}else{
          this.setState({
            error : validator.msg
          })
        }
      }
      this.setState({
        value: event.target.value
      },() = >{
        if(typeof onChange === 'function'){
          onChange(this.state.value); }})}render(){
      const newProps = {
        value: this.state.value,
        onChange: this.onChange
      }
      const props = Object.assign({},this.props,newProps);
      return (
        <>
          <WrappedComponent {. props} / >
          <div>{this.state.error}</div>
        </>)}}}class HOC extends React.Component{
  render() {
    return <input {. this.props} / >}}export default proxyHoc(HOC);
Copy the code

At the same time, it is also the higher-order component, which not only completes the function of onChange bidirectional binding, but also can be verified according to the verification rules passed in

import Input from "./input";

function Hoc() {
  
  const validatorName = {
    func: (val) = > val && val.length > 2.msg : "Name must be greater than 2 characters."
  }

  return (
    <div className="App">
      <Input validator={validatorName} onChange={(val)= >{console.log(val)}} />
    </div>
  );
}

export default Hoc;
Copy the code

It’s also very simple to call. In this way, the abstract components are hierarchical and the code is easy to maintain.

Defects of higher-order components:

  • HOC needs to be wrapped or nested on top of the original components, and if you use HOC in large amounts, you get a lot of nesting, which makes debugging very difficult.

  • HOC can hijack props and can cause conflict if conventions are not followed.

The function components

Function Component is a more thorough state-driven abstraction. It doesn’t even have the concept of a Class Component lifecycle, just one state, whereas React takes care of synchronizing to the DOM.

function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
}
Copy the code

Functional components are easy to read and test and have no state or life cycle. This allows us to quickly write a UI rendering component, which is in line with React’s philosophy of advocating functional programming.

Before React V16.8, function components could simply render components and had no state of their own. With the advent of Hook, this situation changed and function components were given more capabilities.

UseEffect Hook The useEffect Hook provides a “life cycle” for componentDidMount, componentDidUpdate, and componentWillUnmount. It’s just merged into one API.

With useState, function components have their own state.

Well, at this point, you probably already know something about functional components. So let’s take a closer look at the features of functional components.

Capture Value

Let’s start with an example:

class App extends React.Component{
  state = {
    count: 0
  }
  show = () = >{
    setTimeout(() = >{
      console.log('1 second ago count = 0, now count =The ${this.state.count}`);
    },1000)}render() {
    return (
      <div onClick={()= >{ this.show(); This.setstate ({count: 5})}}> Click the Class component</div>); }}Copy the code

Click on the output: 1 second ago count = 0, now count = 5.

This is easy to understand, the timer after 1 second is obtained after the component instance changed state, in line with our normal thinking.

function App(){
  const [count , setCount] = React.useState(0);
  const show = () = >{
    setTimeout(() = >{
      console.log('1 second ago count = 0, now count =${count}`);
    },1000)}return (
    <div onClick={()= >{ show(); setCount(5); }}> Click on the function component</div>
  );
}
Copy the code

Click on the output: 1 second ago count = 0, now count = 0.

This result is completely different from the result of the Class component, where the state obtained after 1 second remains the same as before. This phenomenon is called the Capture Value feature of a function component. It is like taking a snapshot with each click and saving the data at that time. Let’s analyze the specific principle.

Each Render has its own Props and State

When the first click is made it looks like this:

function App(){
  const count = 0;
  const show = () = >{
    setTimeout(() = >{
      console.log('1 second ago count = 0, now count =${count}`); // 1 second ago count = 0, now count = 0
    },1000)}... }Copy the code

When you click on it the second time, it looks like this:

function App(){
  const count = 5;
  const show = () = >{
    setTimeout(() = >{
      console.log('1 second ago count = 0, now count =${count}`); // 1 second ago count = 0, now count = 5
    },1000)}... }Copy the code

It can be assumed that each Render content will form a snapshot and be retained. Therefore, when Rerender the State changes, N Render states are formed, and each Render State has its own fixed Props and State.

Bypass the Capture Value feature

You can bypass Capture Value features with useRef. Ref can be thought of as a unique reference in all Render processes, so all assignments or values to ref get only one final state, without isolation between Render processes.

function App(){
  const [count , setCount] = React.useState(0);
  const lastCount = React.useRef(count);
  const show = () = >{
    setTimeout(() = >{
      console.log('1 second ago count = 0, now count =${lastCount.current}`);
    },1000)}return (
    <div onClick={()= >{ show(); lastCount.current = 5; setCount(5); }}> Click on the function component (useRef)</div>
  );
}
Copy the code

Capture Value can Capture everything except Ref.

Difference between a class component and a Function component

  • function componentIt is a normal function and cannot be usedsetStateTherefore, it is also called stateless component
  • function componentThere is no concept of life cycle
  • function componentCapture Valuefeatures

React Hook

In fact, Hook technology has been extensively used in the previous section of function components, but it needs to be mentioned separately, mainly as a new feature. It is so important that it is almost always mentioned when we talk about React technology, so we still need to learn it and know how to implement it.

UseState use

It allows function components to have state and is very simple to use:

import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={()= > setCount(count + 1)}>
      Click me
      </button>
    </div>
  );
}
Copy the code
  • The first line:Introduce ReactuseStateThe hooks. It lets us store internals in function componentsstate 。
  • The third line:ExampleInside the component, we calluseStateHook declares a new onestateThe variable. It returns a pair of values to our named variable. Let’s call the variable thetacountBecause it stores clicks. We pass0As auseStateUnique argument to initialize it to0. The second returned value is itself a function. It allows us to updatecountThe value of PI, so we call it PIsetCount.
  • Line 7:When the user clicks the button, we pass a new value tosetCount. React will re-renderExampleComponents and put the latestcountTo it.

UseState principle

Based on the simplest use above, let’s take a simple implementation and demystify useState.

import React from "react";
import { render } from ".. /.. /index";

let _state; // Store the state outside

function useState(initialValue) {
  // If there is no _state, it is the first time. Copy initialValue to it
  _state = _state || initialValue;
  function setState(newState) {
    _state = newState;
    render();
  }
  return [_state, setState];
}

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <div>{count}</div>
      <button onClick={()= >{ setCount(count + 1); Click UseState}} ></button>
    </div>
  );
}

export default Counter;
Copy the code

Render function:

export const render = () = >{
  ReactDOM.render(
    <App />.document.getElementById('root')); } render();Copy the code

The useState function stores the value using the closure principle, and setState rerenders the component each time the Render method is called.

UseEffect use

It gives function components a life cycle equivalent to the following three life cycles:

  • componentDidMountThe component has been mounted as a DOM
  • componentDidUpdateComponent has been updated
  • componentWillUnmountComponent uninstall
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    document.title = `You clicked ${count} times`;
  },[count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={()= > setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
Copy the code

Code analysis:

  • When a component is mountedDOMStructure will be calledeffectWill change the title of the page
  • When a click event occurs, the component performs an update and is called againeffectTo update the page title
  • useEffectThe second argument is an array that writescount, which meanscountIt’s executed when the value changeseffect

React ensures that the DOM is updated every time an Effect is run.

UseEffect principle

Simple usage was introduced above, so let’s summarize its features:

  1. There are two parameterscallbackdependenciesAn array of
  2. ifdependenciesDoesn’t exist, thencallbackIt will be executed every time
  3. ifdependenciesIt exists only when it changes,callbackWill perform
import React,{useState} from "react";

let _deps; // _deps records the last useEffect dependency

function useEffect(callback, depArray) {
  consthasNoDeps = ! depArray;// If dependencies does not exist
  consthasChangedDeps = _deps ? ! depArray.every((el, i) = > el === _deps[i]) // Check whether the dependencies are exactly the same
    : true;
  /* If dependencies do not exist, or dependencies change */
  if(hasNoDeps || hasChangedDeps) { callback(); _deps = depArray; }}function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() = >{
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <div>{count}</div>
      <button onClick={()= >{ setCount(count + 1); Click UseState}} ></button>
    </div>
  );
}

export default Counter;
Copy the code

At this point, we’ve implemented another useEffect that works, which doesn’t seem that difficult.

So far, we have implemented the use estate and useEffect that work. But there’s a big problem: both can only be used once, because there’s only one _state and one _deps. Such as

const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
Copy the code

Count and username are always equal because they share a _state and there is no place to store two values separately. We need to be able to store multiple _states and _DEps.

At this point, one must think of data structures such as objects, arrays, and linked lists that can handle this situation.

The key is:

  1. For the first rendering, follow theuseState.useEffectThe order of thestate.depsAnd so on in ordermemoizedStateIn the array.
  2. Update when in order frommemoizedStateTake out the value of the last record.
let memoizedState = []; // hooks store in this array
let cursor = 0; // Currently memoizedState subscript

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // Return the current state and increment cursor by 1
}

function useEffect(callback, depArray) {
  consthasNoDeps = ! depArray;const deps = memoizedState[cursor];
  consthasChangedDeps = deps ? ! depArray.every((el, i) = > el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}
Copy the code

Code explanation:

  • So when you initialize, you put all of these in orderstatePut it in an array.
  • When you performsetOperating using the closure principle, eachstateThe correspondingcurrentCursorBoth have a copy of storage in memory, and do not pollute each other. This will get the appropriate index to update the value in the array, and then executerenderOperation Update interface.

In React, each component stores its own Hook information and uses linked lists instead of arrays.

Like some advanced interview questions:

  • How does a function component go from stateless to stateful?
  • useStateuseEffectHow does it work?

Do you already know the answers to these questions?