purpose

This time, the patch concept at the core of vUE and React framework is mainly implemented to prevent repeated rendering to generate DOM nodes and improve efficiency. Analyze the differences between vNodes of render before and after to determine the update, deletion and addition of DOM nodes. Here we can refer to the React official instructions for this.

React Learn – Coordinate

We’re going to make it a little bit simpler here

Patch process

How to patch? What is the relationship to the previously implemented mount? Here we can draw a flow chart to show the render process

patchThe contrast between old and newVNodeWe will also render our virtual nodes to generate DOM nodes according to this process.

Code implementation

render

Let’s re-implement the render function this time. The following part of the process is implemented. N1 is the last render VNode, we will mount the render VNode to the Container DOM node for the next render comparison

//render.js
/** * Mount the virtual DOM node to the real DOM node *@param {Object} vnode
 * @param {HTMLElement} container* /
export function render(vnode, container) {
  const preVNode = container._vnode;
  if(! vnode) {if(preVNode) { unmount(preVNode); }}else {
    patch(preVNode, vnode, container);
  }
  container._vnode = vnode;
}
Copy the code

If n2 nextVNode does not exist, n1 (preVNode) will be unloaded

unmount

We implement the unmount method, which adds an EL attribute to vnode, storing the corresponding DOM node of the Vnode. When the DOM node is created, el is assigned to the node

//render.js
function unmount(vnode) {
  // Extract the type and corresponding DOM node
  const { shapeFlag, el } = vnode;
  // Whether it is a component
  if (shapeFlag & ShapeFlags.COMPONENT) {
    unmountComponent(vnode);
  } else if (shapeFlag & ShapeFlags.FRAGMENT) {
  //Fragment
    unmountFragment(vnode);
  } else {
  // Remove text or Elementel.parentNode.removeChild(el); }}Copy the code
//render.js
function mountElement(vnode, container) {
  const { type, props } = vnode;
  const el = document.createElement(type);
  mountProps(props, el);
  mountChildren(vnode, el);
  container.appendChild(el);
  // Add the el attribute
  vnode.el = el;
}

function mountTextNode(vnode, container) {
  const textNode = document.createTextNode(vnode.children);
  container.appendChild(textNode);
  // Add the el attribute
  vnode.el = textNode;
}
//vnode.js
/ * * *@param {string | Object | Text | Fragment} type ;
 * @param {Object | null} props
 * @param {string |number | Array | null} children
 * @returns VNode* /
export function h(type, props, children) {
  // Determine the type
  let shapeFlag = 0;
  if (isString(type)) {
    shapeFlag = ShapeFlags.ELEMENT;
  } else if (type === Text) {
    shapeFlag = ShapeFlags.TEXT;
  } else if (type === Fragment) {
    shapeFlag = ShapeFlags.FRAGMENT;
  } else {
    shapeFlag = ShapeFlags.COMPONENT;
  }
  / / determine the children
  if (isString(children) || isNumber(children)) {
    shapeFlag |= ShapeFlags.TEXT_CHILDREN;
    children = children.toString();
  } else if (isArray(children)) {
    shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }
  // Add the el attribute to vnode
  return {
    type,
    props,
    children,
    shapeFlag,
    el: null
  };
}
Copy the code

The unmountComponent and unmountFragment are defined and implemented later

patch

Next, patch compares the difference between old and new VNodes to update, delete, and add DOM nodes

//render.js
function patch(n1, n2, container) {
  // if n1 is different from n2, unmount n1
  if(n1 && ! isSameVNode(n1, n2)) { unmount(n1);// set n1 to null after unmount
    n1 = null;
  }
  // Determine the type n2 COMPONENT, TEXT, FRAGMENT, ELEMENT
  const { shapeFlag } = n2;
  if (shapeFlag & ShapeFlags.COMPONENT) {
    processComponent(n1, n2, container);
  } else if (shapeFlag & ShapeFlags.TEXT) {
    processText(n1, n2, container);
  } else if (shapeFlag & ShapeFlags.FRAGMENT) {
    processFragment(n1, n2, container);
  } else{ processElement(n1, n2, container); }}function isSameVNode(n1, n2) {
  return n1.type === n2.type;
}
Copy the code

The four process methods compare n1 and n2 to update the Container DOM node. We first implement processText processFragment processElement

processText

Let’s start by implementing a simple processText method

The implementation code

//render.js
function processText(n1, n2, container) {
  if (n1) {
    // If n1 exists, since both are text nodes, update n1's DOM node directly to point n2's EL to N1's EL
    n2.el = n1.el;
    n1.el.textContent = n2.children;
  } else {
    // Mount the text node directly if it does not existmountTextNode(n2, container); }}function mountTextNode(vnode, container) {
  const textNode = document.createTextNode(vnode.children);
  container.appendChild(textNode);
  vnode.el = textNode;
}
Copy the code

processElement

The first is also judgmentn1Existence does not exist, does not exist directlymountElementIf it exists, it must determine the properties of n1 and n2props, andchildrenTo update the DOM node

//render.js
function processElement(n1, n2, container) {
 if (n1) {
   patchElement(n1, n2);
 } else{ mountElement(n2, container); }}function patchElement(n1, n2) {
 n2.el = n1.el;
 // Compare the props of the update element
 patchProps(n1.props, n2.props, n2.el);
 // Update children
 patchChildren(n1, n2, n2.el);
}

function mountElement(vnode, container) {
 const { type, props, shapeFlag, children } = vnode;
 const el = document.createElement(type);
 patchProps(null, props, el);
  // The operation to determine the child node is placed in mountElement
 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
   mountTextNode(vnode, el);
 } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
   mountChildren(children, el);
 }
 container.appendChild(el);
 vnode.el = el;
}

function mountChildren(children, container) {
 children.forEach((child) = > {
 If the first parameter is null, the new node can be mounted
   patch(null, child, container);
 });
}
Copy the code

Here we are going to implement the methods patchProps and patchChildren

patchProps

Determine whether the props attribute of N1 needs to be updated or deleted based on the props attribute of N2.

//patchProps.js
export function patchProps(oldProps, newProps, el) {
  if (oldProps === newProps) return;
  // Prevent code error when oldProps or newProps is null
  oldProps = oldProps || {};
  newProps = newProps || {};
  // Check whether the oldProps has an attribute and whether it needs to be updated
  for (const key in newProps) {
    const next = newProps[key];
    const prev = oldProps[key];

    if (prev !== next) {
      patchDomProp(prev, next, key, el);
    }
  }
  // If {class: 'a',style: {}} {style: {}}, {class: 'a',style: {}} {style: {}}, then the dom node should remove the property that does not appear in newProps
  for (const key in oldProps) {
    if (newProps[key] == null) {
      patchDomProp(oldProps[key], null, key, el); }}}Copy the code

Implement specific patchDomProp, which can refer to the mountProps method implemented last time.

//patchProps.js
const domPropsRE = /[A-Z]|^(value | checked | selected | muted | disabled)$/;
function patchDomProp(prev, next, key, el) {
  switch (key) {
    case 'class':
      // Class is updated directly to next with the class attribute null.
      el.className = next || ' ';
      break;
    case 'style':
      // When newProps has no style attribute, the DOM node removes style
      if (next == null) {
        el.removeAttribute('style');
      } else {
      // if the style property is present, newProps will iterate over the style to update the corresponding el property value
        for (const styleName in next) {
          el.style[styleName] = next[styleName];
        }
        // Remove attributes present in oldProps style that are not present in newProps style
        if (prev) {
          for (const styleName in prev) {
            if (next[styleName] == null) {
              el.style[styleName] = ' '; }}}}break;
    default:
    // Use the value in newProps
      if (/^on[^a-z]/.test(key)) {
        const eventName = key.slice(2).toLowerCase();
        if (prev) {
          el.removeEventListener(eventName, prev);
        }
        if(next) { el.addEventListener(eventName, next); }}else if (domPropsRE.test(key)) {
         checked
        if (next === ' ' && isBoolean(el[key])) {
          next = true;
        }
        el[key] = next;
      } else {
        // For example, a custom attribute {custom: "} should be set to .
        {custom: null} apply removeAttribute to 
        if (next == null || next === false) {
          el.removeAttribute(key);
        } else{ el.setAttribute(key, next); }}break; }}Copy the code

patchChildren

PatchChild needs to consider nine cases of N1 and N2, as shown in the figure below

One realization after another:

//render.js
function patchChildren(n1, n2, container) {
  N1,n2, and children
  const { shapeFlag: prevShapeFlag, children: c1 } = n1;
  const { shapeFlag, children: c2 } = n2;
  // Three types of n2 TEXT Array NULL
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    //n2 is a shorthand for the three cases of TEXT
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1);
    }
    // Update the DOM element text information when the text is different, i.e. children
    if(c1 ! == c2) { container.textContent = c2; }// when n2 is ARRAY_CHILDREN
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
      container.textContent = ' ';
      mountChildren(c2, container);
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      patchArrayChildren(c1, c2, container);
    } else{ mountChildren(c2, container); }}else {
    if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
      container.textContent = ' ';
    } else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1); }}}Copy the code

Here we will implement unmountChildren and patchArrayChildren and mountChildren methods again

//render.js
// Call unmount one by one
function unmountChildren(children) {
  children.forEach((child) = > {
    unmount(child);
  });
}
// Mount the child node directly using patch
function mountChildren(children, container) {
  children.forEach((child) = > {
    patch(null, child, container);
  });
}
Copy the code

The patchArrayChildren method takes into account the length of c1 and c2

C1: a b c d c2: a b C d e f g or c1: a b C d e f g c2: a b C dCopy the code

So patch according to their common length, c1 unmoount, C2 unmount

//render.js
function patchArrayChildren(c1, c2, container) {
  const oldLength = c1.length;
  const newLength = c2.length;
  const commonLength = Math.min(oldLength, newLength);

  for (let i = 0; i < commonLength; i++) {
    patch(c1[i], c2[i], container);
  }

  if (oldLength > newLength) {
    unmountChildren(c1.slice(commonLength));
  } else if(oldLength < newLength) { mountChildren(c2.slice(commonLength), container); }}Copy the code

processFragment

Code implementation

//render.js
function processFragment(n1, n2, container) {
  if (n1) {
    patchChildren(n1, n2, container);
  } else{ mountChildren(n2.children, container); }}Copy the code

Test it out and see what happens

//index.js
import { render, h, Fragment } from './runtime';

render(
  h('ul', null, [
    h('li', null, 'first'),
    h(Fragment, null, []),
    h('li', null, 'last'),
  ]),
  document.body
);

setTimeout(() => {
  render(
    h('ul', null, [
      h('li', null, 'first'),
      h(Fragment, null, [h('li', null, 'middle')]),
      h('li', null, 'last'),
    ]),
    document.body
  );
}, 2000);
Copy the code

First render OK, 2 seconds later re-render middle mounted to the very back, problem.

The reason is the second timerenderwhenFragmentChild elementsmiddleWhen mounting, create a new DOM node to mount toFragmentThe parent nodeulIt’s up, because it’s usingappendChildIt went straight to the back, butlastThe node hasn’t changed so it’s still not updated. It’s still infirstIn the back, so in the backlastThere is a problem with the insertion position

The solution is to add an Anchor attribute to the Fragment element. ProcessFragment creates two empty text nodes that represent the beginning and end of the fragment insertion. The child element is positioned in the DOM based on an anchor. Created using insertBefore

Add an anchor to a VNode

//vnode.js
/ * * *@param {string | Object | Text | Fragment} type ;
 * @param {Object | null} props
 * @param {string |number | Array | null} children
 * @returns VNode* /
export function h(type, props, children) {
  // Determine the type
  let shapeFlag = 0;
  if (isString(type)) {
    shapeFlag = ShapeFlags.ELEMENT;
  } else if (type === Text) {
    shapeFlag = ShapeFlags.TEXT;
  } else if (type === Fragment) {
    shapeFlag = ShapeFlags.FRAGMENT;
  } else {
    shapeFlag = ShapeFlags.COMPONENT;
  }
  / / determine the children
  if (isString(children) || isNumber(children)) {
    shapeFlag |= ShapeFlags.TEXT_CHILDREN;
    children = children.toString();
  } else if (isArray(children)) {
    shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }
  return {
    type,
    props,
    children,
    shapeFlag,
    el: null.anchor: null
  };
}
Copy the code

Rewrite the processFragment method

//render.js
function processFragment(n1, n2, container, anchor) {
// Determine whether n1 has any anchor, if so, directly use the words before N1 that do not
  const fragmentStartAnchor = (n2.el = n1
    ? n1.el
    : document.createTextNode(' '));
  const fragmentEndAnchor = (n2.anchor = n1
    ? n1.anchor
    : document.createTextNode(' '));

  if (n1) {
    patchChildren(n1, n2, container, fragmentEndAnchor);
  } else {
    // Insert the tag
    container.insertBefore(fragmentStartAnchor, anchor);
    container.insertBefore(fragmentEndAnchor, anchor);
    // The fragmentEndAnchor here represents before it is mountedmountChildren(n2.children, container, fragmentEndAnchor); }}Copy the code

Rewrite the mountElment, mountTextNode method

//render.js
function mountTextNode(vnode, container, anchor) {
  const textNode = document.createTextNode(vnode.children);
  container.insertBefore(textNode, anchor);
  vnode.el = textNode;
}

function mountElement(vnode, container, anchor) {
  const { type, props, shapeFlag, children } = vnode;
  const el = document.createElement(type);
  patchProps(null, props, el);

  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    mountTextNode(vnode, el);
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(children, el);
  }

  //container.appendChild(el);
  container.insertBefore(el, anchor);
  vnode.el = el;
}
Copy the code

Add anchor properties to all related methods: processComponent, processText, Patch, processElement, mountChildren, patchChildren, patchArrayChildren

Overwrite the unmountFragment method

//render.js
function unmountFragment(vnode) {
  let { el: cur, anchor: end } = vnode;
  const { parentNode } = cur;
  // Facilitate nodes between cur and end and delete
  while(cur ! == end) {let next = cur.nextSibling;
    parentNode.removeChild(cur);
    cur = next;
  }
  // Finally delete the end node
  parentNode.removeChild(end);
}
Copy the code

One more thing to pay attention to

Rewrite the patch

//render.js
function patch(n1, n2, container, anchor) {
  if(n1 && ! isSameVNode(n1, n2)) {// If it is the fragment Anchor set as n1, the next DOM node of the anchor; otherwise, the next node anchor set as N1 EL is inserted before this element
    anchor = (n1.anchor || n1.el).nextSibling;
    unmount(n1);
    n1 = null;
  }
  const { shapeFlag } = n2;
  if (shapeFlag & ShapeFlags.COMPONENT) {
    processComponent(n1, n2, container, anchor);
  } else if (shapeFlag & ShapeFlags.TEXT) {
    processText(n1, n2, container, anchor);
  } else if (shapeFlag & ShapeFlags.FRAGMENT) {
    processFragment(n1, n2, container, anchor);
  } else{ processElement(n1, n2, container, anchor); }}Copy the code

OK now tests the previous code for success