一、虚拟DOM
为什么要引入:访问DOM是非常昂贵的,会造成相当多的性能浪费。
引入前:Vue.js 1.0中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对。但因为粒度太细,每一个绑定都会有一个对应的watcher
来观察状态的变化,这样就会有一些内存开销以及一些依赖追踪的开销。当状态被越多的节点使用时,开销就越大。
引入后:Vue.js 2.0中,组件级别是一个watcher
实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher
在观察这个状态的变化。所以当这个状态发生变化时,只能通知到组件,然后组件内部通过虚拟DOM去进行比对与渲染。这是一个比较折中的方案。
模板转换成视图的过程:模板描述状态与DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面。
虚拟DOM的执行流程:(1)提供与真实DOM节点所对应的虚拟节点vnode。(2)将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图。
二、VNode的类型
VNode的类型:注释节点、文本节点、元素节点、组件节点、函数式组件、克隆节点
三、patch
1、patch的作用:对比两个VNode,修改DOM节点
2、patch的过程:新增节点、删除节点、修改节点
(1)新增节点:
- 当oldVnode不存在,直接使用vnode创建元素并渲染视图
- 当oldVnode和vnode完全不是同一个节点时,需要使用vnode生成真实的DOM元素并插入视图
(2)删除节点:当oldVnode和vnode完全不是同一个节点时,需要使用vnode生成真实的DOM元素并插入到旧节点的旁边,并将旧节点删除。
(3)更新节点:当oldVnode和vnode是同一个节点时,使用更详细的对比操作对真实的DOM节点进行更新。
3.创建节点
只有三种类型的节点会被创建并插入到DOM中: 元素节点、注释节点、文本节点
(1)元素节点 :若有tag属性则为元素节点,使用createElement方法创建(递归创建子节点),使用appendChild插入到父节点。
(2)注释节点:若isComment属性为true,则为注释节点,使用createComment创建。
(3)文本节点:当tag属性不存在,并且isComment不为true时,则为文本节点,使用createTextNode创建。
4、更新节点
(1)静态节点:不会因为数据的改变而改变的节点,不需要更新节点如
<p>我是静态节点</p>
(2)新vnode有文本属性时,无论旧节点中是文本节点还是元素节点,都调用setTextContent,将视图中的DOM节点的内容 改为 vnode的text属性所保存的文字。
(3)新vnode是元素节点时
1.若有子节点
- oldVnode也有子节点,则进行比较并更新
- oldVnode没有子节点,则清空变成空标签再将vnode中的children属性挨个创建成真实的DOM元素节点并插入视图中的DOM节点下。
2.若无子节点,说明是个空节点,将oldVnode中的内容(元素或文本)删除,最后达到视图中是空标签的目的。
5、更新策略
(1)创建子节点插入的位置是所有未处理节点的前面,而不是所有处理节点的后面,因为vnode是和oldVnode比较的,如果处理第一步时在真实DOM中添加了一个新节点,再执行第二步时oldVnode中是看不到刚才添加的新节点,所以如果是将这一步的新节点添加在所有处理节点的后面,则会添加在第一步新增节点的前面。这是不对的。
6、判断哪些节点是未处理过的
所有的对比都是针对未处理的节点的,已处理过的节点忽略不计。那么,怎么分辨哪些节点是处理过的,哪些节点是未处理过的呢?
我们的逻辑都是在循环体内处理的,由于前面我们的优化策略,不能从前向后循环,而应该是从两边向中间循环。
准备4个变量:oldStartIdx
、oldEndIdx
、newStartIdx
和newEndIdx
。4个变量分别表示oldChildren
的开始位置的下标(oldStartIdx
)和结束位置的下标(oldEndIdx
),以及newChildren
的开始位置的下标(newStartIdx
)和结束位置的下标(newEndIdx
)。
在循环体内,每处理一个节点,就将下标向指定的方向移动一个位置,通常情况下是对新旧两个节点进行更新操作,就相当于一次性处理两个节点,将新旧两个节点的下标都向指定方向移动一个位置。
开始位置所表示的节点被处理后,就向后移动一个位置;结束位置的节点被处理后,则向前移动一个位置。
当开始位置大于等于结束位置时,说明所有节点都遍历过了,则结束循环:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 做点什么
}
当新子节点和旧子节点的节点数量不一致时,会导致循环结束后仍然有未处理的节点。
- 如果是
oldChildren
先循环完毕,这个时候如果newChildren
中还有剩余的节点(下标在newStartIdx
和newEndIdx
之间),说明这些节点都是需要新增的节点,直接把这些节点插入到DOM中就行了,不需要循环比对了。 - 如果是
newChildren
先循环完毕,这时如果oldChildren
还有剩余的节点(下标在oldStartIdx
和oldEndIdx
之间),这说明oldChildren
中剩余的节点都是被废弃的节点,是应该被删除的节点。这时不需要循环对比就可以知道需要将这些节点从DOM中移除。
7、更新子节点的整体流程