最近在深入学习 Virtual DOM
的相关知识,参考了许多资料,也拜读了许多大神的文章,所以在这里大致的整理成了比较适合自己理解的方式,方便时不时回来翻翻,复习一下,篇幅较长,主要会分为三个部分来分别进行介绍,具体章节如下,目录名就差不多代表了章节的相关内容
本篇是第一部分,主要介绍 Virtual DOM
相关内容,主要参考的是 HcySunYang/vue-design,本章相关内容如下
- 什么是
Virtual DOM
- 如何将
Virtual DOM
渲染为真实的DOM
节点 - 用
VNode
描述真实DOM
VNode
的种类
- 如何将
- 辅助创建
VNode
的h
函数- 完善
h
函数 - 使用
h
函数来创建VNode
- 完善
下面我们就一步一步来看
什么是 Virtual DOM?
在谈论 Virtual DOM
之前,我们必须要先理解什么是 DOM
?DOM
即 Document Object Model
,是一种通过对象表示结构化文档的方式,DOM
是跨平台的,也是语言无关的(比如 HTML
和 XML
都可以用它表示与操作),浏览器处理 DOM
的实现细节,然后我们可以通过 JavaScript
和 CSS
来与它交互
DOM
的主要问题是没有为创建动态 UI
而优化,虽然可以使用 jQuery
这种可以用来简化 DOM
操作的类库,但是并没有解决大量 DOM
操作的性能问题,因为在大型页面或者单页应用里,动态的创建或销毁 DOM
的操作是很频繁的,DOM
操作是很慢的,比如新创建了一个 div
,并不是只有单单一个 div
元素那么简单,这个元素上本身或者继承很多属性如 width
,height
,offsetHeight
,style
,title
等,另外还需要注册这个元素的诸多方法,比如 onfucos
,onclick
等等,这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低
比如我们来看下图,我们在一个空白网页的 body
中添加一个 div
元素(为了偷懒就直接把百度的首页掏空添加了一个空的 div
)
这个元素会挂载默认的 styles
,得到这个元素的 computed
属性,注册相应的 Event Listener
,DOM Breakpoints
以及大量的 properties
,这些属性和方法的注册肯定是需要耗费大量时间的(看右侧的滚动条就知道需要挂载多少内容了)
而 Virtual DOM
就是解决问题的一种探索,Virtual DOM
建立在 DOM
之上,是基于 DOM
的一层抽象,实际可理解为用更轻量的纯 JavaScript
对象(树)来描述 DOM
(树),操作 JavaScript
对象当然比操作 DOM
快,因为不用更新屏幕,我们可以随意改变 Virtual DOM
,然后仅仅将需要改变的地方再更新到 DOM
上
如何将 Virtual DOM 渲染为真实的 DOM 节点
从这一部分开始,我们为了简便,将会使用
VNode
来简称Virtual DOM
(其实只需要知道它们是同一个东西即可)
VNode
是真实 DOM
的描述,比如我们可以用如下对象描述一个 div
标签
1 | const elementVnode = { |
想要把 elementVnode
渲染成真实 DOM
,我们还需要一个渲染器(Renderer
),下面是一个简单的实现
1 | function render(vnode, container) { |
为了渲染之前的 div
标签,我们可以这样调用 render
函数
1 | // 把 elementVnode 渲染到 id 为 app 的元素下 |
上述只是一个简单的示例,只能适用于普通的标签,但并不适用于『组件』,为了能够渲染组件,我们还需要思考组件的 VNode
应该如何表示?对于 HTML
标签的 VNode
来说,其 tag
属性的值就是标签的名字,但如果是组件的话,我们可以将其 VNode
中的 tag
属性指向组件自身,比如如下组件
1 | // 定义一个 render 方法,里面返回指定的 tag |
但想要正确地渲染该组件,我们还需要修改我们的 render
函数,我们可以通过判断 vnode.tag
是否是字符串 来区分一个 VNode
到底是 HTML
标签还是组件
1 | function render(vnode, container) { |
是不是已经有了一点思路了,我们就可以按照以上的方式逐渐的丰富我们的 VNode
和 render
方法,使其的通用性更强
用 VNode 描述真实 DOM
那么一个 VNode
到底需要拥有哪些属性呢,我们一点一点来讨论,如下
- 首先我们使用
tag
属性来存储标签的名字 - 可以用
data
属性来存储该标签的附加信息,比如style
、class
、event
等,通常我们把一个VNode
对象的data
属性称为VNodeData
- 为了描述子节点,我们需要给
VNode
对象添加children
属性,若有多个子节点,则可以把children
属性设计为一个数组 - 除了标签元素之外,
DOM
中还有文本节点,由于文本节点没有标签名字,所以它的tag
属性值为null
,由于文本节点也无需用额外的VNodeData
来描述附加属性,所以其data
属性值也是null
针对以上内容,我们可以简单的整理出两种类型的描述,『普通标签』和『文本节点』
1 | // 一个 div 标签,具有 style 属性和两个子节点 |
下面我们再来看看如何描述组件,我们之前提到过,可以通过检查 tag
属性值是否是字符串来确定一个 VNode
是否是普通标签,即
1 | <div> |
对应的 VNode
1 | const elementVNode = { |
另外还有两种特殊的组件,即 Fragment
和 Portal
,具体的使用场景就不详细展开了,我们只来看看如何进行表示,我们使用 tag
来标记 Fragment
,当渲染器在渲染 VNode
时,如果发现该 VNode
的类型是 Fragment
,就只需要把该 VNode
的子节点渲染到页面
1 | const Fragment = Symbol() |
同样的针对 Portal
,我们可以得出以下的对应关系
1 | <template> |
Portal
类型的 VNode
与 Fragment
类型的 VNode
类似,都需要一个唯一的标识,来区分其类型,目的是告诉渲染器如何渲染该 VNode
VNode 的种类
不同类型的 VNode
拥有不同的设计,我们可以把 VNode
分成五类,分别是 html/svg
元素、组件、纯文本、Fragment
以及 Portal
,如下图所示
但是这里会存在一个问题,比如之前我们在判断需要挂载的对象是标签还是组件的时候,使用的是通过检查 tag
属性值是否是字符串来确定一个 VNode
是否是普通标签,这样是不严谨的,所以我们就有必要使用一个唯一的标识,来标明某一个 VNode
具体是属于哪一类,我们只需要为每一个 VNode
种类都分配一个 flags
值即可,在 JavaScript
里就用一个对象来表示即可
1 | // flags.js |
这里简单的介绍一下位运算,如下是利用 VNodeFlags
判断 VNode
类型的例子,比如判断一个 VNode
是否是组件
1 | // 使用按位与(&)运算 |
来看下表
VNodeFlags | 左移运算 | 32 位的 bit 序列(出于简略,只用 9 位表示) |
---|---|---|
ELEMENT_HTML |
无 | 00000000 1 |
ELEMENT_SVG |
1 << 1 |
0000000 1 0 |
COMPONENT_STATEFUL_NORMAL |
1 << 2 |
000000 1 00 |
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
1 << 3 |
00000 1 000 |
COMPONENT_STATEFUL_KEPT_ALIVE |
1 << 4 |
0000 1 0000 |
COMPONENT_FUNCTIONAL |
1 << 5 |
000 1 00000 |
TEXT |
1 << 6 |
00 1 000000 |
FRAGMENT |
1 << 7 |
0 1 0000000 |
PORTAL |
1 << 8 |
1 00000000 |
根据上表展示的基本 flags
值可以很容易地得出下表
VNodeFlags | 32 位的 bit 序列(出于简略,只用 9 位表示) |
---|---|
ELEMENT |
00000001 1 |
COMPONENT_STATEFUL |
00001 1 100 |
COMPONENT |
000 1 1 1 1 00 |
所以很自然的,只有 VNodeFlags.ELEMENT_HTML
和 VNodeFlags.ELEMENT_SVG
与 VNodeFlags.ELEMENT
进行按位与(&
)运算才会得到非零值,即为真
更多关于此处的内容可以参考 MDN-标志位与掩码
有了这些 flags
之后,我们在创建 VNode
的时候就可以预先为其打上 flags
,以标明该 VNode
的类型
1 | // html 元素节点 |
那么最后就只剩下 VNode
的 VNodeData
属性,它其实也是一个对象,不过我们会留在后面进行介绍,至此,我们已经对 VNode
完成了一定的设计,目前为止我们所设计的 VNode
对象如下
1 | export interface VNode { |
辅助创建 VNode 的 h 函数
我们之前已经介绍了 VNode
的种类和一些其他相关概念,但是在实际开发过程当中,去手写 VNode
肯定是不太现实的,所以我们需要一个可以帮助我们创建 VNode
对象的函数,在这里我们将其命名为 h
,先来看一个最简单的 h
函数
1 | function h() { |
这个 h
函数只能生成用来描述一个空的 <h1></h1>
,实际上并没有太大的意义,接下来我们会来一一完善它
完善 h 函数
首先需要确定参数,实际上只需要把 tag
、data
和 children
提取为参数即可
1 | // 为什么三个参数就能满足需求,对于 _isVNode 属性,它的值始终都为 true,所以不需要提取到参数中 |
下面我们就来针对各种类型来单独处理,详细内容可以参考注释
1 | import { VNodeFlags, ChildrenFlags } from './flags' |
使用 h 函数来创建 VNode
最后我们来看一些使用 h
函数的实际效果
1 | // 模版 |
现在,我们的 h
函数已经可以创建任何类型的 VNode
对象了,有了 VNode
对象,我们下一步要做的就是将 VNode
对象渲染成真实 DOM
参考
如果想了解更多的相关内容,可以参考以下链接