从snabbdom理解VDOM

349 阅读4分钟

这是我参与8月更文挑战的第8天,活动详情查看:    8月更文挑战 ​

序言

Vue 的三大核心,响应式,VDOM,模板编译,我们之前有研究过响应式了,接下来就是研读 VDOM 了,于是我决定从 VDOM 的启发 库 ,snabbdom 开始学习,今天我们就来看看 snabbdom 给了我们带来什么样的启发。

安装snabbdom

  1. github 上 clone : 这里是TS版本的。
$ git clone https://github.com/snabbdom/snabbdom
  1. 使用 npm :这里是 build 出来的 JS 版的。
npm i -D snabbdom

snabbdom 简介

snabbdom 见名知意,虚拟 DOM。

  1. 那么你知道什么是 Virtual DOM 吗。通俗的说,Virtual DOM 就是一个 JS 对象,它是真实 DOM 的抽象,只保留一些有用的信息,更轻量地描述 DOM 树的结构。
  2. snabbdom 就是一个将真实 DOM 用特定格式的 JS 对象 描述,再将那个特定格式的 JS 对象转换成真实 DOM 的实现。
  3. 那种特定格式的 JS 对象叫做 VNode。

Node与 VNode

真实的 Node 对象。

<a href="http://www.baidu.com">去百度</a>

真实 Node 对应的的虚拟 Node (VNode )。

{"sel":"a",
  "data":{
    props:{
      href:'http://www.baidu.com'
    }
  },
  "text":"去百度",
   children:"undefined",
   elm:"undefined",
   key:"undefined"
}

我们发现,真实 Node 上的属性在虚拟 Node 中都有相应的描述,信息是不会丢失的,就是换了一种表示法 。 VNode 通过 patch 函数可以变成真实 Node 组合成 DOM 挂载到页面上

Vnode 定义

export interface VNode {
  sel: string | undefined;//标签名
  data: VNodeData | undefined;//标签内的属性数据
  children: Array<VNode | string> | undefined;//子标签
  elm: Node | undefined;//当前节点对应的真实DOM节点
  text: string | undefined;//当前节点文本
  key: Key | undefined;// 子节点key属性
}
​
export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: Array<any>; // for thunks
  [key: string]: any; // for any other 3rd party module
}

我们来看一个简单的案例,这个案例我们将展示将一个 Vnode 对象转换成真实 Node 并且显示在页面上

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'// 创建patch函数    
// 参数:数组,传入模块
const patch = init([])
​
// 创建虚拟节点
// 第一个参数:标签 + 选择器
// 第二个参数:如果是字符串就是标签的内容
let vnode = h('div#container.cls', 'Hello World')
const app = document.getElementById('app')
​
//让虚拟节点上DOM树
// 第一个参数:可以是 DOM 元素,内部会把 DOM 元素转换成 vnode
// 第二个参数: VNode
// 返回值:VNode
const oldVNode = patch(app, vnode)

我们可以看到在页面上展示一个内容需要三个步骤

  1. 使用 h 函数创建虚拟节点(VNode)。
  2. 定义要挂载节点的位子。
  3. 使用 patch 函数,使 VNode 变成真实的 Node 并且挂载到传入的节点位子。

小结:由上述案例,我们不禁疑问,h 函数如何创造 Vnode 的呢?patch 方法又是如何将 Vnode 转换成真实 Node并且挂载到页面中呢。

解密 h 函数

看一个函数,首先看入参和出参。 h 函数接受三个参数,

  1. 一个代表标签名的 sel 字符串,
  2. 第二个参数是模块数据的定义(也就是 class,click 等等)
  3. 第三个参数是子节点的形式
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 

我们又发现 h 函数通过函数重载来处理不同的参数传入。

 if (c !== undefined) {
    if (b !== null) {
      data = b
    }
    if (is.array(c)) {
      children = c
    } else if (is.primitive(c)) {
      text = c
    } else if (c && c.sel) {
      children = [c]
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b
    } else if (is.primitive(b)) {
      text = b
    } else if (b && b.sel) {
      children = [b]
    } else { data = b }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
    }
  }

经过处理完后,可以分离出VNode 函数需要的参数。

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

我们可以看到 vnode 方法接受 4 个参数,返回成一个 VNode 对象。 小结:h 函数把接受的参数通过函数重载,将数据分离,然后给VNode函数,最后转换成相应格式的 VNode对象。 我们来看一个真实案例

let vnode = h(
  "div#container.cls",
  {
    class: {
      active: true,
    },
    style: {
      background: "#fff",
    },
    on: {
      click: clickFn,
    },
    dataset: {
      name: "coolFish",
    },
    hook: {
      init: function () {
        console.log("init");
      },
      create: function () {
        console.log("create");
      },
      insert: function () {
        console.log("insert");
      },
      prepatch: function () {
        console.log("beforePatch");
      },
      update: function () {
        console.log("update");
      },
      postpatch: function () {
        console.log("postPatch");
      },
      destroy: function () {
        console.log("destroy");
      },
      remove: function (ch, rm) {
        console.log("remove");
        rm();
      },
    },
  },
  [h("p", {}, "我是第一个Vnode")]
);

function clickFn() {
  console.log("click");
}

以上 h 函数可以生成以下 VNode 我们可以看到他的父容器是一个 id 为 container 的 div。他有class,click事件,style, 为何要转换成虚拟DOM,因为精细化比较,是使用新虚拟DOM和老虚拟DOM进行比较,算出如何最小量更新,然后反应到真实DOM上。 有了VNode,通过 patch函数就能将该VNode挂载到页面中。