一、三个过程
- 用JavaScript模拟DOM树,并渲染这个DOM树
- 比较新老DOM树,得到比较的差异对象-----diff
- 把差异对象应用到渲染的DOM树。------patch:把这些变化用打补丁的方式更新到真实 dom 上去(其实第二步也属于patch)
patch:先找到差异对象(diff),再对其进行节点更新,就是创建新节点、移除等操作,将VDOM渲染成真正的DOM然后插入到容器里面
有一个问题:那个老dom树就是真实dom树吗,对其patch就是对真dom树直接进行操作吗?我觉得可以是真实dom树,如果是真实dom树,那就是边diff边更新 有提到一点点
如果oldVnode是真实DOM节点或hydrating设置为true,需要用hydrate函数将虚拟DOM和真实DOM进行映射(下面),
然后将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点,根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置
- 具备跨平台的优势(由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力)
- 提升渲染性能
二、用JavaScript模拟DOM树,并渲染这个DOM树(就是渲染到页面上)
虚拟DOM,就是用JS对象结构的一种映射。用JS很容易模拟一个DOM树的结构。
- Vue.js通过编译将template 模板转换成渲染函数(render) ,执行渲染函数就可以得到一个虚拟节点树
render
函数的功能是把节点创建好,然后设置节点属性,最后递归创建。这样子我们就得到一个DOM树,然后插入(appendChild)到页面上。这里oldVnode就是真实node了。
三、patch
patch ,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。patch 的核心----diff 算法,找出本次DOM需要更新的节点来更新,其他的不更新
节点类型:相同的节点类型
- oldVnode: 旧的虚拟节点或旧的真实dom节点
- vnode: 新的虚拟节点
- elm 对应dom元素
oldCh
为oldVnode
的子节点,newCh为Vnode
的子节点- sameVnode():节点相同,就是基本属性相同的情况
- hydrating: 是否要跟真实dom混合
patch过程步骤: (应该说patch过程)
- 如果vnode不存在但oldVnode存在,则表示要移除旧的node,那么就进行销毁
- 如果oldVnode不存在但是vnode存在,说明是要创建新节点,那么就调用createElm来创建新节点
- 当vnode和oldVnode都存在时:
- 如果oldVnode与Vnode是同一节点,即基本属性相同的情况。基本属性相同的情况下才认为这个2个
node
只是局部发生了更新,然后才会对这2个node
进行diff
- 如果2个
node
的基本属性存在不一致的情况,那么就会直接跳过diff
的过程,进而依据vnode
新建一个真实的dom,同时删除老的dom
节点。 - 注:基本属性有:
tag
属性即这个vnode
的标签属性、data
属性包含了最后渲染成真实dom
节点后,节点上的class
,attribute
,style
以及绑定的事件、key
属性是vnode
的标记,在diff
过程中可以提高diff
的效率等。就是元素节点、属性节点、文本节点相同
- 如果oldVnode与Vnode是同一节点,即基本属性相同的情况。基本属性相同的情况下才认为这个2个
diff过程(patchVnode函数):
- 首先进行文本节点的判断,若
oldVnode.text !== vnode.text
,那么就会直接进行文本节点的替换;- 注:
其实就是上面的3.2,但文本节点的替换比新建更少耗性能,因为元素节点->属性节点->文本节点
- 注:
- 在
vnode
没有文本节点的情况下,进入子节点的diff
;- 当
oldCh
和ch
都存在且不相同的情况下,调用updateChildren
对子节点进行diff
; - 若
oldCh
不存在,ch
存在,首先清空oldVnode
的文本节点,同时调用addVnodes
方法将ch
添加到elm
真实dom
节点当中; - 若
oldCh
存在,ch
不存在,则删除elm
真实节点下的oldCh
子节点; - 若
oldVnode
有文本节点,而vnode
没有,那么就清空这个文本节点。
- 当
updateChildren(
是整个diff
过程中最重要的环节:):不带key
- 四个指针
- 如果这四个指针没有一对相同的节点,就把
newCh
的startVnode
被添加到oldStartVnode
的前面,同时newStartIndex
前移一位; - 如果这四个指针有一对相同的节点,即满足sameVnode,就对这2个
node
进行diff
,最后将patch
打到oldStartVnode
上,同时这一对的指针移动一位; - 遍历结束条件:
oldCh
或者newCh
的startIndex >= endIndex。如果这个时候newStartIdx > newEndIdx
,说明此时oldCh
存在多余的节点,那么最后就需要将这些多余的节点删除。
updateChildren:带key
- 当为
vnode
引入key
属性后,在每一轮的diff
过程中,当起始
和结束
节点都没有找到sameVnode
时,首先对oldCh
中进行key
值与索引的映射。用以将oldCh
中的key
属性作为键
,而对应的节点的索引作为值
- 然后再判断在
newStartVnode
的属性中是否有key
,且是否在oldKeyToIndx
中找到对应的节点- 上面两点的意思是:先给
oldCh的每一个节点一个
key值,然后再看newStartVnode这一个节点的key有没有在oldCh的key集合中。
- 如果不存在这个
key
,那么就将这个newStartVnode
作为新的节点创建且插入到原有的root
的子节点中 - 如果存在这个
key
,那么就取出oldCh
中的存在这个key
的vnode
,然后再进行diff
的过程(放到该放的地方)
- 上面两点的意思是:先给
- 指针移动,再次判断,直到遍历结束。
四、其他
1、v-once指令:这样模板只会在第一次更新时显示数据,此后再次更新该DOM里面引用的数据时,内容不会自动更新了。Vue内部把模板缓存起来了,把v-once对应的节点当作一个静态节点来看待,而不是一个响应式的数据(没有经过Object.defineproperty处理)。以后的更新渲染都只是从缓存中拿出结果而已。
2、如果旧节点就是真实dom树,那在更新的过程中就通过createElm、删除等操作完成了
如果旧节点还是vdom,则再将其转为真实dom树,如下:
模拟的JS进行转化为真实的DOM:通过递归
const createElement = (vnode) => {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || [];
if(!tag) {
return null;
}
//创建元素
let elem = document.createElement(tag);
//属性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
//子元素
children.forEach(childVnode => {
//给elem添加子元素
elem.appendChild(createElement(childVnode));
})
//返回真实的dom元素
return elem;
}