You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
近几年随着 React、Vue 等前端框架不断兴起,Virtual DOM 概念也越来越火,被用到越来越多的框架、库中。Virtual DOM 是基于真实 DOM 的一层抽象,用简单的 JS 对象描述真实 DOM。本文要介绍的 Snabbdom 就是 Virtual DOM 的一种简单实现,并且 Vue 的 Virtual DOM 也参考了 Snabbdom 实现方式。
对于想要深入学习 Vue Virtual DOM 的朋友,建议先学习 Snabbdom,对理解 Vue 会很有帮助,并且其核心代码 200 多行。
近几年随着 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 库。其中有几个核心特性:
接下来从一个简单示例来体验一下 Snabbdom。
1. 快速上手
安装 Snabbdom:
接着新建 index.html,设置入口元素:
然后新建 demo1.js 文件,并使用 Snabbdom 提供的函数:
这样就实现一个简单示例,在浏览器打开 index.html,页面将显示 “Hello Leo” 文本。
接下来,我会以 snabbdom-demo 项目作为学习示例,从简单示例到模块系统使用的示例,深入学习和分析 Snabbdom 源码,重点分析 Snabbdom 模块系统。
二、Snabbdom-demo 分析
Snabbdom-demo 项目中的三个演示代码,为我们展示如何从简单到深入 Snabbdom。
首先克隆仓库并安装:
虽然本项目没有 README.md 文件,但项目目录比较直观,我们可以轻松的从 src 目录找到这三个示例代码的文件:
接着在 index.html 中引入想要学习的代码文件,默认
<script src="./src/01-basicusage.js"></script>
,通过 package.json 可知启动命令并启动项目:1. 简单示例分析
当我们要研究一个库或框架等比较复杂的项目,可以通过官方提供的简单示例代码进行分析,我们这里选择该项目中最简单的 01-basicusage.js 代码进行分析,其代码如下:
运行项目以后,可以看到页面展示了“Hello Snabbdom”文本,这里你会觉得奇怪,前面的 “Hello World” 文本去哪了?
原因很简单,我们把 demo 中的下面两行代码注释后,页面便显示文本是 “Hello World”:
这里我们可以猜测
patch()
函数可以将 VNode 渲染到页面。更进一步可以理解为,这边第一个执行patch()
函数为首次渲染,第二次执行patch()
函数为更新操作。2. VNode 介绍
这里可能会有小伙伴疑惑,示例中的 VNode 是什么?这里简单解释下:
其实 VNode 就是一个 JS 对象,在 Snabbdom 中是这么定义 VNode 的类型:
在 VNode 对象中含描述节点选择器
sel
字段、节点数据data
字段、节点所包含的子节点children
字段等。在这个 demo 中,我们似乎并没有看到模块系统相关的代码,没事,因为这是最简单的示例,下一节会详细介绍。
从这个 demo 主要执行过程可以看出,主要用到有三个函数:
init()
/patch()
/h()
,它们到底做什么用的呢?我们分析一下 Snabbdom 源码中这三个函数的入参和出参情况:3. init() 函数分析
init()
函数被定义在package/init.ts
文件中:其参数类型如下:
init()
函数接收一个模块数组modules
和可选的domApi
对象作为参数,返回一个函数,即patch()
函数。domApi
对象的接口包含了很多 DOM 操作的方法。这里的
modules
参数本文将重点介绍。4. patch() 函数分析
init()
函数返回了一个patch()
函数,其类型为:patch()
函数接收两个 VNode 对象作为参数,并返回一个新 VNode。5. h() 函数分析
h()
函数被定义在package/h.ts
文件中:h()
函数接收多种参数,其中必须有一个sel
参数,作用是将节点内容挂载到该容器中,并返回一个新 VNode。6. 小结
通过前面介绍,我们在回过头看看这个 demo 的代码,大致调用流程如下:
三、深入 Snabbdom 模块系统
学习完前面这些基础知识后,我们已经知道 Snabbdom 使用方式,并且知道其中三个核心方法入参出参情况和大致作用,接下来开始看本文核心 Snabbdom 模块系统。
1. Modules 介绍
Snabbdom 模块系统是 Snabbdom 提供的一套可拓展、可灵活组合的模块系统,用来为 Snabbdom 提供操作 VNode 时的各种模块支持,如我们组建需要处理 style 则引入对应的 styleModule,需要处理事件,则引入 eventListenersModule 既可,这样就达到灵活组合,可以支持按需引入的效果。
Snabbdom 模块系统的特点可以概括为:支持按需引入、独立管理、职责单一、方便组合复用、可维护性强。
当然 Snabbdom 模块系统还有其他内置模块:
setAttribute
方法。h('a', { attrs: { href: '/foo' } }, 'Go to Foo')
h('a', { class: { active: true, selected: false } }, 'Toggle')
data- *
)。然后可以使用 HTMLElement.dataset 属性访问它们。h('button', { dataset: { action: 'reset' } }, 'Reset')
h('div', { on: { click: clickHandler } })
h('a', { props: { href: '/foo' } }, 'Go to Foo')
h('span', {style: { color: '#c0ffee'}}, 'Say my name')
2. Hooks 介绍
Hooks 也称钩子,是 DOM 节点生命周期的一种方法。Snabbdom 提供丰富的钩子选择。模块既使用钩子来扩展 Snabbdom,也在普通代码中使用钩子,用来在 DOM 节点生命周期中执行任意代码。
这里大致介绍一下所有的 Hooks:
pre
init
vnode
create
emptyVnode, vnode
insert
vnode
prepatch
oldVnode, vnode
update
oldVnode, vnode
postpatch
oldVnode, vnode
destroy
vnode
remove
vnode, removeCallback
post
模块中可以使用这些钩子:
pre
,create
,update
,destroy
,remove
,post
。单个元素可以使用这些钩子:
init
,create
,insert
,prepatch
,update
,postpatch
,destroy
,remove
。Snabbdom 是这么定义钩子的:
接下来我们通过 03-modules.js 文件的示例代码,我们需要样式处理和事件操作,因此引入这两个模块,并进行灵活组合:
上面代码中,引入了 styleModule 和 eventListenersModule 两个模块,并且作为参数组合,传入
init()
函数中。此时我们可以看到页面上显示的内容已经有包含样式,并且点击事件也能正常输出日志
'clicked.'
:这里我们看下 styleModule 模块源码,把代码精简一下:
在看看 eventListenersModule 模块源码:
明显可以看出,两个模块返回的都是个对象,并且每个属性为一种钩子,如
pre/create
等,值为对应的处理函数,每个处理函数有统一的入参。继续看下 styleModule 中,样式是如何绑定上去的。这里分析它的
updateStyle
方法,因为元素创建(create 钩子)和元素更新(update 钩子)阶段都是通过这个方法处理:3. init() 分析
接着我们看下
init()
函数内部如何处理这些 Module。首先在 init.ts 文件中,可以看到声明了默认支持的 Hooks 钩子列表:
接着看
hooks
是如何使用的:上面代码中,创建
hooks
变量用来声明默认支持的 Hooks 钩子,在init()
函数中,创建cbs
对象,通过两层循环,保存每个 module 中的 hook 函数到cbs
对象的指定钩子中。通过断点可以看到这是 demo 中,
cbs
对象是下面这个样子:这里
cbs
对象收集了每个 module 中的 Hooks 处理函数,保存到对应 Hooks 数组中。比如这里的create
钩子中保存了updateStyle
函数和updateEventListeners
函数。到这里,
init()
函数已经保存好所有 module 的 Hooks 处理函数,接下来就要看看init()
函数返回的patch()
函数,这里面将用到前面保存好的cbs
对象。4. patch() 分析
init()
函数中最终返回一个patch()
函数,这边形成一个闭包,闭包里面可以使用到init()
函数作用域定义的变量和方法,因此在patch()
函数中能使用cbs
对象。patch()
函数会在不同时机点(可以参照前面的 Hooks 介绍),遍历cbs
对象中不同 Hooks 处理函数列表。patchVnode()
函数定义如下:createVnode()
函数定义如下:removeNodes()
函数定义如下:这部分代码跳转较多,总结一下这个过程,如下图:
四、自定义 Snabbdom 模块
前面我们介绍了 Snabbdom 模块系统是如何收集 Hooks 并保存下来,然后在不同时机点执行不同的 Hooks。
在 Snabbdom 中,所有模块独立在
src/package/modules
下,使用的时候可以灵活组合,也方便做解耦和跨平台,并且所有 Module 返回的对象中每个 Hooks 类型如下:因此,如果开发者需要自定义模块,只需实现不同 Hooks 并导出即可。
接下来我们实现一个简单的模块 replaceTagModule,用来将节点文本自动过滤掉 HTML 标签。
1. 初始化代码
考虑到方便调试,我们直接在
node_modules/snabbdom/src/package/modules/
目录中新建 replaceTag.ts 文件,然后写个最简单的 demo 框架:接下来引入到 03-modules.js 代码中,并简化下代码:
刷新浏览器,就可以看到 replaceTagModule 的每个钩子都被正常执行:
2. 实现 updateReplaceTag() 函数
我们删除掉多余代码,接下来实现
updateReplaceTag()
函数,当 vnode 创建和更新时,都会调用该方法。在
updateReplaceTag()
函数中,比较新旧 vnode 的文本内容是否一致,如果一致则直接返回,否则将新的 vnode 的替换后的文本设置到 vnode 的 text 属性,完成更新。其中有个细节:
这里直接对
vnode.text
进行赋值,页面上的内容也随之发生变化。这是因为vnode
是个响应式对象,通过调用其setter
方法,会触发响应式更新,这样就实现页面内容更新。于是我们看到页面内容中的 HTML 标签被清空了。
3. 小结
这个小节中,我们实现一个简单的
replaceTagModule
模块,体验了一下 Snabbdom 模块灵活组合的特点,当我们需要自定义某些模块时,便可以按照 Snabbdom 的模块开发方式,开发自定义模块,然后通过 Snabbdom 的init()
函数注入模块即可。我们再回顾一下 Snabbdom 模块系统特点:支持按需引入、独立管理、职责单一、方便组合复用、可维护性强。
五、通用模块生命周期模型
下面我将前面 Snabbdom 的模块系统,抽象为一个通用模块生命周期模型,其中包含三个核心层:
在本层可以按照模块开发规范,自定义各种模块。
一般是在业务开发层或组件层中,用来导入模块。
一般是在开发的模块系统的插件中,提供初始化函数(init 函数),执行初始化函数会遍历每个 Hooks,并执行对应处理函数列表的每个函数。
抽象后的模型如下:
在使用 Module 的时候就可以灵活组合搭配使用啦,在模块初始化层,就会做好调用。
六、总结
本文主要以 Snabbdom-demo 仓库为学习示例,学习了 Snabbdom 运行流程和 Snabbdom 模块系统的运行流程,还通过手写一个简单的 Snabbdom 模块,带大家领略一下 Snabbdom 模块的魅力,最后为大家总结了一个通用模块插件模型。
大家好好掌握 Snabbdom 对理解 Vue 会很有帮助。
The text was updated successfully, but these errors were encountered: