👶 simple-virtual-dom

simple-virtual-dom is a library which, a bit like đź’Ż hundred VDom, is intended to serve as a learning resource to understand how the virtual DOM works. It is, as far as I can tell, implemented in a pretty similar way to other virtual DOM examples which are out there.

Usage example

The package exports three functions which work together to implement a virtual DOM: el, patch, and diff. el has a call signature which is compatible with the standard inferface for 👩‍🔬 JSX (so it can be used to create a tree of virtual DOM objects), which can be rendered to the DOM by calling a .render() method. diff takes two virtual DOM trees (old and new) and returns a set of patches which can be applied to the former to turn it into the latter, and patch actually applies those patches.

Using this looks like:

var svd = require('simple-virtual-dom')

var el = svd.el
var diff = svd.diff
var patch = svd.patch

// 1. use `el(tagName, [propeties], children)` to create a virtual dom tree
var tree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: blue'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li')])
])

// 2. generate a real dom from virtual dom. `root` is a `div` element
var root = tree.render()

// 3. generate another different virtual dom tree
var newTree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: red'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li'), el('li')])
])

// 4. diff two virtual dom trees and get patches
var patches = diff(tree, newTree)

// 5. apply patches to real dom
patch(root, patches)

// now the `root` dom is updated

A tour of the code

Let’s start with the el function for creating new elements and look at how that is rendered to the DOM, and then move on to diffing and patching.

el / Element

The el function which is exported is an alias for the Element object constructor, defined here:

lib/element.js:L3-L17

/**
 * Virtual-dom Element.
 * @param {String} tagName
 * @param {Object} props - Element's properties,
 *                       - using object to store key-value pair
 * @param {Array<Element|String>} - This element's children elements.
 *                                - Can be Element instance or just a piece plain text.
 */
function Element (tagName, props, children) {
  if (!(this instanceof Element)) {
    if (!_.isArray(children) && children != null) {
      children = _.slice(arguments, 2).filter(_.truthy)
    }
    return new Element(tagName, props, children)
  }

This function can be called either with new or without, and it works fairly similarly to the equivalent function in đź’Ż hundred VDom, except that it is an object constructor rather than a plain function which returns an object created with an object literal.

But anyhow, it does things you’d expect, like setting some values on this based on the args (note the fun easter egg here):

lib/element.js:L24-L29

  this.tagName = tagName
  this.props = props || {}
  this.children = children || []
  this.key = props
    ? props.key
    : void 666

Then it deals with children:

lib/element.js:L31-L43

  var count = 0

  _.each(this.children, function (child, i) {
    if (child instanceof Element) {
      count += child.count
    } else {
      children[i] = '' + child
    }
    count++
  })

  this.count = count
}

If the children are instances of Element (aka virtual DOM nodes in this context - probably would have been better to call this function VElement or something like that) then it adds the child’s count property to its own count property, and otherwise it coerces the value to a string, and in either case it increments count. Since this is basically happening recursivey up and down the tree the root node will have a .count property reflecting the total number of child nodes.

Then the final thing is a render method which creates a new HTML element, renders all the children into it, and then returns that element so the caller can attach it to the document body.

lib/element.js:L45-L65

/**
 * Render the hold element tree.
 */
Element.prototype.render = function () {
  var el = document.createElement(this.tagName)
  var props = this.props

  for (var propName in props) {
    var propValue = props[propName]
    _.setAttr(el, propName, propValue)
  }

  _.each(this.children, function (child) {
    var childEl = (child instanceof Element)
      ? child.render()
      : document.createTextNode(child)
    el.appendChild(childEl)
  })

  return el
}

One more interesting bit here is the call to _.setAttr which is a little utility func defined nearby which does a bit more than Element.setAttribute:

lib/util.js:L43-L64

_.setAttr = function setAttr (node, key, value) {
  switch (key) {
    case 'style':
      node.style.cssText = value
      break
    case 'value':
      var tagName = node.tagName || ''
      tagName = tagName.toLowerCase()
      if (
        tagName === 'input' || tagName === 'textarea'
      ) {
        node.value = value
      } else {
        // if it is not a input or textarea, use `setAttribute` to set
        node.setAttribute(key, value)
      }
      break
    default:
      node.setAttribute(key, value)
      break
  }
}

Ok so basically the way this works is you call Element with normal 👩‍🔬 JSX factory function arguments and you get back a tree of these Element objects. Then on the root one you can call .render to render it to DOM nodes, which you can then attach to the document.

diff

This function does a tree traversal, comparing the old and new virtual DOM trees and, when they differ, produces patch objects which will be interpreted by the patch function to bring the state of the DOM in-line with the new virtual DOM tree.

I believe that the types for the patch objects could be represented in TypeScript like this:

const REPLACE = 0
const  REORDER = 1
const PROPS = 2
const TEXT = 3

type ReplacePatch = {
  type: REPLACE,
  node: Element
}

type ReorderPatch {
  type: REORDER,
  moves: Move[]
}

type Move {
  index: number,
  // type 0 is removing, type 1 is inserting
  type: 0 | 1,
  item?: Element
}

type PropsPatch {
  type: PROPS,
  props: Record<string, any>
}

type TextPatch {
  type: TEXT,
  content: string
}

type Patch =
  | ReplacePatch
  | ReorderPatch
  | PropsPatch
  | TextPatch

The entry point for producing these is this function:

lib/diff.js:L5-L10

function diff (oldTree, newTree) {
  var index = 0
  var patches = {}
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

The dfsWalk function is where most of the work happens. You can read through it to get an idea:

lib/diff.js:L12-L51

function dfsWalk (oldNode, newNode, index, patches) {
  var currentPatch = []

  // Node is removed.
  if (newNode === null) {
    // Real DOM node will be removed when perform reordering, so has no needs to do anything in here
  // TextNode content replacing
  } else if (_.isString(oldNode) && _.isString(newNode)) {
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  // Nodes are the same, diff old node's props and children
  } else if (
      oldNode.tagName === newNode.tagName &&
      oldNode.key === newNode.key
    ) {
    // Diff props
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // Diff children. If the node has a `ignore` property, do not diff children
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  // Nodes are not the same, replace the old node with new node
  } else {
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
}

One interesting bit to pay attention to here is the index argument. Initially when dfsWalk is called from diff this is set to 0:

lib/diff.js:L6

  var index = 0

In dfsWalk a series of patches are assembled in currentPatch and then that array of patches is stuffed into the patches record (which basically has type Record<number, Patch[]>) under the index passed in.

The index is also passed down to diffChildren, the function responsible for diffing the children of an element and for recursively calling dfsWalk with those children:

lib/diff.js:L62-L71

  var leftNode = null
  var currentNodeIndex = index
  _.each(oldChildren, function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count)
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches)
    leftNode = child
  })

Basically what’s going on here is that as we do a depth-first traversal left-to-right traversal of the tree we end up incrementing this index argument for each component we encounter. The patches for a given component are then stored in patches under the index that corresponds to their order in that left-to-right, depth-first traversal. This means that later on when we’re patching we know which elements our patches correspond to without having to keep a reference around to the elements themselves.

patch

Patch is sort of the final piece of the puzzle. Here’s the entry function:

lib/patch.js:L8-L11

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

Note that node here is an HTMLElement, returned by Element.render().

Here we follow a similar strategy as we did in the diff function with incrementing the index, but here instead we store a variable in an object which we can mutate as we traverse the tree.

This calls out to a walker function dfsWalk which basically just traverses the tree and calls an applyPatches function:

lib/patch.js:L13-L28

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) {
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches)
  }
}

Note that we get currentPatches, i.e. the patches which apply to the current node, by grabbing patches[walker.index].

The applyPatches function then basically switches on the patch type and calls out to some functions which do the work:

lib/patch.js:L30-L57

function applyPatches (node, currentPatches) {
  _.each(currentPatches, function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string')
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        if (node.textContent) {
          node.textContent = currentPatch.content
        } else {
          // fuck ie
          node.nodeValue = currentPatch.content
        }
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

You can read the file for the rest - that’s basically how the whole thing works!