探索 Snabbdom 模块系统原理 上

简介: 探索 Snabbdom 模块系统原理 上


网络异常,图片无法展示
|

近几年随着 React、Vue 等前端框架不断兴起,Virtual DOM 概念也越来越火,被用到越来越多的框架、库中。Virtual DOM 是基于真实 DOM 的一层抽象,用简单的 JS 对象描述真实 DOM。本文要介绍的 Snabbdom 就是 Virtual DOM 的一种简单实现,并且 Vue 的 Virtual DOM 也参考了 Snabbdom 实现方式。

对于想要深入学习 Vue Virtual DOM 的朋友,建议先学习 Snabbdom,对理解 Vue 会很有帮助,并且其核心代码 200 多行。

本文挑选 Snabbdom 模块系统作为主要核心点介绍,其他内容可以查阅官方文档《Snabbdom》

网络异常,图片无法展示
|

一、Snabbdom 是什么

Snabbdom 是一个专注于简单性、模块化、强大特性和性能的虚拟 DOM 库。其中有几个核心特性:

  1. 核心代码 200 行,并且提供丰富的测试用例;
  2. 拥有强大模块系统,并且支持模块拓展和灵活组合;
  3. 在每个 VNode 和全局模块上,都有丰富的钩子,可以在 Diff 和 Patch 阶段使用。

接下来从一个简单示例来体验一下 Snabbdom。

1. 快速上手

安装 Snabbdom:

npm install snabbdom -D

接着新建 index.html,设置入口元素:

<div id="app"></div>

然后新建 demo1.js 文件,并使用 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)

这样就实现一个简单示例,在浏览器打开 index.html,页面将显示 “Hello Leo” 文本。

网络异常,图片无法展示
|

接下来,我会以 snabbdom-demo 项目作为学习示例,从简单示例到模块系统使用的示例,深入学习和分析 Snabbdom 源码,重点分析 Snabbdom 模块系统。

二、Snabbdom-demo 分析

Snabbdom-demo 项目中的三个演示代码,为我们展示如何从简单到深入 Snabbdom。 首先克隆仓库并安装:

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

虽然本项目没有 README.md 文件,但项目目录比较直观,我们可以轻松的从 src 目录找到这三个示例代码的文件:

  • 01-basicusage.js
  • 02-basicusage.js
  • 03-modules.js -> 本文核心介绍

接着在 index.html 中引入想要学习的代码文件,默认 <script src="./src/01-basicusage.js"></script>  ,通过 package.json 可知启动命令并启动项目:

$ npm run dev

1. 简单示例分析

当我们要研究一个库或框架等比较复杂的项目,可以通过官方提供的简单示例代码进行分析,我们这里选择该项目中最简单的 01-basicusage.js 代码进行分析,其代码如下:

// 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') // 入口元素
const oldVNode = patch(app, vnode)
// 假设时刻
vnode = h('div', 'Hello Snabbdom')
patch(oldVNode, vnode)

运行项目以后,可以看到页面展示了“Hello Snabbdom”文本,这里你会觉得奇怪,前面的 “Hello World” 文本去哪了

网络异常,图片无法展示
|

原因很简单,我们把 demo 中的下面两行代码注释后,页面便显示文本是 “Hello World”:

vnode = h('div', 'Hello Snabbdom')
patch(oldVNode, vnode)

这里我们可以猜测 patch() 函数可以将** VNode** 渲染到页面。更进一步可以理解为,这边第一个执行 patch() 函数为首次渲染,第二次执行 patch() 函数为更新操作

网络异常,图片无法展示
|

2. VNode 介绍

这里可能会有小伙伴疑惑,示例中的 VNode 是什么?这里简单解释下:

VNode,该对象用于描述节点的信息,它的全称是虚拟节点(virtual node)。与 “虚拟节点” 相关联的另一个概念是 “虚拟 DOM”,它是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。“虚拟 DOM” 由 VNode 组成的。 —— 全栈修仙之路 《Vue 3.0 进阶之 VNode 探秘》

其实 VNode 就是一个 JS 对象,在 Snabbdom 中是这么定义 VNode 的类型:

export interface VNode {
  sel: string | undefined; // selector的缩写
  data: VNodeData | undefined; // 下面VNodeData接口的内容
  children: Array<VNode | string> | undefined; // 子节点
  elm: Node | undefined; // element的缩写,存储了真实的HTMLElement
  text: string | undefined; // 如果是文本节点,则存储text
  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?: any[] // for thunks
  [key: string]: any // for any other 3rd party module
}

在 VNode 对象中含描述节点选择器 sel 字段、节点数据 data 字段、节点所包含的子节点 children 字段等。

在这个 demo 中,我们似乎并没有看到模块系统相关的代码,没事,因为这是最简单的示例,下一节会详细介绍。

我们在学习一个函数时,可以重点了解该函数的“入参”和“出参”,大致就能判断该函数的作用。

从这个 demo 主要执行过程可以看出,主要用到有三个函数: init() / patch() / h() ,它们到底做什么用的呢?我们分析一下 Snabbdom 源码中这三个函数的入参和出参情况:

3. init() 函数分析

init() 函数被定义在 package/init.ts 文件中:

// node_modules/snabbdom/src/package/init.ts
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  // 省略其他代码
}

其参数类型如下:

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
}

init() 函数接收一个模块数组 modules 和可选的 domApi 对象作为参数,返回一个函数,即 patch() 函数。 domApi 对象的接口包含了很多 DOM 操作的方法。 这里的 modules 参数本文将重点介绍。

4. patch() 函数分析

init() 函数返回了一个 patch() 函数,其类型为:

// node_modules/snabbdom/src/package/init.ts
patch(oldVnode: VNode | Element, vnode: VNode) => VNode

patch() 函数接收两个 VNode 对象作为参数,并返回一个新 VNode。

5. h() 函数分析

h() 函数被定义在 package/h.ts 文件中:

// 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{
  // 省略其他代码
}

h() 函数接收多种参数,其中必须有一个 sel 参数,作用是将节点内容挂载到该容器中,并返回一个新 VNode。

6. 小结

通过前面介绍,我们在回过头看看这个 demo 的代码,大致调用流程如下:

网络异常,图片无法展示
|


目录
相关文章
|
24天前
|
缓存 Java
java开发常用模块——缓存模块
java开发常用模块——缓存模块
|
2月前
|
Python
理解模块功能
理解模块功能
26 8
|
7月前
31 # 模块的概念
31 # 模块的概念
29 0
|
10月前
|
Linux
模块的加载过程四
模块的加载过程四
82 0
|
10月前
|
存储 Linux C语言
模块的加载过程一
模块的加载过程一
100 0
|
10月前
|
Linux
模块的加载过程二(上)
模块的加载过程二
65 0
|
10月前
|
程序员 Linux
模块的加载过程二(下)
模块的加载过程二(下)
92 0
|
10月前
|
编译器
模块的加载过程三(下)
模块的加载过程三(下)
96 0
|
10月前
|
Linux 索引
模块的加载过程三
模块的加载过程三
55 0
|
12月前
|
数据采集 算法 数据可视化
MMdetection框架速成系列 第03部分:简述整体构建细节与模块+训练测试模块流程剖析+深入解析代码模块与核心实现
按照抽象到具体方式,从多个层次进行训练和测试流程深入解析,从最抽象层讲起,到最后核心代码实现,希望帮助大家更容易理解 MMDetection 开源框架整体构建细节
517 0