Vue source code: virtual DOM and diff algorithm

Briefly introduce the virtual DOM and diff algorithm

demand

Method 1: dismantle and rebuild

Method 2: diff

primary coverage

snabbdom introduction and test environment construction

Introduction to snabbdom

  • snabbdom is a Swedish word, which originally means "speed";

  • Snabbdom is a famous virtual DOM library and the ancestor of diff algorithm. Vue source code draws on snabbdom;

  • Official git: snabbdom

Installing snabbdom

  • The snabbdom source code on Git is written in TypeScript, and git does not provide a compiled JavaScript version;

  • If you want to directly use the built JavaScript version of the snabbdom library, you can download it from npm:

    npm i -S snabbdom
    
  • When learning the bottom layer of the library, it is recommended that you read the original TS code, preferably with the original comments of the library author, which will greatly improve your reading ability of the source code.

Construction of snabbdom test environment

  • The snabbdom library is a DOM library and cannot run in the nodejs environment. Therefore, it is necessary to build a webpack and webpack dev server development environment without installing any loader s

  • Note here that the latest version must be installed webpack@5 , cannot be installed webpack@4 , because there is no ability to read exports in webpack4

    npm i -S webpack@5 webpack-cli@3 webpack-dev-server@3
    
  • Refer to the official website of webpack and write webpack config. JS file

Run through the demo program on the official git home page

  • Run through s nabbdom official git home page demo program, which proves that the debugging environment has been successfully built

  • Don't forget in index Place a div#container in HTML

Virtual DOM and h functions

Virtual DOM is similar to token in mustache

diff occurs on the virtual DOM

The course does not look at how DOM becomes a virtual dom

research contents

  • How are virtual functions generated by rendering functions (h functions)?

    Handwritten h function

  • Principle of diff algorithm?

    Handwritten diff algorithm

  • How does a virtual DOM become a real DOM through diff

    In fact, changing the virtual DOM back to the real DOM is covered by the diff algorithm

How are virtual functions generated by rendering functions (h functions)?

The h function is used to generate virtual nodes

The h function is used to generate a virtual node (vnode)

For example, this will call the h function

You will get such a virtual node

{
    "sel": "a",
    "data": {
        props: {
            href: "http://www.atguigu.com"
        }
    },
    "text": "Shang Silicon Valley"
}

It represents the real DOM node

What attributes does a virtual node have

{
    children: undefined,	// Child element
    data: {},	// Attributes, styles
    elm: undefined,	// It corresponds to the real DOM node. If it is undefined, it means that the node has not been in the tree
    key: undefined,	// Node unique ID
    sel: "div",	// selector
    text: "I am a box"	// written words
}

The h function can be nested to obtain a virtual DOM tree( 🧨)

For example, use the h function nested like this

You will get such a virtual DOM tree

The usage of h function is very flexible

for example

Handwritten h function

vnode

vnode.js

/**
 * vnode The function of the function is very simple, that is, it returns the incoming five parameter combination object
 */
export default function (sel, data, children, text, elm) {
  return {
    sel, data, children, text, elm
  };
}

h function

h.js

import vnode from "./vnode";

/*
* Write a low configuration version of h function. This function must receive three parameters, which are indispensable - the overload function is weak
* That is to say, the calling mode must be one of the following three:
* Form â‘  h('div ', {},' text ')
* Form â‘¡ h('div ', {}, [])
* Configuration â‘¢ h('div', {}, h())
* */
export default function (sel, data, c) {
  // Check the number of parameters
  if (arguments.length !== 3)
    throw new Error('I'm sorry,h Function must pass in 3 parameters,We are a low configuration version h function');
  // Check the type of parameter c
  if (typeof c === 'string' || typeof c === 'number') {
    return vnode(sel, data, undefined, c, undefined);
  } else if (Array.isArray(c)) {
    // It shows that calling h function is form â‘¡
    let children = [];
    // Traversal c, mobile children
    for (let i = 0; i < c.length; i++) {
      // Check c[i] must be an object
      if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))
        throw new Error('An item in the passed in array parameter is not h function');
      // There is no need to execute c[i], because it has been executed in the test statement
      // Just collect the children
      children.push(c[i]);
    }
    // When the cycle is over, it means that children have been collected. At this time, you can return to the virtual node. There are children nodes
    return vnode(sel, data, children, undefined, undefined);
  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
    // It shows that calling h function is form â‘¢
    // That is, the incoming c is the only children You do not need to execute c because c is already executed in the test statement
    return vnode(sel, data, [c], undefined, undefined);
  } else {
    throw new Error('The parameter type passed in is incorrect');
  }
};

Look at the TS code and write the JS code

  • Look at the TS version of the source code, and then copy the JS code
  • As long as the trunk function, give up the implementation of some details

Perceptual diff algorithm

By changing the content of the li tag, we know that the diff algorithm is updated with the minimum amount

The key can uniquely identify the node and serve the minimum amount of updates

import {init} from 'snabbdom/init';
import {classModule} from "snabbdom/modules/class";
import {propsModule} from "snabbdom/modules/props";
import {styleModule} from "snabbdom/modules/style";
import {eventListenersModule} from "snabbdom/modules/eventlisteners";
import {h} from 'snabbdom/h';

// Create a patch function
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);

// Get the box and button
const container = document.getElementById('container');
const btn = document.getElementById('btn');

// Create virtual node
const vnode1 = h('ul', {}, [
  h('li', {key: 'A'}, 'A'),
  h('li', {key: 'B'}, 'B'),
  h('li', {key: 'C'}, 'C'),
  h('li', {key: 'D'}, 'D')
])


patch(container, vnode1)

const vnode2 = h('ul', {}, [
  h('li', {key: 'E'}, 'E'),
  h('li', {key: 'A'}, 'A'),
  h('li', {key: 'B'}, 'B'),
  h('li', {key: 'C'}, 'C'),
  h('li', {key: 'D'}, 'D'),
])

// Click the button to change vnode1 to vnode2
btn.onclick = () => {
  patch(vnode1, vnode2)
}

Experience

  • The minimum update is very powerful! It's really the least amount of updates. Of course, key is very important.

    key is the unique identifier of this node and tells the diff algorithm that they are the same DOM node before and after the change.

  • Only the same virtual node can be used for fine comparison., Otherwise, it is violent to delete the old and insert the new.

    Extension problem: how to define the same virtual node? Answer: the selectors are the same and the key s are the same.

  • Only the same layer comparison is performed, and cross layer comparison is not performed.. Even if it's the same virtual node, it's cross layer. Sorry, refinement doesn't diff you. Instead, it violently deletes the old one and then inserts the new one.

diff is not so meticulous! Does it really affect efficiency??
A: the above 2 and 3 operations will not be encountered in the actual Vue development, so this is a reasonable optimization mechanism.

Schematic diagram of comparison on the same floor

// Create virtual node
const vnode1 = h('ul', {}, [
  h('li', {key: 'A'}, 'A'),
  h('li', {key: 'B'}, 'B'),
  h('li', {key: 'C'}, 'C'),
  h('li', {key: 'D'}, 'D')
])


patch(container, vnode1)

const vnode2 = h('ul', {}, h('section', {}, [
  h('li', {key: 'E'}, 'E'),
  h('li', {key: 'A'}, 'A'),
  h('li', {key: 'B'}, 'B'),
  h('li', {key: 'C'}, 'C'),
  h('li', {key: 'D'}, 'D'),
]))

As in the above code, adding a layer of nodes will no longer update the minimum amount, but reconstruct it.

diff algorithm deals with the problem that the old and new nodes are not the same node

How to define "same node"

The key of the old node should be the same as that of the new node

And

The selector of the old node should be the same as that of the new node

When creating nodes, all child nodes need to be created recursively

When I first went up the tree

patch.js

import vnode from "./vnode";
import createElement from "./createElement";

export default function (oldVnode, newVnode) {
  // Determine whether the first parameter passed in is a DOM node or a virtual node
  if (oldVnode.sel === '' || oldVnode.sel === undefined) {
    // The first parameter passed in is the DOM node, which should be wrapped as a virtual node
    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
  }
  // Judge whether oldVnode and newVnode are the same node
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    // Is the same node
    // TODO: fine comparison
  } else {
    // Not the same node
    createElement(newVnode, oldVnode.elm);
  }
};

createElement.js

// Really create nodes. Create vnode as DOM and insert it before the pivot element
export default function (vnode, pivot) {
  // The purpose is to insert the virtual node vnode before the benchmark pivot
  // Create a DOM node, which is still an orphan node
  let domNode = document.createElement(vnode.sel);
  // Child nodes or text
  if (vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)) {
    // Inside is text
    domNode.innerText = vnode.text;
    // Put the orphan node on the tree. Let the parent element of the benchmark node call the insertBefore method to insert the new orphan node before the label node
    pivot.parentNode.insertBefore(domNode, pivot);
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {

  }
};

Create child nodes recursively

To accommodate recursive operations, insert operations into patch JS instead of createElement

Here, diff is used to deal with the situation that the old and new nodes are not the same node. Create a new insert and delete it violently

index.js

import h from './mySnabbdom/h';
import patch from './mySnabbdom/patch'

const container = document.getElementById('container');
const btn = document.getElementById('btn');

const myVnode1 = h('h1', {}, 'Hello');

const myVnode2 = h('ul', {}, [
  h('li', {}, 'A'),
  h('li', {}, 'B'),
  h('li', {}, [
    h('div', {}, [
      h('ol', {}, [
        h('li', {}, 'Ha ha ha'),
        h('li', {}, 'Hey, hey, hey'),
        h('li', {}, 'Interesting'),
      ])
    ])
  ]),
  h('li', {}, 'D'),
])

const myVnode3 = h('section', {}, [
  h('h1', {}, 'I'm new h1'),
  h('h2', {}, 'I'm new h2'),
  h('h3', {}, 'I'm new h3'),
])

patch(container, myVnode2);

btn.onclick = function () {
  patch(myVnode2, myVnode3);
}

createElement.js

// Really create nodes. Create vnode as DOM, which is an orphan node and will not be inserted
export default function createElement(vnode) {
  // console.log(` the purpose is to turn the virtual node ${vnode} into a real DOM `)
  // Create a DOM node, which is still an orphan node
  let domNode = document.createElement(vnode.sel);
  // Child nodes or text
  if (vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)) {
    // Inside is text
    domNode.innerText = vnode.text;
    // Supplementary elm attribute
    vnode.elm = domNode;
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    // If it is not a child node, you need to create a node recursively
    for (let i = 0; i < vnode.children.length; i++) {
      // Get the current children
      let ch = vnode.children[i];
      // Create its dom. Once createElement is called, it means that the DOM has been created, and its elm attribute points to the created DOM, but it has not been up the tree. It is an orphan node
      let chDom = createElement(ch);
      // Go up the tree
      domNode.appendChild(chDom);
    }
  }
  // Supplementary elm attribute
  vnode.elm = domNode;
  // Returns elm, which is a pure DOM object
  return vnode.elm;
};

patch.js

import vnode from "./vnode";
import createElement from "./createElement";

export default function (oldVnode, newVnode) {
  // Determine whether the first parameter passed in is a DOM node or a virtual node
  if (oldVnode.sel === '' || oldVnode.sel === undefined) {
    // The first parameter passed in is the DOM node, which should be wrapped as a virtual node
    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
  }
  // Judge whether oldVnode and newVnode are the same node
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    // Is the same node
    // TODO: fine comparison
  } else {
    // Not the same node
    let newVnodeElm = createElement(newVnode);
    // Before inserting into the old node
    if (oldVnode.elm.parentNode && newVnodeElm)
      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
    // Delete old node
    oldVnode.elm.parentNode.removeChild(oldVnode.elm);
  }
};

diff handles when the old and new nodes are the same node

Different situations of handwritten old and new text nodes

patch.js

import vnode from "./vnode";
import createElement from "./createElement";

export default function (oldVnode, newVnode) {
  // Determine whether the first parameter passed in is a DOM node or a virtual node
  if (oldVnode.sel === '' || oldVnode.sel === undefined) {
    // The first parameter passed in is the DOM node, which should be wrapped as a virtual node
    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
  }
  // Judge whether oldVnode and newVnode are the same node
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    console.log('Is the same node')
    // Judge whether the old and new node s are the same object
    if (oldVnode === newVnode) return;
    // Determine whether newVnode has text attribute
    if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
      // The new vnode has a text attribute
      // console.log('New vnode has text attribute ')
      if (newVnode.text !== oldVnode.text)
        // If the text in the new virtual node is different from the text in the old virtual node, you can directly write the new text into the old elm. If the old elm is children, it will disappear immediately
        oldVnode.elm.innerText = newVnode.text;
    } else {
      // The new vnode has no text attribute and children
      // console.log('New vnode has no text attribute ')
      // Judge whether the old have children
      if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
        // Old people have children. This is the most complicated situation. There are children both old and new
      } else {
        // The old has no children, the new has children
        // Empty the contents of the old node
        oldVnode.elm.innerHTML = '';
        // Traverse the child nodes of newVnode, create dom, and loop up the tree
        newVnode.children.forEach(node => {
          let dom = createElement(node);
          oldVnode.elm.appendChild(dom);
        })
      }
    }
  } else {
    // Not the same node
    let newVnodeElm = createElement(newVnode);
    // Before inserting into the old node
    if (oldVnode.elm.parentNode && newVnodeElm)
      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
    // Delete old node
    oldVnode.elm.parentNode.removeChild(oldVnode.elm);
  }
};

Add key to vnode

If there is a key in the data, bind the key to vnode

vnode.js

/**
 * vnode The function of the function is very simple, that is, it returns the incoming five parameter combination object
 */
export default function (sel, data, children, text, elm) {
  const key = data.key;
  return {
    sel, data, children, text, elm, key
  };
}

Attempt to write diff to update child nodes

patchVnode.js

import createElement from "./createElement";

export default function patchVnode(oldVnode, newVnode) {
// Judge whether the old and new node s are the same object
  if (oldVnode === newVnode) return;
  // Judge whether newVnode has text attribute
  if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
    // The new vnode has a text attribute
    // console.log('New vnode has text attribute ')
    if (newVnode.text !== oldVnode.text)
      // If the text in the new virtual node is different from the text in the old virtual node, you can directly write the new text into the old elm. If the old elm is children, it will disappear immediately
      oldVnode.elm.innerText = newVnode.text;
  } else {
    // The new vnode has no text attribute and children
    // console.log('New vnode has no text attribute ')
    // Judge whether the old have children
    if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
      // There are old children. This is the most complicated situation. There are children both old and new
      // The beginning of all unprocessed nodes
      let un = 0;
      for (let i = 0; i < newVnode.children.length; i++) {
        let ch = newVnode.children[i];
        // Traverse again to see if there is a node in oldVnode and it is the same
        let isExist = false;
        for (let j = 0; j < oldVnode.children.length; j++) {
          if (oldVnode.children[j].sel === ch.sel && oldVnode.children[j].key === ch.key) {
            isExist = true;
          }
        }
        if (!isExist) {
          let dom = createElement(ch);
          ch.elm = dom;
          if (un < oldVnode.children.length)
            oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm);
          else
            oldVnode.elm.appendChild(dom);
        } else {
          // Move the processed node down one bit
          un++;
          // Judge whether the positions of front and rear nodes are consistent
        }
      }
    } else {
      // The old has no children, the new has children
      // Empty the contents of the old node
      oldVnode.elm.innerHTML = '';
      // Traverse the child nodes of newVnode, create dom, and loop up the tree
      newVnode.children.forEach(node => {
        let dom = createElement(node);
        oldVnode.elm.appendChild(dom);
      })
    }
  }
}

For example, in the following three cases, all cases are very complex and mixed together, so an excellent update algorithm is needed

New information

Newly created nodes are inserted before all unprocessed nodes, not after all processed nodes.

For example, on the way, if MN is inserted and all processed nodes are AB, MN will be inserted into B in turn, which is inconsistent with the purpose.

Deletion

Update status

Sub node update strategy of diff algorithm

Four hit lookups:

Before new: all unprocessed nodes in the new virtual node

After new: all the last unprocessed nodes in the new virtual node

Old: all unprocessed nodes in the old virtual nodes

After old: all the last unprocessed nodes in the old virtual nodes

Optimization strategy of classical diff algorithm

  • New front and old front: if the old node completes the cycle first, it indicates that there are nodes to be inserted in the new node.
  • New after and old after: if the new node completes the cycle first, if there are still remaining nodes in the old node (the node between the old and new pointers), it indicates that they are the nodes to be deleted.
  • New and old: (if this happens and involves a mobile node, the node pointed to by the new front will be after the old one)
  • New front and old rear: (if this happens and involves a mobile node, the node pointed to by the new front is before the old front of the mobile)

If you hit one, you will no longer judge the hit

If you don't hit, you need to find it with a loop. Move to before oldStartIdx.

New information

Deletion

Multiple deletion

Complex situation

Handwriting child node update strategy

updateChildren.js

import patchVnode from "./patchVnode";
import createElement from "./createElement";

// Determine whether it is the same virtual node
function checkSameVnode(vnodeA, vnodeB) {
  return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key;
}

export default function updateChildren(parentElm, oldCh, newCh) {
  // console.log('I'm updateChildren ')
  // console.log(oldCh, newCh)
  // Define old, new, old and new numbers
  let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1;
  // Define old, old, new and new nodes
  let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx],
    newEndVnode = newCh[newEndIdx];
  let keyMap = null;
  // Start big while
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // First, it is not to judge â‘  â‘¡ â‘¢ â‘£ hit, but to skip what has been marked undefined
    if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newStartVnode == null || newCh[newStartIdx] === undefined) {
      newStartVnode = newCh[++newStartIdx];
    } else if (newEndVnode == null || newCh[newEndIdx] === undefined) {
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(newStartVnode, oldStartVnode)) {
      // New and old
      console.log("â‘ New and old hits")
      patchVnode(oldStartVnode, newStartVnode);
      newStartVnode = newCh[++newStartIdx];
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (checkSameVnode(newEndVnode, oldEndVnode)) {
      // New and old
      console.log("â‘¡New and old hits")
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(newEndVnode, oldStartVnode)) {
      // New and old
      console.log("â‘¢New and old hits")
      patchVnode(oldStartVnode, newEndVnode);
      // When â‘¢ new and old hits, move the node at this time. Move the node pointed to before the new node to the back of the old node
      // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      newEndVnode = newCh[--newEndIdx];
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (checkSameVnode(newStartVnode, oldEndVnode)) {
      // New front and old back
      console.log("â‘£New front and old rear hits")
      patchVnode(oldEndVnode, newStartVnode);
      // â‘£ when the new front and old rear hit, move the node at this time. Move the node pointed by the new node to the front of the old node
      // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      newStartVnode = newCh[++newStartIdx];
      oldEndVnode = oldCh[--oldEndIdx];
    } else {
      // None of the four hits hit
      // Make the map mapping object of the key so that you don't have to traverse the old object every time.
      if (!keyMap) {
        keyMap = {};
        // From oldStartIdx to oldEndIdx, create a KeyMap mapping object
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
          const key = oldCh[i].key;
          if (key !== undefined)
            keyMap[key] = i;
        }
      }
      //console.log(keyMap);
      // Find the position sequence number of the mapping of the current item (nextStartIdx) in the keyMap
      const idxInOld = keyMap[newStartVnode.key];
      // console.log(idxInOld);
      if (idxInOld === undefined) {
        // Judge that if idxInOlx is undefined, it means it is a new item and only needs to be inserted
        // The added item (newStartVnode) is not a real DOM node at present
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
      } else {
        // If it is not undefined, it needs to be moved
        const elmToMove = oldCh[idxInOld];
        patchVnode(elmToMove, newStartVnode);
        // Setting this item to undefined indicates that this item has been processed
        oldCh[idxInOld] = undefined;
        // To move, you can also call insertBefore to move
        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
      }
      // Move the pointer down to move only the new head
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // After the while is finished, you need to continue to see if there are any remaining nodes to judge whether you need to delete or add nodes
  if (newStartIdx <= newEndIdx) {
    // console.log('new, there are still remaining nodes that have not been processed, and items should be added. To insert all remaining nodes before oldStart ');
    // After the loop ends, start is still smaller than old
    // before is the inserted benchmark
    // const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    // Traverse the new newCh and add it to the old one without processing
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      // The insertBefore method can automatically identify null. If it is null, it will automatically queue at the end of the queue. It's consistent with appendChild.
      // newCh[i] there is no real DOM yet, so you need to call the createElement() function to change it into dom
      // parentElm.insertBefore(createElement(newCh[i]), before);
      parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
    }
  } else if (oldStartIdx <= oldEndIdx) {
    // console.log('old has remaining nodes that have not been processed, items to be deleted ')
    // Batch delete items between oldStart and oldEnd pointers
    for (let i = oldStartIdx; i <= oldEndIdx; i++)
      if (oldCh[i]) parentElm.removeChild(oldCh[i].elm);
  }
};

Full handwritten

patch.js

import vnode from "./vnode";
import createElement from "./createElement";
import patchVnode from "./patchVnode";

export default function patch(oldVnode, newVnode) {
  // Determine whether the first parameter passed in is a DOM node or a virtual node
  if (oldVnode.sel === '' || oldVnode.sel === undefined) {
    // The first parameter passed in is the DOM node, which should be wrapped as a virtual node
    oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
  }
  // Judge whether oldVnode and newVnode are the same node
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    // console.log('Is the same node ')
    patchVnode(oldVnode, newVnode);
  } else {
    // Not the same node
    let newVnodeElm = createElement(newVnode);
    // Before inserting into the old node
    if (oldVnode.elm.parentNode && newVnodeElm)
      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
    // Delete old node
    oldVnode.elm.parentNode.removeChild(oldVnode.elm);
  }
};

vnode.js

/**
 * vnode The function of the function is very simple, that is, it returns the incoming five parameter combination object
 */
export default function (sel, data, children, text, elm) {
  const key = data.key;
  return {
    sel, data, children, text, elm, key
  };
}

createElement.js

// Really create nodes. Create vnode as DOM, which is an orphan node and will not be inserted
export default function createElement(vnode) {
  // console.log(` the purpose is to turn the virtual node ${vnode} into a real DOM `)
  // Create a DOM node, which is still an orphan node
  let domNode = document.createElement(vnode.sel);
  // Child nodes or text
  if (vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)) {
    // Inside is text
    domNode.innerText = vnode.text;
    // Supplementary elm attribute
    vnode.elm = domNode;
  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    // If it is not a child node, you need to create a node recursively
    for (let i = 0; i < vnode.children.length; i++) {
      // Get the current children
      let ch = vnode.children[i];
      // Create its dom. Once createElement is called, it means that the DOM has been created, and its elm attribute points to the created DOM, but it has not been up the tree. It is an orphan node
      let chDom = createElement(ch);
      // Go up the tree
      domNode.appendChild(chDom);
    }
  }
  // Supplementary elm attribute
  vnode.elm = domNode;
  // Returns elm, which is a pure DOM object
  return vnode.elm;
};

patchVnode.js

import createElement from "./createElement";
import updateChildren from "./updateChildren";

// Compare the same virtual node
export default function patchVnode(oldVnode, newVnode) {
// Judge whether the old and new node s are the same object
  if (oldVnode === newVnode) return;
  // Determine whether newVnode has text attribute
  if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
    // The new vnode has a text attribute
    // console.log('New vnode has text attribute ')
    if (newVnode.text !== oldVnode.text)
      // If the text in the new virtual node is different from the text in the old virtual node, you can directly write the new text into the old elm. If the old elm is children, it will disappear immediately
      oldVnode.elm.innerText = newVnode.text;
  } else {
    // The new vnode has no text attribute and children
    // console.log('New vnode has no text attribute ')
    // Judge whether the old have children
    if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
      // There are children in the old and children in the new. This is the most complicated situation. There are children both old and new
      updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
    } else {
      // The old has no children, the new has children
      // Empty the contents of the old node
      oldVnode.elm.innerHTML = '';
      // Traverse the child nodes of newVnode, create dom, and loop up the tree
      newVnode.children.forEach(node => {
        let dom = createElement(node);
        oldVnode.elm.appendChild(dom);
      })
    }
  }
}

h.js

import vnode from "./vnode";

/*
* Write a low configuration version of h function. This function must receive three parameters, which are indispensable - the overload function is weak
* That is to say, the calling mode must be one of the following three:
* Form â‘  h('div ', {},' text ')
* Form â‘¡ h('div ', {}, [])
* Configuration â‘¢ h('div', {}, h())
* */
export default function (sel, data, c) {
  // Check the number of parameters
  if (arguments.length !== 3)
    throw new Error('I'm sorry,h Function must pass in 3 parameters,We are a low configuration version h function');
  // Check the type of parameter c
  if (typeof c === 'string' || typeof c === 'number') {
    return vnode(sel, data, undefined, c, undefined);
  } else if (Array.isArray(c)) {
    // It shows that calling h function is form â‘¡
    let children = [];
    // Traversal c, mobile children
    for (let i = 0; i < c.length; i++) {
      // Check c[i] must be an object
      if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))
        throw new Error('An item in the passed in array parameter is not h function');
      // There is no need to execute c[i], because it has been executed in the test statement
      // Just collect the children
      children.push(c[i]);
    }
    // When the cycle is over, it means that children have been collected. At this time, you can return to the virtual node. There are children nodes
    return vnode(sel, data, children, undefined, undefined);
  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
    // It shows that calling h function is form â‘¢
    // That is, the incoming c is the only children You do not need to execute c because c is already executed in the test statement
    return vnode(sel, data, [c], undefined, undefined);
  } else {
    throw new Error('The parameter type passed in is incorrect');
  }
};

updateChildren.js

import patchVnode from "./patchVnode";
import createElement from "./createElement";

// Determine whether it is the same virtual node
function checkSameVnode(vnodeA, vnodeB) {
  return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key;
}

export default function updateChildren(parentElm, oldCh, newCh) {
  // console.log('I'm updateChildren ')
  // console.log(oldCh, newCh)
  // Define old, new, old and new numbers
  let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1;
  // Define old, old, new and new nodes
  let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx],
    newEndVnode = newCh[newEndIdx];
  let keyMap = null;
  // Start big while
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // First, it is not to judge â‘  â‘¡ â‘¢ â‘£ hit, but to skip what has been marked undefined
    if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newStartVnode == null || newCh[newStartIdx] === undefined) {
      newStartVnode = newCh[++newStartIdx];
    } else if (newEndVnode == null || newCh[newEndIdx] === undefined) {
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(newStartVnode, oldStartVnode)) {
      // New and old
      console.log("â‘ New and old hits")
      patchVnode(oldStartVnode, newStartVnode);
      newStartVnode = newCh[++newStartIdx];
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (checkSameVnode(newEndVnode, oldEndVnode)) {
      // New and old
      console.log("â‘¡New and old hits")
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (checkSameVnode(newEndVnode, oldStartVnode)) {
      // New and old
      console.log("â‘¢New and old hits")
      patchVnode(oldStartVnode, newEndVnode);
      // When â‘¢ new and old hits, move the node at this time. Move the node pointed to before the new node to the back of the old node
      // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      newEndVnode = newCh[--newEndIdx];
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (checkSameVnode(newStartVnode, oldEndVnode)) {
      // New front and old back
      console.log("â‘£New front and old rear hits")
      patchVnode(oldEndVnode, newStartVnode);
      // â‘£ when the new front and old rear hit, move the node at this time. Move the node pointed by the new node to the front of the old node
      // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      newStartVnode = newCh[++newStartIdx];
      oldEndVnode = oldCh[--oldEndIdx];
    } else {
      // None of the four hits hit
      // Make the map mapping object of the key so that you don't have to traverse the old object every time.
      if (!keyMap) {
        keyMap = {};
        // From oldStartIdx to oldEndIdx, create a KeyMap mapping object
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
          const key = oldCh[i].key;
          if (key !== undefined)
            keyMap[key] = i;
        }
      }
      //console.log(keyMap);
      // Find the position sequence number of the mapping of the current item (nextStartIdx) in the keyMap
      const idxInOld = keyMap[newStartVnode.key];
      // console.log(idxInOld);
      if (idxInOld === undefined) {
        // Judge that if idxInOlx is undefined, it means it is a new item and only needs to be inserted
        // The added item (newStartVnode) is not a real DOM node at present
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
      } else {
        // If it is not undefined, it needs to be moved
        const elmToMove = oldCh[idxInOld];
        patchVnode(elmToMove, newStartVnode);
        // Setting this item to undefined indicates that this item has been processed
        oldCh[idxInOld] = undefined;
        // To move, you can also call insertBefore to move
        parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
      }
      // Move the pointer down to move only the new head
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // After the while is finished, you need to continue to see if there are any remaining nodes to judge whether you need to delete or add nodes
  if (newStartIdx <= newEndIdx) {
    // console.log('new, there are still remaining nodes that have not been processed, and items should be added. To insert all remaining nodes before oldStart ');
    // After the loop ends, start is still smaller than old
    // before is the inserted benchmark
    // const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    // Traverse the new newCh and add it to the old one without processing
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      // The insertBefore method can automatically identify null. If it is null, it will automatically queue at the end of the queue. It's consistent with appendChild.
      // newCh[i] there is no real DOM yet, so you need to call the createElement() function to change it into dom
      // parentElm.insertBefore(createElement(newCh[i]), before);
      parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
    }
  } else if (oldStartIdx <= oldEndIdx) {
    // console.log('old has remaining nodes that have not been processed, items to be deleted ')
    // Batch delete items between oldStart and oldEnd pointers
    for (let i = oldStartIdx; i <= oldEndIdx; i++)
      if (oldCh[i]) parentElm.removeChild(oldCh[i].elm);
  }
};

Keywords: Vue

Added by crinkle on Sun, 23 Jan 2022 20:41:03 +0200