目录
第5章 虚拟DOM简介
5.1 什么是虚拟DOM
虚拟DOM是随着时代发展而诞生的产物。
在Web早期,页面的交互效果比现在简单得多,没有很复杂得状态需要管理,也不太需要频繁地操作DOM,使用jQuery来开发也可以满足我们的需求。
随着时代的发展页面上的功能越来越多,我们需要实现的需求也越来越复杂,程序中需要维护的状态也越来越多,DOM的操作也越来越频繁。
现在,我们使用三大主流框架Vue.js
、Angular
和Reac
t都是声明式操作DOM。我们通过描述状态和DOM之间的映射关系式怎么样的,就可以将状态渲染成视图。关于状态到视图的转换过程,框架会帮我们做,不需要我们手动去操作DOM。那这些框架是如何帮我们进行操作DOM的呢?
在Angular
中就是脏检查的流程,React
中使用虚拟DOM,Vue.js 1.0
通过颗粒度的绑定。因此虚拟DOM本质上只是众多解决方案中的一种,可以用但不一定必须用。
虚拟DOM的解决方法是通过状体生成一个虚拟节点数,然后使用虚拟节点数进行渲染,在渲染之前,会使用新生成的虚拟节点数和上一次生成的虚拟节点数进行对比,只渲染不同的部分。
5.2 为什么要引入虚拟DOM
事实上,Angular
和React
的变化侦测有一个共同点,那就是它们都不知道那些状态变量,因此,就需要进行暴力的比较。React
是通过虚拟DOM的比较,Angular
是使用脏检查的流程。
Vue.js
的变化侦测跟他们都不一样,它在一定程度上知道具体哪些状态发生的变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js
当中,当状态发生变化时,它在一定程度上知道哪个节点使用的这个状态,从而对这些节点进行更新操作,根本不需要对比。事实上在Vue.js 1.0
的时候就是这样实现的。
但是这样做其实也有一定的代价,因为粒度太小了,每一个绑定都会有一个对应的watcher
来观察状态的变化,这样就会有一些内存开销以及一些依赖追踪的开销,当状态被越多的节点使用时,开销越大。对于一个大型项目来说,这个开销是非常大的。
因此,在Vue.js 2.0
开始选择了一个中等粒度的解决方案,那就是引入虚拟DOM。组件级别是一个 watcher
实例,也就是说即便一个组件内有10个节点使用的某个状态,但其实也只有一个watcher来观察这个状态的变化,所以当这个状态发生变化是只能通知到组件,然后组件内部通知虚拟DOM去进行对比与渲染,这是一个比较折中的方案。
5.3 Vue.js中的虚拟DOM
在Vue.js中,我们使用模板来描述状态与DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数,执行渲染函数就可以得到一个虚拟节点树,在虚拟节点映射到视图的过程中,将虚拟节点与上次渲染视图所使用的旧虚拟节点(oldVnode
)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无任何改动的DOM。
可以看出,虚拟DOM在Vue.js
中所做的事情其实并没有想象中那么复杂,它主要做了两件事。
- 提供与真实DOM节点所对应的虚拟节点
vnode
- 将虚拟节点
vnode
和旧虚拟节点oldVnode
进行比较更新视图
对两个虚拟节点进行比较时虚拟DOM中最核心的算法(即patch
),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。
第6章 VNode
6.1 什么是VNode
在Vue.js
中存在一个vnode
类,可以理解JavaScript对象版本的DOM元素,使用它可以实例化不同类型的vnode
实例,而不同类型的vnode实例各自表示不同类型的DOM元素。
6.2 VNode的作用
每次渲染视图都是先创建vnode
,然后使用它创建真实DOM插入到页面中,所以可以将上一次渲染视图时所创建的vnode缓存起来,之后每当需要重新渲染视图时,将新创建的vnode
和上次缓存的vnode
进行对比,查看它们之间有哪些不一样的地方,找出这些不一样的地方并基于此去修改真实的DOM。
Vue.js
目前对状态的侦测策略采用了中等粒度。当状态发生变化时,只通知到组件级别,然后组件内使用虚拟DOM来渲染视图。
6.3 VNode的类型
vnode的类型有以下几种:
- 注释节点
一个注释节点只有两个有效属性——text
和isComment
,其余属性全是默认undefined或者false - 文本节点
只有一个text
属性 - 元素节点
元素节点通常会存在4种有效属性:
tag
:一个节点的名称;
data
:该属性包含了一些节点上的数据;
children
:当前节点的子节点列表;
context
:它是当前组件的Vue.js 实例 - 组件节点
组件节点和元素节点类似,有以下两个独有的属性:
componentOptions
:组件节点的选项参数,其中包含propsDate、tag和children等信息;
componentInstance
:组件的实例,也是Vue.js的实例。事实上,在Vue.js中,每个组件都是一个Vue.js
实例。 - 函数组件
函数式组件和组件节点类似,它有两个独有属性:functionalContext
和functionalOptions
- 克隆节点
克隆节点是将现有节点的属性复制到新的节点中,让新创建的节点被克隆节点属性保持一致,从而实现克隆效果。它的作用是优化静态节点和插槽节点。
以静态节点为例,当组件内的某个状态发生变化后,当前组件会通过虚拟DOM重新渲染视图,静态节点因为它的内容不会改变,所以除了首次渲染需要执行渲染函数获取vnode之外,后续更新不需要执行渲染函数重新生成vnode。因此,这时就会使用创建克隆节点的方法将vnode克隆一份,使用克隆节点进行渲染。这样就不需要重新执行渲染函数生成新的静态节点的vnode,从而提升一定程度的性能。
克隆节点和被克隆节点之间的唯一区别是isCloned
属性,克隆节点的isCloned
为true,被克隆的原始节点的isCloned
为false
第7章 patch
虚拟DOM最核心的部分是patch
,它可以将vnode
渲染成真实的DOM。
7.1 patch介绍
patch
也可以叫作patching算法
,通过它渲染真实DOM时,并不是暴力覆盖原有DOM,而是比对新旧两个vnode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新,从名字就可以看出,patch
本身就有补丁、修补等意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。patch对现在DOM进行修改需要做三件事:
1、创建新增的节点
(1)当oldVnode
不存在该节点,而Vnode
存在,就需要使用Vnode
生成真实的DOM元素并将其插入到视图当中去。在首次渲染时,DOM中不存在任何节点,所以oldVnode
是不存在,直接使用Vnode
创建元素并渲染视图。
(2)oldVnode
和Vnode
都有节点,但是两个不是相同节点的时候,可以得知Vnode
是一个全新的节点,而oldVnode
就是一个被废弃的节点,此时我们使用Vnode
创建一个新的DOM节点,用它去替换oldVnode
所对应的真实DOM节点。
2、删除已经废弃的节点
(1)当一个节点只在oldVnode
中存在,我们需要把它从DOM中删除。因为渲染视图时,需要以Vnode
为标准,所以Vnode
中不存在的节点都属于被废弃的节点,而被废弃的节点需要从DOM中删除。
(2)节点替换过程,也有删除节点。当oldVnode
和Vnode
不是同一个节点时,在DOM中需要使用Vnode
创建的新节点替换oldVnode
所对于的旧节点,而替换过程是将新创建的DOM节点插入到旧节点的旁边,然后再将旧节点删除,从而完成替换过程。
3、修改需要更新的节点
新旧两个节点是同一个节点,我们需要多者两个节点进行比较细致的比对。比如说,新旧两个节点是同一个文本节点,但是两个文本节点的文本不一样,我们需要重新设置oldVnode
在视图中所对应的真实DOM节点的文本。
小结
接下来是对 ’patch对现在DOM进行修改需要做三件事‘ 的一个详细描述
7.2 创建节点
vnode
只有三种类型的节点会被创建并插入到DOM中:元素节点(判断是否具有tag
属性即可)、注释节点(是否有唯一的标识isComment:true
)和文本节点(前面两种节点都不是,那剩下的就是文本节点)。
如果判断好节点类型之后,则需要调用当前环境下的createElement
方法(在浏览器环境下就是调用document.createElement
)来创建真实的元素节点,接下来将元素插人到指定的父节点中。如果,如果说元素节点还有子节点的话,我们也需要把子节点创建出来插入到指定的元素节点,一起跟着元素节点插入到元素节点的父节点。
如果是文本节点,则调用当前环境下的createTextNode
方法(浏览器环境下调用document.createTextNode
)来创建真实的文本节点并将其插入到指定的父节点中;
如果是注释节点,则调用当前环境下的createComment
方法(浏览器环境下调用document.createComment
方法)来创建真实的注释节点并将其插入到指定的父节点中。
将元素渲染到视图的过程非常简单。只需要调用当前环境下的 appendchild
方法(在浏览器环境下就是调用parentNode . appendChild
),就可以将一个元素插入到指定的父节点中。如果这个指定的父节点已经被渲染到视图,那么把元素插人到它的下面将会自动将元素渲染到视图。
小结
7.3 删除节点
removeVnodes(vnodes,startIdx,endIdx)
方法用于删除vnodes数组
中从startIdx
指定的位置到endIdx
指定位置的内容;
removeNode
方法用于删除视图中的单个节点,而nodeOps
是对节点操作的封装;
为什么要封装?直接删除单个节点不行蛮?
这里涉及到跨平台的知识,跨平台渲染的本质是在设计框架的时候,要让框架的渲染机制和DOM解耦。只要把框架更新DOM时的节点操作进行封装,就可以实现跨平台渲染,在不同平台下调用节点的操作。这也是使用nodeOps封装的原因。
7.4 更新节点
直接上图清楚明了
7.5 更新子节点
对比两个子节点列表( children ),首先需要做的事情是循环。循环newChildren
(新子节点列表),每循环到一个新子节点,就去 oldChildren
(旧子节点列表)中找到和当前节点相同的那个旧子节点。如果在 oldchildren
中找不到,说明当前子节点是由于状态变化而新增的节点,我们要进行创建节点并插入视图的操作;如果找到了,就做更新操作;如果找到的旧子节点的位置和新子节点不同,则需要移动节点等。
这里说下移动优化策略:
通常情况下,并不是所有子节点的位置都会发生移动,一个列表中总有几个节点的位置是不变的。针对这些位置不变的或者说位置可以预测的节点,我们不需要循环来查找,因为我们有一个更快捷的查找方式。我们只需要尝试使用相同位置的两个节点来比较是否是同一个节点:如果恰巧是同一个节点,直接就可以进入更新节点的操作;如果尝试失败了,再用循环的方式来查找节点,我们把这种很快速的查找节点的方式称为快捷查找,那么它共有4中查找方法,分别是:
新前与旧前;
新后与旧后;
新后与旧前;
新前与旧后;
如果这4种方式对比之后都没有找到相同的节点,这是再通过循环的方式去oldChildren中详细找一圈,看看能否找到。循环的条件就是 while(oldStartIdx < = oldEndIdx && newStartIdx <= newEndIndx){}
这里 ’更新子节点‘ 写的比较简单。小编能力有限,太累了。我之前也写过一篇关于diff算法文章,可以两者结合看,可能会比较清楚。叫人头疼得diff算法原理