目录
一、感受diff算法的心得:
二、深入diff核心思路:
1. diff处理新旧节点不是同一节点时:
(1)如何定义是不是同一个节点:
(2)编写patch.js函数大体框架:
(3)编写createElement.js函数(此内容属于将虚拟DOM 变为真正DOM):
2. diff处理新旧节点是同一个节点时:
3. 手写新旧节点text的不同情况(patchVnode.js函数):
4. 图示经典diff算法优化策略(四种命题查找):
新节点前面索引与旧节点的前面索引(新前旧前)
新节点的后面索引与旧节点的后面索引(新后旧后)
新节点的后面索引与旧节点的前面索引(新后旧前,节点要进行移动)
新节点的前面索引与旧节点的后面索引(新前旧后,节点要进行移动)
情况分析:
(1)新增情况:
(2)多删除情况:
(3)复杂情况:(此时会进行节点的移动)
5. 手写子节点更新策略(updateChildren.js):
三、diff算法完整图解:
1. 最小量更新:
在最小量更新中key很重要,key是这个节点的唯一标识。告诉diff算法在更改前后它们是同一个DOM节点。
2. 虚拟节点:
只有是同一个虚拟节点,才能进行精细比较,否则就是暴力删除旧的,插入新的。
3. 如何定义同一个虚拟节点?:
选择器相同且key相同。
4. 同层比较:
diff算法只进行同层比较,不会进行跨层比较。即使是同一层虚拟节点但是跨层了,那么就不会进行精细算法比较,而是暴力删除旧的,然后插入新的。
老节点的key要和新节点的key相同且新节点的选择器要和老节点的选择器相同。
import vnode from './vnode.js'
import createElement from './createElement.js'
/*
* oldVnode是真实DOM,newVnode是虚拟结点
* */
export default function(oldVnode,newVnode){
// 判断传入的第一个参数,是DOM节点还是虚拟节点?(判断其sel的值)
if(oldVnode.sel ==='' || oldVnode.sel === undefined){
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);
}
// 判断是不是同一个节点key和选择器都要相同
if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key){
console.log("是同一个节点,此为最复杂的情况");
}else{ //如果不是同一个节点,则以旧结点为标杆添加新节点并删除旧节点。
console.log("不是同一个节点,暴力插入新的,删除旧的");
createElement(newVnode,oldVnode.elm);
}
}
功能:真正创建节点,将vnode虚拟节点创建为真是DOM节点,并添加到其虚拟节点vnode的elm属性上。
文本直接添加其值。
内部是子节点那就需要递归创建子节点。首先遍历子元素,递归调用createElement创建节点,并将其添加到父节点上。
// 真正创建节点,将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode){
console.log("目的是把虚拟节点",vnode,"真正变为DOM但是不插入");
let domNode = document.createElement(vnode.sel); //创建虚拟节点的选择器
//有子节点还是有文本?
if(vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)){
// 它内部是文字
domNode.innerText = vnode.text;
}else if(Array.isArray(vnode.children) && vnode.children.length>0){
//它内部是子节点,就要递归创建结点
for(let i = 0;i
再看patch.js函数:
以旧节点为标杆插入新节点并删除老节点。
import vnode from './vnode.js'
//vnode负责创建出虚拟节点
import createElement from './createElement'
import patchVnode from "./patchVnode";
/*
* oldVnode是真实DOM,newVnode是虚拟结点
* */
export default function(oldVnode,newVnode){
// 判断传入的第一个参数,是DOM节点还是虚拟节点?(判断其sel的值)
if(oldVnode.sel ==='' || oldVnode.sel === undefined){
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);
}
// 判断是不是同一个节点key和选择器都要相同
if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key){
console.log("是同一个节点,此为最复杂情况");
}else{ //如果不是同一个节点,则以旧结点为标杆添加新节点并删除旧节点。
//添加新节点,得到真实dom节点
let newVnodeElm = createElement(newVnode);
//插入到老节点之前(上树) elm:此元素对应的真正dom节点
if(oldVnode.elm.parentNode && newVnodeElm){
oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm);
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
}
完善上面的图:在上一个图基础上我们又添加了以下几种情况
根据图示完善patch.js中是同一个节点的情况(提出一个单独的函数patchVnode.js):
patchVnode.js:对新老节点是否是同一个节点进行判断
import patch from "./patch"
import createElement from './createElement'
import updateChildren from './updateChildren'
//对比同一个虚拟节点
export default function patchVnode(oldVnode,newVnode){
//首先判断新旧节点在内存中是否相等
if(oldVnode === newVnode){
console.log("在内存中相等");
}
//判断newVnode有没有text属性
if(newVnode.text != undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
//新节点有text属性
console.log("新Vnode有text属性");
if(newVnode.text!=oldVnode.text){
//如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可
//如果老的elm中是children也会立即消失掉
oldVnode.elm.innerText = newVnode.text;
}
}else{
console.log("新vnode没有text属性");
//新节点没有text属性,意味着newVnode有children
// 现在需要判断oldVnode有没有children
//判断老的有没有children
if(oldVnode.children != undefined && oldVnode.children.length>0){
console.log("老节点有Children此时为最复杂的情况");
}else{
//老节点没有children
//清空老的节点
oldVnode.elm.innerHTML = '';
//oldVnode没有children
for(let i = 0;i
4. 循环条件是 while(新前 <= 新后 && 旧前 <= 旧后),循环结束:只要是旧节点先循环完毕,那么说明新的节点当中是由有剩余节点没有被遍历,那么说明新前指向的节点到新后指向这个节点之间的所有节点是需要新增的节点,直接把这些节点插入到dom中就可以。
首先进行新前与旧前指针比较(命中),则两个指针分别向后移动,若命中接着移动。
移动到新前与旧前不匹配;则进行新后与旧后也不匹配;进行新后与旧前不匹配;进行新前与旧后。
若都没有匹配则用循环进行查找。发现旧节点中有当前新节点中指针指向的节点,那么将此节点的虚拟dom设置成undefined。
然后新前指针向后移动,最后到不符合循环条件,退出。
那么旧前和旧后中间的指针则会被删除掉。
当(3)新后与旧前 命中:
当(4)新前与旧后命中,此时要移动节点。
3. 接着新前往后移,再进行4中命中比较。发现都没有则将遍历老节点,老节点里面也没有则将新前插入到旧前节点的前面。
4. 新节点结束循环后,如果老节点中还有剩余节点,那么旧前和旧后指针中间的节点就是要被删除的节点。
patchVnode.js函数补充:在最复杂的情况中调用(updateChildren.js )
import patch from "./patch"
import createElement from './createElement'
import updateChildren from './updateChildren'
//对比同一个虚拟节点
export default function patchVnode(oldVnode,newVnode){
//首先判断新旧节点在内存中是否相等
if(oldVnode === newVnode){
console.log("在内存中相等");
}
//判断newVnode有没有text属性
if(newVnode.text != undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
//新节点有text属性
console.log("新Vnode有text属性");
if(newVnode.text!=oldVnode.text){
//如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可
//如果老的elm中是children也会立即消失掉
oldVnode.elm.innerText = newVnode.text;
}
}else{
console.log("新vnode没有text属性");
//判断老的有没有children
if(oldVnode.children != undefined && oldVnode.children.length>0){
console.log("老节点有Children此时为最复杂的情况");
updateChildren(oldVnode.elm,oldVnode.children,newVnode.children);
}else{
//新节点没有text属性,意味着newVnode有children
// 现在需要判断oldVnode有没有children
//清空老的节点
oldVnode.elm.innerHTML = '';
//oldVnode没有children
for(let i = 0;i
创建updateChildren.js 函数:
import patchVnode from "./patchVnode.js"
import patch from "./patch";
import createElement from './createElement'
//判断是否是同一个虚拟节点
function checkSameVnode(a,b){
return a.sel == b.sel && a.key == b.key;
}
export default function updateChildren(parentElm,oldCh,newCh){
console.log(oldCh,newCh);
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length-1;
// 新后
let newEndIdx = newCh.length-1;
// 真正的旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
while(oldStartIdx<=oldEndIdx && newStartIdx<=newEndIdx){
//首先不是判断是否命中,而是要略过已经加undefined标记的东西
if(oldStartVnode == null || oldCh[oldStartIdx] == undefined){
oldStartVnode = oldCh[++oldStartIdx];
}else if(oldEndVnode == null || oldCh[oldEndIdx] == undefined){
oldEndVnode = oldCh[--oldEndIdx];
}else if(newStartVnode == null || newCh[newStartIdx] == undefined){
newStartVnode = newCh[++newStartIdx];
}else if(newEndVnode == null || newCh[newEndIdx] == undefined){
newEndVnode = newCh[--newEndIdx];
}else if(checkSameVnode(oldStartVnode,newStartVnode)){
console.log("1命中");
patchVnode(oldStartVnode,newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}else if(checkSameVnode(oldEndVnode,newEndVnode)){
console.log("2命中");
patchVnode(oldEndVnode,newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}else if(checkSameVnode(oldStartVnode,newEndVnode )){
console.log("3命中");
patchVnode(oldStartVnode,newEndVnode);
//进行插入(新后与旧前命中时,此时要移动节点,移动新前指向的这个节点到老节点的旧后(未处理节点)的后面)
//如何移动节点,只要插入一个已经在DOM树上的节点,它就会被移动
parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}else if(checkSameVnode(oldEndVnode,newStartVnode )){
console.log("4命中");
patchVnode(oldEndVnode,newStartVnode);
//进行插入
parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm.previousSibling);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}else{
// 四中都没有命中上
if(!keyMap){
keyMap= {};
//从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
for(let i = oldStartIdx;i<=oldEndIdx;i++){
const key = oldCh[i].key;
if(key != undefined){
keyMap[key] = i;
}
}
}
console.log(keyMap);
// 寻找当前这项(newStartIdx)在keyMap中映射的位置序号
const idxInold = keyMap[newStartVnode.key];
console.log(idxInold);
if(idxInold == undefined){
// 判断,如果idxInold是undefined表示它是全新的项
// 被加入的项(就是newStartVnode这项)现在不是真实dom
parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm);
}else{
// 如果不是undefined,不是全新的项,而是要移动
const elmToMove = oldCh[idxInold];
patchVnode(elmToMove,newStartVnode);
// 移动,把这项设置为undefined,,表示已经处理完这项了
oldCh[idxInold] = undefined;
// 移动 调用insertBdfore也可以实现移动
parentElm.insertBefore(elmToMove.elm ,oldStartVnode.elm);
}
// 指针下移,只移动新的头
newStartVnode = newCh[++newStartIdx];
}
}
// 继续看有没有剩余的
// 新增情况
if(newStartIdx<=newEndIdx){
console.log("new还有剩余节点没有处理,要加项,要把所有剩余的节点都要插入到oldStartIdx之前");
//遍历新的newCh,添加到老的没有处理的之前
for(let i = newStartIdx;i<=newEndIdx;i++){
// insertBefore方法可以自动识别null。如果是null就会自动排到队尾去
// new[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
parentElm.insertBefore(createElement(newCh[i]),oldCh[oldStartIdx].elm);
}
}else if(oldStartIdx<=oldEndIdx){// 删除情况
console.log("old部分还有节点没有处理完");
// 批量删除oldStartIdx和oldEndIdx之间的项
for(let i = oldStartIdx;i<=oldEndIdx;i++){
if(oldCh[i]){
parentElm.removeChild(oldCh[i].elm);
}
}
}
}
注:资料参考《尚硅谷Vue源码系列课程》。本文源码地址:https://gitee.com/c-fff/diff