Dom-diff knowledge is no longer needed, and this article will implement a complete DOM-DIff step by step. Welcome to watch and guide ~

First, the idea of implementation

  • Green element: Update
  • Blue element: Insert
  • Orange element: Insert and update
  • Red element: Delete

1. Build the map

Associate the key of the old node with its corresponding virtual DOM node and store it in map, forming:

map = { 
    'A': 'A corresponding virtual DOM'.'B': 'virtual DOM corresponding to B'.'C': 'C corresponding virtual DOM'.'D': Virtual DOM corresponding to'D '.'E': 'E corresponding to the virtual DOM'.'F': 'virtual DOM corresponding to F',}Copy the code

In this way, we can find the corresponding old node by key

2. Loop through the new node array

Record the key of each new node during traversal, and obtain the virtual DOM node corresponding to this key. At this point, take the key to search in the map of the first step. If it can be found, it means that there is a corresponding old node, so we will reuse the old node:

2-1. Update the attributes of the new node to the old node

2-2. Check whether the node is moved

To determine whether the current node needs to be moved, use the index of a node that does not need to be moved on the lastPlaceIndex variable. If the index mounted on the old node is greater than the value of lastPlaceIndex, there is no need to move it. If less than the value of lastPlaceIndex, move to the current index location. The element moved will be added with MOVE type, and then pushed to patch, and the node reused will be deleted.

2-3. Get the element to move

After the above steps, the remaining nodes in the old node array are the ones that have not been reused. We need to delete the node from the parent node.

2-4. Insert or move a node

After creating a new real DOM from the new virtual DOM, we get the new DOM at the index corresponding to the old DOM, and then inserBefore the new DOM. If there is no new DOM appenedChild directly.

Two, code implementation

1. src/index.js

import React from "./react";
import ReactDOM from "./react-dom";

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: ["A"."B"."C"."D"."E"."F"]}; } handClick =() = > {
    this.setState({
      list: ["A"."C"."E"."B"."G"]}); };render() {
    return (
      <div>
        <ul>
          {this.state.list.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
        <button onClick={this.handClick}>button</button>
      </div>
    );
  }
}
ReactDOM.render(<Counter />.document.getElementById("root"));

Copy the code

2. src/constants.js

//React element: h1 span div
export const REACT_ELEMENT = Symbol("react.element");

// Text: string or number
export const REACT_TEXT = Symbol("react.text");

// the function component forwards the ref
export const REACT_FORWARD_REF = Symbol("react.forward_ref");

> > > // Insert the node
> > > export const PLACEMENT = Symbol("PLACEMENT");
> > > 
> > > // Move the node
> > > export const MOVE = Symbol("MOVE");
Copy the code

3. src/react-dom.js

This file is longer, so the code not related to dom-diff has been pruned. If you want to see the full code, please go to my code Cloud repository, or check it out in the previous section.

> > > import { REACT_TEXT, REACT_FORWARD_REF, MOVE, PLACEMENT } 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 *@param {*} Vdom Virtual DOM *@return Real DOM * /
function createDOM(vdom) {}

/** Mount class components */
function mountClassComponent(vdom) {}

/** Mount the function component */
function mountFunctionComponent(vdom) {}

/** Mounts the function component of the forwarded ref */
function mountForwardComponent(vdom) {}

/** 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) {}

/** * 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) {
  // 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); }}/** * the essence of dom-diff -- update the old and new DOM types * If the old and new DOM types are the same, then the node can be reused */
function updateElement(oldVdom, newVdom) {
  // Old and new nodes are text nodes: reuse old nodes, replace content
  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 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];
  newVChildren = Array.isArray(newVChildren) ? newVChildren : [newVChildren];
> > >   Create map {key of virtual DOM: virtual DOM}
> > >   let keyedOldMap = {};
> > >   oldVChildren.forEach((oldVChild, index) = >{> > >letoldKey = 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
> > >       letcurrentDOM = 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 { type, 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

Third, summary

Move rule: New arrays with smaller indexes and higher status do not move. According to the above logic, if we move the last element to the first, the rest of the elements move, so we should try to minimize the number of top-ups.