Performance optimization, essentially reducing the number of page re-renders. The article will be developed from two points:

  1. Class component: PureComponent
  2. Function component: Memo

A, PureComponent

Principle 1.

The core of a pure component is an internal override of the shouldComponentUpdate method. When a component is updated, if the props and state of the component are not changed, the render method will not trigger, saving the generation and comparison of the virtual DOM and improving performance.

ShouldComponentUpdate () in react.pureComponent is only a superficial comparison of objects. If the object contains complex data structures, deep differences may not be detected, resulting in false comparisons. So use this only when props and state are simple, or call forceUpdate() when the underlying data structure changes to ensure that the component is updated correctly. You can also use immutable objects to speed comparisons of nested data.

Also, shouldComponentUpdate() in React skips props updates for all subcomponent trees. Therefore, ensure that all child components are also pure components.

2. The train of thought

2-1. Realize PureComponent

At the heart of PureComponent is overriding the shouldComponentUpdate method. The shouldComponentUpdate function always returns true, which tells React to rerender the component in any case.

So we put shouldComponentUpdate in the PureComponent class with a new property and a new state. Internally, the shallowEqual method is used to compare with the old attribute and the old state respectively. If the new and old attribute or the new and old state are not equal, it will return true if it needs to be updated; if it is equal, it will return false if it does not need to be updated.

2 – (2) implement shallowEqual

ShouldComponentUpdate by default allows updates to return true, so within shallowEqual we need to check if we don’t update and return false.

  1. Determine the type of two objects: if one of them is not'object'Or one of them could be zeronull, are considered unequal returnsfalse
  2. Determine the key set of two objects: if the key length is not equal, it is considered unequalfalse
  3. Determine the key names of two objects: iterate over the first object and getkeyIf there is no key in the second object, or if the two values of the key with the same name are not equal, the task is unequalfalse

Note: Since this is a shallow comparison, simply determine if the reference addresses of two objects are equal, then it is considered equal and return true.

Second, the memo

Principle 1.

React.memo() is a higher-order function that, like the react.pureComponent, controls the re-rendering of function components.

React.memo() takes two arguments. The first is the component to optimize, and the second is the method to control rendering, which defaults to compare.

React. Memo () returns a purified component, MemoFuncComponent, which will be rendered in the JSX tag. React checks if the props and state of the component are the same as the next. If they are the same, the component will not be rendered; if they are different, the component will be rendered again.

2. The train of thought

2-1. Realize the React. Memo

printReact.memoThe call:We just have to mimic this structure asReact.memo()The return value of.compareIs a way to compare old and new properties or states,typeIs the original function component.

2 – (2) implement mountMemoComponent

When the component creates createDOM, it determines that if the node type is react. Memo, it calls the mountMemoComponent to mount it.

Deconstruct the Type and props from the virtual DOM, where type is the result returned in the first step. So: the type attribute is the original function component. To get the DOM to mount, just call the Type pass props. Add the prevProps property to the virtual DOM to record the old property so that you can compare the old property with the new one.

2-3. Implement updateMemoComponent

When a component updates an updateElement, it determines if the node type is react. Memo and calls updateMemoComponent to do the update.

When a component updates, it executes type, passing the old and new property objects to Compare, not updating them if they are the same, and updating them if they are different.

Three, code implementation

1. src/index.js

import React from "./react";
import ReactDOM from "./react-dom";
/** * ComponentUpdate (shoudlComponentUpdate) {/** * {shoudlComponentUpdate (shoudlComponentUpdate)

class ClassCounter extends React.PureComponent {
  render() {
    console.log("ClassCouter.render");
    return <div>ClassCounter: {this.props.count}</div>; }}function FunctionCounter(props) {
  console.log("FunctionCounter.render");
  return <div>FunctionCounter: {props.count}</div>;
}

const MemoFunctionCounter = React.memo(FunctionCounter);
console.log("MemoFunctionCounter", MemoFunctionCounter);

class App extends React.Component {
  state = {
    number: 0}; amountRef = React.createRef(); hanldeClick =() = > {
    let nextNum = this.state.number + parseInt(this.amountRef.current.value);
    this.setState({
      number: nextNum,
    });
  };

  render() {
    return (
      <div>
        <ClassCounter count={this.state.number} />{/ *<FunctionCounter count={this.state.number} />* /}<MemoFunctionCounter count={this.state.number} />{/* When the real DOM is created for the input, the value is given to the ref's current attribute */}<input ref={this.amountRef} defaultValue={0} />
        <button onClick={this.hanldeClick}>+</button>
      </div>
    );
  }
}

ReactDOM.render(<App />.document.getElementById("root"));

Copy the code

2 src/react.js

> > > class PureComponent extends Component {> > >shouldComponentUpdate(nextProps, nextState){> > >// Return true if the attributes are not equal or the status is not equal, indicating that an update is required
> > >     return(> > >! shallowEqual(this.props, nextProps) || > > > ! shallowEqual(this.state, nextState) > > > ); > > >} > > >}Copy the code

3 src/utils.js

> > > export function shallowEqual(obj1, obj2) {> > >// If two objects reference the same address, they are considered equal
> > >   if (obj1 === obj2) {
> > >     return true; > > >} > >// Any object that is not an object or whose value is null is not equal
> > >   if(> > >typeofobj1 ! = ="object" ||
> > >     obj1 === null ||
> > >     typeofobj2 ! = ="object" ||
> > >     obj2 === null> > >) {> > >return false; > > >} > >let keys1 = Object.keys(obj1);
> > >   let keys2 = Object.keys(obj2);
> > >   // If the number of attributes is not equal, it is not equal
> > >   if(keys1.length ! == keys2.length) { > > >return false; > > >} > > > > > >for (let key of keys1) {
> > >     if(! obj2.hasOwnProperty(key) || obj1[key] ! == obj2[key]) { > > >return false; > > > >} > > > > > >return true; > > >}Copy the code

4. src/react-dom.js

import {
  REACT_TEXT,
  REACT_FORWARD_REF,
  MOVE,
  PLACEMENT,
  REACT_PROVIDER,
  REACT_CONTEXT,
> > >   REACT_MEMO,
} from "./constants";
import { addEvent } from "./event";

/** * Insert the virtual DOM into the real DOM@param {*} Vdom Virtual DOM/React element *@param {*} Container Real DOM container */
function render(vdom, container) {
  mount(vdom, container);
}

/** the page mounts the real DOM */
function mount(vdom, parentDOM) {
  // Turn the virtual DOM into the real DOM
  let newDOM = createDOM(vdom);
  // Append the real DOM to the container
  parentDOM.appendChild(newDOM);
  if (newDOM.componentDidMount) newDOM.componentDidMount();
}

/** * Turn the virtual DOM into the real DOM *@param {*} Vdom Virtual DOM *@return Real DOM * /
function createDOM(vdom) {
  if(! vdom)return null; // null/und is also a valid DOM
  let { type, props, ref } = vdom;
  let dom; / / true DOM
  
> > >   if (type && type.$$typeof === REACT_MEMO) {
> > >     returnmountMemoComponent(vdom); > > >}else if (type && type.$$typeof === REACT_PROVIDER) {
    return mountProviderComponent(vdom);
  } else if (type && type.$$typeof === REACT_CONTEXT) {
    return mountContextComponent(vdom);
  } else if (type && type.$$typeof === REACT_FORWARD_REF) {
    return mountForwardComponent(vdom);
  } else if (type === REACT_TEXT) {
    If the element is text, create a text node
    dom = document.createTextNode(props.content);
  } else if (typeof type === "function") {
    if (type.isReactComponent) {
      // This is a class component
      return mountClassComponent(vdom);
    } else {
      // Function components
      returnmountFunctionComponent(vdom); }}else if (typeof type === "string") {
    // Create a DOM node span div p
    dom = document.createElement(type);
  }

  // Handle attributes
  if (props) {
    // We will implement component and page updates after updating DOM properties.
    updateProps(dom, {}, props);
    let children = props.children;
    // If children is a React element, it is also a virtual DOM
    if (typeof children === "object" && children.type) {
      // Mount the child virtual DOM to the parent DOM
      mount(children, dom);
      props.children.mountIndex = 0;
    } else if (Array.isArray(children)) {
      reconcileChildren(children, dom);
    }
  }
  vdom.dom = dom; // Add a dom attribute to the virtual DOM to point to the real DOM corresponding to the virtual DOM
  if (ref) ref.current = dom;
  return dom;
}

> > > /** Mount the Memo component */
> > > function mountMemoComponent(vdom) {> > >let { type, props } = vdom; // Type is the return value of the Memo component. The type attribute is the original function component
> > >   let renderVdom = type.type(props);
> > >   vdom.prevProps = props; // Record the old property object for comparison when updating later
> > >   vdom.oldRenderVdom = renderVdom;
> > >   returncreateDOM(renderVdom); > > >}/** Mount the Provider component */
function mountProviderComponent(vdom) {
  let { type, props } = vdom; // type = { $$typeof: REACT_PROVIDER, _context: context }
  let context = type._context; // { $$typeof: REACT_CONTEXT, _currentValue: undefined }
  // 1. Assign the value to the value used by the Provider
  context._currentValue = props.value;
  // 2. Render the children of the Provider package
  let renderVdom = props.children;
  // Prepare for future updates
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** Mount the Context component -Consumer */
function mountContextComponent(vdom) {
  let { type, props } = vdom;
  let context = type._context; // type = { $$typeof: REACT_CONTEXT, _context: context }
  let renderVdom = props.children(context._currentValue);
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** Mount class components */
function mountClassComponent(vdom) {
  let { type: ClassComponent, props, ref } = vdom;
  // Pass the class component's properties to the class component's constructor,
  // Create an instance of the class component and return the component instance object so that the instance's methods can be executed directly when the component is unloaded
  let classInstance = new ClassComponent(props);
  if (classInstance.contextType) {
    // Assign the value to the instance's context property
    classInstance.context = ClassComponent.contextType._currentValue;
  }

  // Mount classInstance on the virtual DOM, pointing to an instance of the class
  vdom.classInstance = classInstance;
  // If ref is present, the instance is assigned to the current attribute
  if (ref) ref.current = classInstance;
  if (classInstance.componentWillMount) {
    classInstance.componentWillMount();
  }
  // It can be the virtual DOM of a native component, a class component, or a function component
  let renderVdom = classInstance.render();
  // Add oldRenderVdom=renderVdom to the class instance when the class component is first mounted
  // oldRenderVdom property of the virtual DOM of the class component, pointing to renderVdom
  vdom.oldRenderVdom = classInstance.oldRenderVdom = renderVdom;
  let dom = createDOM(renderVdom);
  if (classInstance.componentDidMount) {
    dom.componentDidMount = classInstance.componentDidMount.bind(classInstance);
  }
  return dom;
}

/** Mount the function component */
function mountFunctionComponent(vdom) {
  let { type: functionComponent, props } = vdom;
  // Get the virtual DOM that the component will render
  let renderVdom = functionComponent(props);
  The oldRenderVdom property of the function component points to the rendered virtual DOM--renderVdom
  vdom.oldRenderVdom = renderVdom;
  return createDOM(renderVdom);
}

/** Mounts the function component of the forwarded ref */
function mountForwardComponent(vdom) {
  let { type, props, ref } = vdom;
  let renderVdom = type.render(props, ref);
  return createDOM(renderVdom);
}

/** If the child element is an array, traverse mount to container */
function reconcileChildren(children, parentDOM) {
  // Mount the mountIndex attribute to each virtual DOM to record its index
  children.forEach((childVdom, index) = > {
    childVdom.mountIndex = index;
    mount(childVdom, parentDOM);
  });
}

/** * Update the new attribute to the real DOM *@param {*} Dom Real DOM *@param {*} OldProps Old property object *@param {*} NewProps New property object */
function updateProps(dom, oldProps, newProps) {
  for (let key in newProps) {
    if (key === "children") {
      // Child nodes are handled separately
      continue;
    } else if (key === "style") {
      let styleObj = newProps[key];
      for (let attr instyleObj) { dom.style[attr] = styleObj[attr]; }}else if (/^on[A-Z].*/.test(key)) {
      // Bind event ==> dom.onclick = event function
      // dom[key.toLowerCase()] = newProps[key];
      // Instead of binding event functions to the corresponding DOM, events are delegated to document objects
      addEvent(dom, key.toLowerCase(), newProps[key]);
    } else{ dom[key] = newProps[key]; }}for (let key in oldProps) {
    // If an attribute exists in the old attribute object, but the new attribute does not, it needs to be deleted
    if(! newProps.hasOwnProperty(key)) { dom[key] =null; }}}/** * DOM-diff: recursively compare the old virtual DOM and the new virtual DOM, find the differences between the two, and minimize the differences to the real DOM *@param {*} ParentDOM Actual DOM *@param {*} OldVdom The old virtual DOM *@param {*} NewVdom New virtual DOM *@param {*} NextDOM new virtual DOM * */
export function compareToVdom(parentDOM, oldVdom, newVdom, nextDOM) {
  /** / OldRenderVdom = findDOM(oldVdom); oldRenderVdom = findDOM(oldVdom); oldRenderVdom = findDOM(oldVdom); Let newDOM = createDOM(newVdom); Parentdom.replacechild (newDOM, oldDOM); // Replace old real DOM with new real DOM parentdom.replacechild (newDOM, oldDOM); * /

  // 1. Old - nothing new - nothing: nothing
  if(! oldVdom && ! newVdom)return;
  // 2. Old-new-none: Delete the old node directly
  if(oldVdom && ! newVdom) { unMountVdom(oldVdom); }// 3. Old-none new-yes: Insert a node
  if(! oldVdom && newVdom) { mountVdom(parentDOM, newVdom, nextDOM); }// 4-1. Old - New - Yes: Check the different types, delete the old and add the new
  if(oldVdom && newVdom && oldVdom.type ! == newVdom.type) { unMountVdom(oldVdom); mountVdom(parentDOM, newVdom, nextDOM); }// 4-2. Old - New - Yes: Judge the same type, perform DOM-diff, and the node can be reused
  if(oldVdom && newVdom && oldVdom.type === newVdom.type) { updateElement(oldVdom, newVdom); }}/** * update the old and new DOM types the same ---- dom-diff essence * If the old and new DOM types are the same, then the node can be reused */
function updateElement(oldVdom, newVdom) {> > >/ / memo components
> > >   if (oldVdom.type.$$typeof === REACT_MEMO) {
> > >     updateMemoComponent(oldVdom, newVdom);
> > >     / / Consumer component> > >}else if (oldVdom.type.$$typeof === REACT_PROVIDER) {
    updateProviderComponent(oldVdom, newVdom);
    / / the Provider component
  } else if (oldVdom.type.$$typeof === REACT_CONTEXT) {
    updateContextComponent(oldVdom, newVdom);
    // Old and new nodes are text nodes: reuse old nodes, replace content
  } else if (oldVdom.type === REACT_TEXT) {
    // The old real DOM gives the new DOM attributes to change the content
    let currentDOM = (newVdom.dom = findDOM(oldVdom));
    currentDOM.textContent = newVdom.props.content;
    // Native node
  } else if (typeof oldVdom.type === "string") {
    let currentDOM = (newVdom.dom = findDOM(oldVdom));
    // Update attributes
    updateProps(currentDOM, oldVdom.props, newVdom.props);
    // Compare sons recursively
    updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
    // Class component or function component
  } else if (typeof oldVdom.type === "function") {
    / / class components
    if (oldVdom.type.isReactComponent) {
      // Synchronize the instance first
      newVdom.classInstance = oldVdom.classInstance;
      updateClassComponent(oldVdom, newVdom);
      // Function components
    } else{ updateFunctionComponent(oldVdom, newVdom); }}} > > >/** Update the Memo component */
> > > function updateMemoComponent(oldVdom, newVdom) {> > >let { type, prevProps } = oldVdom; // Type old attribute
> > >   // New and old attributes are not equal, update
> > >   if(! type.compare(prevProps, newVdom.props)) { > > >// Get the old real DOM
> > >     let oldDOM = findDOM(oldVdom);
> > >     // Get the real parent node
> > >     let parentDOM = oldDOM.parentNode;
> > >     let { type, props } = newVdom;
> > >     let renderVdom = type.type(props);
> > >     compareToVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
> > >     newVdom.prevProps = props;
> > >     newVdom.oldRenderVdom = renderVdom;
> > >     // New attributes are equal to old attributes> > >}else{ > > > newVdom.prevProps = prevProps; > > > newVdom.oldRenderVdom = oldVdom.oldRenderVdom; > > >} > > >}/** Update the Proveder component */
function updateProviderComponent(oldVdom, newVdom) {
  // Get the old real DOM
  let oldDOM = findDOM(oldVdom);
  // Get the real parent node
  let parentDOM = oldDOM.parentNode;
  let { type, props } = newVdom;
  let context = type._context;
  // Assign the new attribute to _currentValue
  context._currentValue = props.value;
  let renderVdom = props.children;
  compareToVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  newVdom.oldRenderVdom = renderVdom;
}

/** Update the context component */
function updateContextComponent(oldVdom, newVdom) {
  // Get the old real DOM
  let oldDOM = findDOM(oldVdom);
  // Get the real parent node
  let parentDOM = oldDOM.parentNode;
  let { type, props } = newVdom;
  let context = type._context;
  / / from the value
  let renderVdom = props.children(context._currentValue);
  compareToVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom);
  newVdom.oldRenderVdom = renderVdom;
}

/** * Update class components *@param {*} oldVdom
 * @param {*} newVdom* /
function updateClassComponent(oldVdom, newVdom) {
  // Reuse old class component instances
  let classInstance = (newVdom.classInstance = oldVdom.classInstance);
  if (classInstance.componentWillReceiveProps) {
    classInstance.componentWillReceiveProps(newVdom.props);
  }
  classInstance.updater.emitUpdate(newVdom.props);
}

/** * Update function component *@param {*} oldVdom
 * @param {*} newVdom* /
function updateFunctionComponent(oldVdom, newVdom) {
  // Get the parent of the old real DOM
  let parentDOM = findDOM(oldVdom).parentNode;
  let { type, props } = newVdom;
  let newRenderVdom = type(props);
  // Each time the function component is updated, the function is re-executed to retrieve the new virtual DOM
  compareToVdom(parentDOM, oldVdom.oldRenderVdom, newRenderVdom);
  newVdom.newRenderVdom = newRenderVdom;
}

/** * recursively compares child nodes *@param {*} parentDOM
 * @param {*} oldVChildren
 * @param {*} newVChildren* /
function updateChildren(parentDOM, oldVChildren, newVChildren) {
  // To facilitate subsequent dom-diff, save it as an array
  oldVChildren = (
    Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren]
  ).filter((item) = > item);
  newVChildren = (
    Array.isArray(newVChildren) ? newVChildren : [newVChildren]
  ).filter((item) = > item);
  Create map {key of virtual DOM: virtual DOM}
  let keyedOldMap = {};
  oldVChildren.forEach((oldVChild, index) = > {
    let oldKey = oldVChild.key ? oldVChild.key : index;
    keyedOldMap[oldKey] = oldVChild;
  });
  // Patch pack: store the operation to be performed
  let patch = [];
  // The last placed index that does not need to be moved
  let lastPlaceIndex = 0;
  // dom-diff 2. Traverse the new array to find the old virtual DOM
  newVChildren.forEach((newVChild, index) = > {
    newVChild.mountIndex = index;
    let newKey = newVChild.key ? newVChild.key : index;
    // Find if there is a node with this key in the old virtual DOM
    let oldVChild = keyedOldMap[newKey];
    // If found, reuse the old node
    if (oldVChild) {
      / / update first
      updateElement(oldVChild, newVChild);
      // Move oldVChild to mountIndex
      if (oldVChild.mountIndex < lastPlaceIndex) {
        patch.push({
          type: MOVE,
          oldVChild,
          newVChild,
          mountIndex: index,
        });
      }
      // Delete the node that has been overused
      delete keyedOldMap[newKey];
      lastPlaceIndex = Math.max(oldVChild.mountIndex, lastPlaceIndex);
    } else {
      // If not, insert a new node
      patch.push({
        type: PLACEMENT,
        newVChild,
        mountIndex: index, }); }});// dom-diff 3. Get the element to move
  let moveChildren = patch
    .filter((action) = > action.type === MOVE)
    .map((action) = > action.oldVChild);
  // Iterate over the elements left over from the map.
  Object.values(keyedOldMap)
    .concat(moveChildren)
    .forEach((oldVChild) = > {
      // Get the old DOM
      let currentDOM = findDOM(oldVChild);
      parentDOM.removeChild(currentDOM);
    });
  // dom-diff 4. Insert or move the node
  patch.forEach((action) = > {
    let { type, oldVChild, newVChild, mountIndex } = action;
    // Real DOM node collection
    let childNodes = parentDOM.childNodes;
    if (type === PLACEMENT) {
      // Create a new real DOM from the new virtual DOM
      let newDOM = createDOM(newVChild);
      // Get the real DOM at the corresponding index in the old DOM
      let childNode = childNodes[mountIndex];
      if (childNode) {
        parentDOM.insertBefore(newDOM, childNode);
      } else{ parentDOM.appendChild(newDOM); }}else if (type === MOVE) {
      let oldDOM = findDOM(oldVChild);
      let childNode = childNodes[mountIndex];
      if (childNode) {
        parentDOM.insertBefore(oldDOM, childNode);
      } else{ parentDOM.appendChild(oldDOM); }}});// // Maximum length
  // let maxLength = Math.max(oldVChildren.length, newVChildren.length);
  // // Each is compared in depth
  // for (let i = 0; i < maxLength; i++) {
  // // search in the old virtual DOM, there are old nodes and the old node really corresponds to a real DOM node, and the index is bigger than me (in order to find its next node).
  // let nextVdom = oldVChildren.find(
  // (item, index) => index > i && item && findDOM(item)
  / /);
  // compareToVdom(
  // parentDOM,
  // oldVChildren[i],
  // newVChildren[i],
  // nextVdom && findDOM(nextVdom)
  / /);
  // }
}

/** * Insert new real DOM *@param {}} parentDOM
 * @param {*} vdom
 * @param {*} nextDOM* /
function mountVdom(parentDOM, newVdom, nextDOM) {
  let newDOM = createDOM(newVdom);
  if (nextDOM) {
    parentDOM.insertBefore(newDOM, nextDOM);
  } else {
    parentDOM.appendChild(newDOM);
  }
  if(newDOM.componentDidMount) { newDOM.componentDidMount(); }}/** * delete the old real DOM *@param {*} Vdom The old virtual DOM */
function unMountVdom(vdom) {
  let { props, ref } = vdom;
  // Get the old real DOM
  let currentDOM = findDOM(vdom);
  // If the child node is a class component, its unload lifecycle function is also executed
  if (vdom.classInstance && vdom.classInstance.componentWillUnmount) {
    vdom.classInstance.componentWillUnmount();
  }
  // If ref exists, delete the real DOM corresponding to ref
  if (ref) ref.current = null;
  // Cancel the listener function
  Object.keys(props).forEach((propName) = > {
    if (propName.slice(0.2) = = ="on") {
      // Events do this in the real DOM
      // const eventName = propName.slice(2).toLowerCase()
      // currentDOM.removeEventListener(eventName, props[propName])
      // But we first handle the synthesized event, which is registered on the store
      deletecurrentDOM.store; }});// Delete all children recursively if there are any
  if (props.children) {
    let children = Array.isArray(props.children)
      ? props.children
      : [props.children];
    children.forEach(unMountVdom);
  }
  // Delete yourself from the parent node
  if (currentDOM) currentDOM.parentNode.removeChild(currentDOM);
}

/** The virtual DOM returns the real DOM */
export function findDOM(vdom) {
  if(! vdom)return null;
  // If there are DOM attributes, the vDOM is the virtual DOM of the native component, and there are DOM attributes pointing to the real DOM
  if (vdom.dom) {
    return vdom.dom;
  } else {
    returnfindDOM(vdom.oldRenderVdom); }}const ReactDOM = {
  render,
};
export default ReactDOM;

Copy the code