我们希望是在更新的时候通过新渲染的虚拟DOM树和旧的虚拟DOM树进行对比,记录这两颗树的差异。记录下来的不同就是我们需要对页面真正的DOM操作,然后把它们渲染在真正的DOM结构上,页面就对应的变化了。
上篇文章《虚拟DOM怎样进化为实在DOM》中讲到了怎样经过虚拟DOM树转化为实在DOM烘托到页面中。可是在烘托的进程中,咱们直接将新的虚拟DOM树转化成实在DOM替换掉旧的DOM结构。当实在的DOM中的状况或许内容发生改变的时分,从头烘托新的虚拟DOM树再替换掉旧的,这样的话会显得很无力。想象一种情形,当咱们对整个DOM结构中仅仅修改了一个小的数据乃至是一个标点符号的时分或许数据量很大的时分,咱们要把本来旧的DOM结构悉数替换掉,这样的话对计算机而言太糟蹋功能了。
故咱们期望是在更新的时分经过新烘托的虚拟DOM树和旧的虚拟DOM树进行比照,记载这两颗树的差异。记载下来的不同便是咱们需求对页面实在的DOM操作,然后把它们烘托在实在的DOM结构上,页面就对应的改变了。这样就完结了:看似视图悉数结构得到了最新的烘托,可是终究操作DOM结构的时分仅仅改变了与原结构不同的当地。
即虚拟DOM的diff算法的主体思路是:
1.将虚拟DOM结构转化为实在的DOM结构替换到旧的DOM(第一次旧的为undefined),烘托到页面中。
2.当状况改变的时分,新烘托一颗虚拟DOM树和本来旧的虚拟DOM树比照,比照之后记载下差异。
3.将终究由差异的部分转化成实在DOM结构烘托到页面上。
完结
在旧的虚拟节点和新的虚拟节点的比照进程中会呈现以下几种状况,下面咱们以Vue为例看Vue2.0是Diff算法是怎样完结的:
比较两个元素的标签
假设标签不相同的话直接替换掉,例如:div变成p
- div->p
- <<<<<<<HEAD
- <p>前端简报</p>
- =========
- <div>前端简报</div>
- >>>>>>>>
判别虚拟节点的tag特点是否持平,假设不持平将新的虚拟DOM树转化为实在DOM结构把本来节点替换掉
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
效果图:
比较两个元素的文本
当标签相同的时分比较文本是否相同。假设文本不相同的话那么直接替换掉文本内容。
- <<<<<<<HEAD
- <div>前端</div>
- =========
- <div>简报</div>
- >>>>>>>>
两个节点的tag都是div,故比较孩子虚拟DOM树的是否相同,孩子的tag为undefined阐明是文本节点,此刻比较本文内容text是否共同即可
- if(!oldVnode.tag){
- //文本的比照
- if(oldVnode.text!=vnode.text){
- return(oldVnode.el.textContent=vnode.text);
- }
- }
效果图:
比较标签特点
假设两个标签相同那么比较标签的特点,当特点更新的时分经过新旧特点的比照会呈现下面几种状况:
1、特点比照
假设旧的虚拟节点有,新的虚拟节点没有那么需求删去旧的虚拟节点上的特点。
- letnewProps=vnode.data||{};//新的特点
- letel=vnode.el;
- //老的有新的没有需求删去特点
- for(letkeyinoldProps){
- if(!newProps[key]){
- el.removeAttribute(key);//移除实在dom的特点
- }
- }
反过来,假设旧的虚拟节点没有,新的虚拟节点有那么直接设置新的特点即可
- //新的有那就直接用新的去更新即可
- for(letkeyinnewProps){
- el.setAttribute(key,newProps[key]);
- }
- 对应的源码地址:src\platforms\web\runtime\modules\attrs.js
2、款式处理
假设老的款式中存在新的款式没有那么删去老的款式。
- -style={color:red}
- +style={background:red}
- letnewStyle=newProps.style||{};
- letoldStyle=oldProps.style||{};
- //老的款式中有的新的没有删去老的款式
- for(letkeyinoldStyle){
- if(!newStyle[key]){
- el.style[key]="";
- }
- }
相反假设老的款式没有,新的款式存在那么直接更新新的款式即可
- for(letkeyinnewProps){
- if(key=="style"){
- for(letstyleNameinnewProps.style){
- el.style[styleName]=newProps.style[styleName];
- }
- }
- }
- 对应的源码地址:src\platforms\web\runtime\modules\style.js
3、类名处理
关于类名处理咱们运用新节点的类名
- -class="titleant-title"
- +class="titleant-mian-title"
0
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
- 对应的源码地址src\platforms\web\runtime\modules\class.js
比较儿子
在比较儿子的进程中能够分为以下几种状况:
1、老节点有儿子,新节点没有儿子删去老节点的儿子即可
1
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
2、老节点没有儿子,新节点有儿子遍历children转化为实在的DOM结构添加到页面中
2
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
3、老节点有儿子,新节点有儿子
当老节点的儿子和新节点的儿子都存在而且不持平的时分,这种状况比较杂乱也是diff算法的中心。
在vue2.0中比较老节点和新节点差异的时分选用了双指针的办法,经过一起向同一个方向循环老节点和新节点,只需有一个节点循环完结果完毕循环。假设是老节点先完毕,那么将新节点剩下的元素添加到烘托列表;假设是新节点先完毕,那么将旧节点剩下的元素删去即可。
界说最初指针其间包含老节点的开端方位和完毕方位,新节点的开端方位和完毕方位。
3
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
经过判别两个节点的key和tag是否持平来确认同一元素
4
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
正序摆放
假设剩余的节点的右边的话,那么从左往右顺次判别老的开端节点和新的开端节点是否是同一节点,假设是同一节点调用patchVode办法去递归子节点,将老节点和新节点的下标加1向右移动,直到下标大于children的长度。
5
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
效果图:
假设是新节点剩余添加到烘托视图,如上图从左到右比照时,g节点的下一个el是null,insertBefore适当于appendChild办法向后刺进;假设是从右向左,g节点的下一个el是a,那么选用insertBefore适当于向a前面刺进节点。
6
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
假设是老节点剩余,那么阐明这些节点是不需求的,删去掉即可,假设在删去的进程中呈现null,阐明这个节点现已处理过了越过即可。
7
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
假设剩余的节点在左面,从头老节点的完毕节点开端下标顺次减1
8
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
回转摆放
假设遇到新老节点回转的状况,经过老节点的开端节点和新节点的完毕节点作比照或许老节点和完毕节点和新节点的开端节点作比照。
假设老节点的开端节点和新节点的完毕节点是同一节点,那么将老的开端节点刺进到老的完毕节点的下一个节点之前,然后顺次别离向右向左移动节点对应的下标,获取对应的值持续遍历。
9
- if(oldVnode.tag!=vnode.tag){
- returnoldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
- }
假设老节点的完毕节点和新节点的开端节点是同一节点吗,那么将老节点的完毕节点刺进到老节点的开端节点前面,然后顺次别离向左向右移动节点对应的下标,获取对应的值持续遍历。
0
- <<<<<<<HEAD
- <div>前端</div>
- =========
- <div>简报</div>
- >>>>>>>>
毫无联系摆放
假设在比照的进程中儿子之间没有任何的联系,经过从头节点的开端节点开端顺次和老节点的一切节点作比照,假设没有相同的就创立新的节点刺进的老节点的开端节点之前,假设在循环的进程中找到了相同的元素,那么直接复用老元素,将和新节点相同的老节点刺进到老节点的开端节点之前,为了避免数组的陷落问题,将移走的老节点的方位设为undefined,终究将剩余的老节点悉数删去即可。
设置缓存组运用老节点的key和下标做一个映射表,新节点的key去老的映射表里挑选,假设没有挑选到,那么就不复用直接创立新节点刺进到老节点的开端节点之前。
1
- <<<<<<<HEAD
- <div>前端</div>
- =========
- <div>简报</div>
- >>>>>>>>
假设在老节点中找到,那么移动老节点到老节点开端节点之前
2
- <<<<<<<HEAD
- <div>前端</div>
- =========
- <div>简报</div>
- >>>>>>>>
在移动的进程中开端指针和完毕指针或许存在指向null的状况,假设指向null的话那么无法在进行比较,能够直接越过,指向下一个元素即可。
3
- <<<<<<<HEAD
- <div>前端</div>
- =========
- <div>简报</div>
- >>>>>>>>
源码地址:src/core/vdom/patch.js
为什么要运用key?
人丑话不多先看图
有key
没有key
如上图所示,第一个图为有key的状况,第二个图为没有key的状况,能够很明显的看到所展现内容假设有key的话,复用了key为A,B,C,D的4个节点,成果仅仅将新创立的E节点刺进到C节点的前面完结烘托。假设没有key的话,那么创立了E,C,D三个节点,降低了复用率,功能方面必定没有有key 的状况高。
为什么不能用index作为key呢?
平常开发进程中,假设仅仅经过页面静态烘托是能够运用index作为key的,假设在页面上有杂乱的逻辑改变,那么运用index作为key适当于没有key。
4
- <<<<<<<HEAD
- <div>前端</div>
- =========
- <div>简报</div>
- >>>>>>>>
如上代码所示,将下标为0和2的A和C变换方位之后需求从头创立节点A和C,此刻C的下标为0,A的下标为2。而以id或许仅有标识作为key的话,适当所以将A和C元素的方位进行平移。平移的功能比创立节点的功能高。
在运用index作为key的时分还会发生意想不到的问题,假设咱们把B节点删去,咱们最开端取值为B,现在取值变成了C。
总结
Vue2.0的diff算法pathVode办法的基本思路能够总结为以下几点:
1.判别oldVode和newVode是否是同一目标,假设是的话直接return。2.是界说实在DOM为el。
3.假设oldVode和newVode都有文本节点而且不持平,那么将old的文本节点设置为newVode的文本节点。
4.假设oldVode有子节点newVode没有,那么删掉子节点。
5.假设oldVode没有子节点newVode有。那么将子节点转化为实在DOM添加到el中。6.假设都有子节点,那么履行updateChildren函数比较子节点
以上便是Diff算法的整个进程,它对整个Vue烘托进程的功能有着至关重要的效果。
知优网 » 「源码分析」怎么完成一个虚拟DOM算法(虚拟dom实现原理)