In recent years, with the rise of front-end frameworks such as React and Vue, the concept of Virtual DOM has become more and more popular and has been used in more and more frameworks and libraries. The Virtual DOM is a layer of abstraction based on the real DOM, which is represented by simple JS objects. The Snabbdom introduced in this paper is a simple implementation of Virtual DOM, and the Virtual DOM of Vue also refers to the implementation of Snabbdom.

For friends who want to learn Vue Virtual DOM in depth, it is recommended to learn Snabbdom first, which will be very helpful to understand Vue, and its core code is more than 200 lines.

This paper chooses the Snabbdom module system as the main core point for introduction. For other contents, you can refer to the official document Snabbdom.

What is Snabbdom

Snabbdom is a virtual DOM library focused on simplicity, modularity, powerful features, and performance. There are several core features:

  1. 200 lines of core code, and provide rich test cases;
  2. Have a strong module system, and support module expansion and flexible combination;
  3. On each VNode and global module, there are rich hooks that can be used in the Diff and Patch phases.

Let’s take a look at Snabbdom with a simple example.

1. Get started quickly

Install Snabbdom:

npm install snabbdom -D
Copy the code

Create a new index.html and set the entry element:

<div id="app"></div>
Copy the code

Then create a new demo1.js file and use the function provided by Snabbdom:

// demo1.js
import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

const patch = init([])
let vnode = h('div#app'.'Hello Leo')
const app = document.getElementById('app')
patch(app, vnode)
Copy the code

For a simple example, open index.html in a browser and the page will display “Hello Leo” text.

Next, I will take the SNabbDOM-Demo project as a learning example, from a simple example to the example used by the module system, in-depth study and analysis of snabbDOM source code, focusing on the analysis of the SNabbDOM module system.

2. Snabbdom-demo analysis

The Snabbdom- Demo project includes three demo codes that show us how to go from simple to deep Snabbdom. First clone the repository and install:

$ git clone https://github.com/zyycode/snabbdom-demo.git
$ npm install
Copy the code

Although there is no readme. md file for this project, the project directory is more intuitive and we can easily find the three sample code files in the SRC directory:

  • 01-basicusage.js
  • 02-basicusage.js
  • Modules. Js -> Modules

$ npm run dev
Copy the code

1. Simple example analysis

When we want to study a complex project such as a library or framework, we can analyze it through the simple example code provided by the official. Here we choose the simplest 01-BasicUsage.js code for analysis, and its code is as follows:

// src/01-basicusage.js

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

const patch = init([])

let vnode = h('div#container.cls'.'Hello World')
const app = document.getElementById('app') // Entry element

const oldVNode = patch(app, vnode)

// Assume time
vnode = h('div'.'Hello Snabbdom')
patch(oldVNode, vnode)
Copy the code

When you run the project, you see that the page displays the “Hello Snabbdom” text, and you wonder, where is the “Hello World” text?

The reason is simple: when we comment out the following two lines of code in the demo, the page displays the text “Hello World” :

vnode = h('div'.'Hello Snabbdom')
patch(oldVNode, vnode)
Copy the code

Here we can guess that the patch() function will render ** VNode** to the page. It can be further understood that the first patch() function executed here is the first rendering, and the second patch() function executed is the update operation.

2. To introduce VNode

What is the VNode in this example? Here’s a quick explanation:

VNode: This object describes node information. Its full name is virtual node. Another concept associated with “virtual node” is the “virtual DOM,” which is the name we use for the entire VNode tree built from the Vue component tree. The virtual DOM consists of vNodes. “Vue 3.0 Advanced VNode Exploration”

The Snabbdom defines the type of a VNode as follows:

export interface VNode {
  sel: string | undefined; // selector
  data: VNodeData | undefined; // The following contents of the VNodeData interface
  children: Array<VNode | string> | undefined; / / child nodes
  elm: Node | undefined; // The abbreviation for element, which stores the actual HTMLElement
  text: string | undefined; // If it is a text node, it stores text
  key: Key | undefined; // The key of the node, useful for making lists
}

export interfaceVNodeData { props? : Props attrs? : Attrsclass? :Classes
  style? :VNodeStyle
  dataset? :Dataset
  on? :On
  hero? :Hero
  attachData? :AttachData
  hook? :Hooks
  key? :Key
  ns? :string // for SVGs
  fn? : ()=> VNode // for thunksargs? :any[] // for thunks
  [key: string] :any // for any other 3rd party module
}
Copy the code

The VNode object contains sel field, data field and children field of the node.

In this demo, we don’t seem to see any code related to the module system, which is fine, because this is the simplest example and will be covered in more detail in the next section.

When we learn a function, we can focus on understanding the function’s “input parameter” and “output parameter”, and roughly judge the function’s role.

3 functions: init()/patch()/h() Snabbdom = Snabbdom = Snabbdom = Snabbdom = Snabbdom

3. Init () function analysis

The init() function is defined in the package/init.ts file:

// node_modules/snabbdom/src/package/init.ts

export function init (modules: Array<Partial<Module>>, domApi? : DOMAPI) {
	// omit other code
}
Copy the code

The parameter types are as follows:

function init(modules: Array<Partial<Module>>, domApi? : DOMAPI) : (oldVnode: VNode | Element, vnode: VNode) = >VNode

export type Module = Partial<{
  pre: PreHook
  create: CreateHook
  update: UpdateHook
  destroy: DestroyHook
  remove: RemoveHook
  post: PostHook
}>
  
export interface DOMAPI {
  createElement: (tagName: any) = > HTMLElement
  createElementNS: (namespaceURI: string, qualifiedName: string) = > Element
  createTextNode: (text: string) = > Text
  createComment: (text: string) = > Comment
  insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) = > void
  removeChild: (node: Node, child: Node) = > void
  appendChild: (node: Node, child: Node) = > void
  parentNode: (node: Node) = > Node | null
  nextSibling: (node: Node) = > Node | null
  tagName: (elm: Element) = > string
  setTextContent: (node: Node, text: string | null) = > void
  getTextContent: (node: Node) = > string | null
  isElement: (node: Node) = > node is Element
  isText: (node: Node) = > node is Text
  isComment: (node: Node) = > node is Comment
}
Copy the code

The init() function takes an array of modules and an optional domApi object as arguments and returns a function, the patch() function. The domApi object’s interface contains many methods for DOM manipulation. The modules parameter here is the focus of this article.

4. Analysis of patch() function

The init() function returns a patch() function of type:

// node_modules/snabbdom/src/package/init.ts

patch(oldVnode: VNode | Element, vnode: VNode) => VNode
Copy the code

The patch() function takes two VNode objects as arguments and returns a new VNode.

5. H () function analysis

The h() function is defined in the package/h.ts file:

// node_modules/snabbdom/src/package/h.ts

export function h(sel: string) :VNode
export function h(sel: string, data: VNodeData | null) :VNode
export function h(sel: string, children: VNodeChildren) :VNode
export function h(sel: string, data: VNodeData | null, children: VNodeChildren) :VNode
export function h (sel: any, b? :any, c? :any) :VNode{// omit other code}Copy the code

The h() function takes multiple arguments, including an sel argument that mounts the contents of the node to the container and returns a new VNode.

6. Summary

Through the previous introduction, we go back to the demo code, roughly call the flow is as follows:

Third, in-depth Snabbdom module system

After learning the above basic knowledge, we already know how to use Snabbdom, and know the input and output of the three core methods and their general functions. Next, we start to look at the core Snabbdom module system of this paper.

1. The Modules is introduced

Snabbdom module system is an expandable and flexible module system provided by Snabbdom, which is used to support various modules when Snabbdom operates VNode. If we need to process style, we will introduce the corresponding styleModule, which needs to process events. The introduction of eventListenersModule allows for a flexible combination that supports on-demand import.

The characteristics of Snabbdom module system can be summarized as follows: introduction on demand, independent management, single responsibility, convenient combination reuse, and strong maintainability.

Of course, the Snabbdom module system has other built-in modules:

The name of the module Module function The sample code
attributesModule Sets attributes for DOM elements to use when attributes are added and updatedsetAttributeMethods. h('a', { attrs: { href: '/foo' } }, 'Go to Foo')
classModule Used to dynamically set and switch class names on DOM elements. h('a', { class: { active: true, selected: false } }, 'Toggle')
datasetModule Set custom data attributes for DOM elements (data- *). And then you can useHTMLElement.datasetProperty to access them. h('button', { dataset: { action: 'reset' } }, 'Reset')
eventListenersModule Bind event listeners to DOM elements. h('div', { on: { click: clickHandler } })
propsModule Sets attributes for DOM elements, which are overridden by attributesModule if you also use it. h('a', { props: { href: '/foo' } }, 'Go to Foo')
styleModule Set CSS properties for DOM elements. h('span', {style: { color: '#c0ffee'}}, 'Say my name')

2. The Hooks is introduced

Hooks, also called Hooks, are methods of the DOM node lifecycle. Snabbdom provides a rich selection of hooks. Modules use hooks both to extend the Snabbdom and in normal code to execute arbitrary code during the DOM node lifecycle.

Here’s a quick overview of all the Hooks:

The name of the hook trigger The callback parameter
pre The patch phase starts. none
init A VNode has been added. vnode
create A DOM element is created based on VNode. emptyVnode, vnode
insert An element has been added to the DOM element. vnode
prepatch An element is about to enter patch. oldVnode, vnode
update An element starts to update. oldVnode, vnode
postpatch One element completes the patch phase. oldVnode, vnode
destroy An element is deleted directly or indirectly. vnode
remove An element is removed directly from the DOM element. vnode, removeCallback
post The patch phase is complete. none

Modules can use these hooks: pre, Create, Update, destroy, remove, and Post. Individual elements can use these hooks: init, create, INSERT, prepatch, Update, postpatch, destroy, and remove.

Snabbdom defines hooks like this:

// snabbdom/src/package/hooks.ts

export type PreHook = () = > any
export type InitHook = (vNode: VNode) = > any
export type CreateHook = (emptyVNode: VNode, vNode: VNode) = > any
export type InsertHook = (vNode: VNode) = > any
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) = > any
export type UpdateHook = (oldVNode: VNode, vNode: VNode) = > any
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) = > any
export type DestroyHook = (vNode: VNode) = > any
export type RemoveHook = (vNode: VNode, removeCallback: () => void) = > any
export type PostHook = () = > any

export interfaceHooks { pre? : PreHook init? : InitHook create? : CreateHook insert? : InsertHook prepatch? : PrePatchHook update? : UpdateHook postpatch? : PostPatchHook destroy? : DestroyHook remove? : RemoveHook post? : PostHook }Copy the code

Next we go through the sample code in the 03-modules.js file. We need style handling and event manipulation, so we introduce these two modules and combine them flexibly:

// src/03-modules.js

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

// 1. Import modules
import { styleModule } from 'snabbdom/src/package/modules/style'
import { eventListenersModule } from 'snabbdom/src/package/modules/eventlisteners'

// 2. Register module
const patch = init([ styleModule, eventListenersModule ])

// 3. Use the second argument to h() to pass in the data (object) required by the module.
let vnode = h('div', {
  style: { backgroundColor: '#4fc08d'.color: '#35495d' },
  on: { click: eventHandler }
}, [
  h('h1'.'Hello Snabbdom'),
  h('p'.'This is p tag')])function eventHandler() {
  console.log('clicked.')}const app = document.getElementById('app')
patch(app, vnode)
Copy the code

The styleModule and eventListenersModule modules are introduced in the above code and passed as arguments into the init() function. At this point, we can see that the content on the page has been included, and clicked on the event will output the ‘clicked.’ normally:

Here’s a look at the styleModule source code to simplify the code:

// snabbdom/src/package/modules/style.ts

function updateStyle (oldVnode: VNode, vnode: VNode) :void {
	// omit other code
}

function forceReflow () {
  // omit other code
}

function applyDestroyStyle (vnode: VNode) :void {
  // omit other code
}

function applyRemoveStyle (vnode: VNode, rm: () => void) :void {
  // omit other code
}

export const styleModule: Module = {
  pre: forceReflow,
  create: updateStyle,
  update: updateStyle,
  destroy: applyDestroyStyle,
  remove: applyRemoveStyle
}
Copy the code

EventListenersModule module

// snabbdom/src/package/modules/eventlisteners.ts

function updateEventListeners (oldVnode: VNode, vnode? : VNode) :void {
	// omit other code
}

export const eventListenersModule: Module = {
  create: updateEventListeners,
  update: updateEventListeners,
  destroy: updateEventListeners
}
Copy the code

It is obvious that both modules return an object, and each property is a hook, such as pre/ Create, with a corresponding handler value, and each handler has a uniform input parameter.

Take a look at how styles are bound in styleModule. Its updateStyle method is analyzed here, because the element creation and update phases are handled through this method:

// snabbdom/src/package/modules/style.ts

function updateStyle (oldVnode: VNode, vnode: VNode) :void {
  var cur: any
  var name: string
  var elm = vnode.elm
  var oldStyle = (oldVnode.data as VNodeData).style
  var style = (vnode.data as VNodeData).style

  if(! oldStyle && ! style)return
  if (oldStyle === style) return
  
  // 1. Set the old and new style defaults
  oldStyle = oldStyle || {}
  style = style || {}
  var oldHasDel = 'delayed' in oldStyle

  // 2
  for (name in oldStyle) {
    if(! style[name]) {if (name[0= = =The '-' && name[1= = =The '-') {
        (elm as any).style.removeProperty(name)
      } else {
        (elm as any).style[name] = ' '}}}for (name in style) {
    cur = style[name]
    if (name === 'delayed' && style.delayed) {
      // Omit some code
    } else if(name ! = ='remove'&& cur ! == oldStyle[name]) {if (name[0= = =The '-' && name[1= = =The '-') {
        (elm as any).style.setProperty(name, cur)
      } else {
        // 3. Set the new style to the element
        (elm as any).style[name] = cur
      }
    }
  }
}
Copy the code

3. The init () analysis

Next, let’s look at how these modules are handled inside the init() function.

First in the init.ts file you can see the list of Hooks declared to be supported by default:

// snabbdom/src/package/init.ts

const hooks: Array<keyof Module> = ['create'.'update'.'remove'.'destroy'.'pre'.'post']
Copy the code

Now look at how hooks are used:

// snabbdom/src/package/init.ts

export function init (modules: Array<Partial<Module>>, domApi? : DOMAPI) {
  let i: number
  let j: number
  const cbs: ModuleHooks = {  // Create a CBS object to collect hooks from module
    create: [].update: [].remove: [].destroy: [].pre: [].post: []}// Collect the hooks in module and save them in CBS
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]]
      if(hook ! = =undefined) {
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }
	// Omit the other code, described later
}
Copy the code

In init(), create a CBS object. Through a two-layer loop, save the hooks in each module to the CBS object.

From the breakpoint you can see that the CBS object in the demo looks like this:

The CBS object here collects the Hooks handlers from each Module in the corresponding Hooks array. For example, the Create hook holds the updateStyle function and updateEventListeners.

Now that the init() function has saved all the module Hooks, it’s time to look at the patch() function returned by init(), which uses the previously saved CBS object.

4. The patch () analysis

The init() function finally returns a patch() function, which forms a closure that uses variables and methods defined in the init() function’s scope, so you can use CBS objects in patch().

The patch() function iterates through the list of Hooks handlers in the CBS object at various points in time (see the Hooks section above).

// snabbdom/src/package/init.ts

export function init (modules: Array<Partial<Module>>, domApi? : DOMAPI) {
	// omit other code
  return function patch (oldVnode: VNode | Element, vnode: VNode) :VNode {
    let i: number.elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()  // [Hooks] Iterate over the list of Pre Hooks handler functions

    if(! isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode)// Create an empty VNode when the oldVnode argument is not VNode
    }

    if (sameVnode(oldVnode, vnode)) {  // When two VNodes are the same VNode, they are compared and updated
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      createElm(vnode, insertedVnodeQueue) // When two vNodes are different, a new element is created

      if(parent ! = =null) {  // If the oldVnode has a parent node, insert it and remove the original nodeapi.insertBefore(parent, vnode.elm! , api.nextSibling(elm)) removeVnodes(parent, [oldVnode],0.0)}}for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()  // [Hooks] Iterate over the list of post Hooks handler functions
    return vnode
  }
}
Copy the code

PatchVnode () is defined as follows:

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // omit other code
    if(vnode.data ! = =undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)  // [Hooks] Iterate over the list of update Hooks handling functions}}Copy the code

The createVnode() function is defined as follows:

  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue) :Node {
    // omit other code
    const sel = vnode.sel
    if (sel === '! ') {
      // omit other code
    } else if(sel ! = =undefined) {
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)  // [Hooks] Iterate over the list of create Hooks handling functions
      consthook = vnode.data! .hook }return vnode.elm
  }
Copy the code

The removeNodes() function is defined as follows:

  function removeVnodes (parentElm: Node,vnodes: VNode[],startIdx: number,endIdx: number) :void {
    // omit other code
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if(ch ! =null) { rm = createRmCb(ch.elm! , listeners)for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm) // [Hooks] Iterate over the list of remove Hooks handling functions}}}Copy the code

There are many jumps in this part of the code. To summarize the process, see the figure below:

Customize the Snabbdom module

Earlier we saw how the Snabbdom module system collects Hooks, saves them, and then executes different Hooks at different points in time.

In Snabbdom, all modules are independent of each other in SRC /package/modules, which can be combined flexibly and can be easily decoued and cross-platform, and each Module returns an Hooks type as follows:

// snabbdom/src/package/init.ts

export type Module = Partial<{
  pre: PreHook
  create: CreateHook
  update: UpdateHook
  destroy: DestroyHook
  remove: RemoveHook
  post: PostHook
}>

// snabbdom/src/package/hooks.ts
export type PreHook = () = > any
export type CreateHook = (emptyVNode: VNode, vNode: VNode) = > any
export type UpdateHook = (oldVNode: VNode, vNode: VNode) = > any
export type DestroyHook = (vNode: VNode) = > any
export type RemoveHook = (vNode: VNode, removeCallback: () => void) = > any
export type PostHook = () = > any
Copy the code

Therefore, if a developer needs a custom module, just implement different Hooks and export them.

Next, we implement a simple module, replaceTagModule, that automatically filters out the node text with HTML tags.

1. Initialize code

Considering the convenient debugging, we directly in node_modules/snabbdom/SRC/package/modules/directory replaceTag. The new ts file, and then write a simple demo framework:

import { VNode, VNodeData } from '.. /vnode'
import { Module } from './module'

const replaceTagPre = () = > {
    console.log("run replaceTagPre!")}const updateReplaceTag = (oldVnode: VNode, vnode: VNode): void= > {
    console.log("run updateReplaceTag!", oldVnode, vnode)
}

const removeReplaceTag = (vnode: VNode): void= > {
    console.log("run removeReplaceTag!", vnode)
}

export const replaceTagModule: Module = {
    pre: replaceTagPre,
    create: updateReplaceTag,
    update: updateReplaceTag,
    remove: removeReplaceTag
}
Copy the code

Let’s introduce the 03-modules.js code and simplify the code:

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

// 1. Import modules
import { styleModule } from 'snabbdom/src/package/modules/style'
import { eventListenersModule } from 'snabbdom/src/package/modules/eventlisteners'
import { replaceTagModule } from 'snabbdom/src/package/modules/replaceTag';

// 2. Register module
const patch = init([
  styleModule,
  eventListenersModule,
  replaceTagModule
])

// 3. Use the second argument to h() to pass in the data (object) required by the module.
let vnode = h('div'.'<h1>Hello Leo</h1>')

const app = document.getElementById('app')
const oldVNode = patch(app, vnode)

let newVNode = h('div'.'<div>Hello Leo</div>')

patch(oldVNode, newVNode)
Copy the code

Refresh the browser and see that every hook in the replaceTagModule executes normally:

2. Implement updateReplaceTag()

We removed the extra code and implemented the updateReplaceTag() function, which is called when a VNode is created and updated.

import { VNode, VNodeData } from '.. /vnode'
import { Module } from './module'

const regFunction = str= > str && str.replace(/\<|\>|\//g."");

const updateReplaceTag = (oldVnode: VNode, vnode: VNode): void= > {
    const oldVnodeReplace = regFunction(oldVnode.text);
    const vnodeReplace = regFunction(vnode.text);
    if(oldVnodeReplace === vnodeReplace) return;
    vnode.text = vnodeReplace;
}

export const replaceTagModule: Module = {
    create: updateReplaceTag,
    update: updateReplaceTag,
}
  
Copy the code

The updateReplaceTag() function compares the text content of the old vNode and the new vNode. If the text content is the same, the new vNode is returned. Otherwise, the new vNode text is set to the text property of the vnode to complete the update.

Here’s a detail:

vnode.text = vnodeReplace;
Copy the code

Vnode.text is assigned directly, and the content on the page changes accordingly. This is because vNode is a reactive object, and by calling its setter methods, reactive updates are triggered, thus updating the page content.

We see that the HTML tags in the page content are cleared.

3. Summary

In this section, we implement a simple replaceTagModule and experience the characteristics of flexible combination of Snabbdom modules. When we need to customize some modules, we can follow the Snabbdom module development method to develop custom modules. Then inject the module through the init() function of Snabbdom.

Let’s review the characteristics of Snabbdom module system: it supports introduction on demand, independent management, single responsibility, convenient combination reuse and strong maintainability.

5. General module life cycle model

Below, I abstract the previous Snabbdom module system into a generic module lifecycle model with three core layers:

  1. Module definition layer

In this layer, you can customize various modules according to module development specifications.

  1. Module application layer

Typically in the business development layer or component layer, it is used to import modules.

  1. Module initialization layer

Typically provided in a plug-in for a developed module system, init functions are executed that iterate over each Hooks and execute each function corresponding to the list of handler functions.

The abstract model is as follows:

When using modules, you can combine and match them flexibly, and in the Module initialization layer, you will make calls.

Six, summarized

This paper mainly takes the SNabbDOM-Demo warehouse as an example to learn the running process of Snabbdom and the running process of Snabbdom module system. It also takes you to appreciate the charm of Snabbdom module by handwriting a simple Snabbdom module. Finally, a general module plug-in model is summarized.

A good grasp of Snabbdom will help you understand Vue.