如上述代码中操作,增加一层节点,将不再进行最小量更新,而是重新构造。
旧节点的key要和新节点的key相同
且
旧节点的选择器要和新节点的选择器相同
patch.js
import vnode from “./vnode”;
import createElement from “./createElement”;
export default function (oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点
if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// 是同一个节点
// TODO:精细化比较
} else {
// 不是同一个节点
createElement(newVnode, oldVnode.elm);
}
};
createElement.js
// 真正创建节点。将vnode创建为DOM,插入到pivot元素之前
export default function (vnode, pivot) {
// 目的是把虚拟节点vnode插入到标杆pivot之前
// 创建一个DOM节点,这个节点目前还是孤儿节点
let domNode = document.createElement(vnode.sel);
// 有子节点还是文本
if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {
// 内部是文字
domNode.innerText = vnode.text;
// 将孤儿节点上树。让标杆节点的父元素调用insertBefore方法,将新的孤儿节点插入到标签节点之前
pivot.parentNode.insertBefore(domNode, pivot);
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
}
};
为了适应递归操作,将插入操作放入到patch.js中,而不是在createElement中进行
这里处理的是diff处理新旧节点不是同一个节点的情况,创建新的插入并暴力删除
index.js
import h from ‘./mySnabbdom/h’;
import patch from ‘./mySnabbdom/patch’
const container = document.getElementById(‘container’);
const btn = document.getElementById(‘btn’);
const myVnode1 = h(‘h1’, {}, ‘你好’);
const myVnode2 = h(‘ul’, {}, [
h(‘li’, {}, ‘A’),
h(‘li’, {}, ‘B’),
h(‘li’, {}, [
h(‘div’, {}, [
h(‘ol’, {}, [
h(‘li’, {}, ‘哈哈哈’),
h(‘li’, {}, ‘嘿嘿嘿’),
h(‘li’, {}, ‘呵呵呵’),
])
])
]),
h(‘li’, {}, ‘D’),
])
const myVnode3 = h(‘section’, {}, [
h(‘h1’, {}, ‘我是新的h1’),
h(‘h2’, {}, ‘我是新的h2’),
h(‘h3’, {}, ‘我是新的h3’),
])
patch(container, myVnode2);
btn.onclick = function () {
patch(myVnode2, myVnode3);
}
createElement.js
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
// console.log(目的是把虚拟节点${vnode}变成真正的DOM
)
// 创建一个DOM节点,这个节点目前还是孤儿节点
let domNode = document.createElement(vnode.sel);
// 有子节点还是文本
if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {
// 内部是文字
domNode.innerText = vnode.text;
// 补充elm属性
vnode.elm = domNode;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内不是子节点,就要递归创建节点
for (let i = 0; i < vnode.children.length; i++) {
// 得到当前的children
let ch = vnode.children[i];
// 创建它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点
let chDom = createElement(ch);
// 上树
domNode.appendChild(chDom);
}
}
// 补充elm属性
vnode.elm = domNode;
// 返回elm,elm是一个纯DOM对象
return vnode.elm;
};
patch.js
import vnode from “./vnode”;
import createElement from “./createElement”;
export default function (oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点
if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// 是同一个节点
// TODO:精细化比较
} else {
// 不是同一个节点
let newVnodeElm = createElement(newVnode);
// 插入到老节点之前
if (oldVnode.elm.parentNode && newVnodeElm)
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
};
patch.js
import vnode from “./vnode”;
import createElement from “./createElement”;
export default function (oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点
if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
console.log(‘是同一个节点’)
// 判断新旧node是否是同一个对象
if (oldVnode === newVnode) return;
// 判断newVnode有没有text属性
if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
// 新vnode有text属性
// console.log(‘新vnode有text属性’)
if (newVnode.text !== oldVnode.text)
// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉
oldVnode.elm.innerText = newVnode.text;
} else {
// 新vnode没有text属性,有children
// console.log(‘新vnode没有text属性’)
// 判断老的有没有children
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// 老的有children,此时就是最复杂的情况。就是新老都有children
} else {
// 老的没有children,新的有children
// 清空老的节点的内容
oldVnode.elm.innerHTML = ‘’;
// 遍历newVnode的子节点,创建dom,循环上树
newVnode.children.forEach(node => {
let dom = createElement(node);
oldVnode.elm.appendChild(dom);
})
}
}
} else {
// 不是同一个节点
let newVnodeElm = createElement(newVnode);
// 插入到老节点之前
if (oldVnode.elm.parentNode && newVnodeElm)
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
};
如果data中存在key,将key也绑定在vnode上
vnode.js
/**
- vnode函数的功能非常简单,就是把传入的5个参数组合对象返回
*/
export default function (sel, data, children, text, elm) {
const key = data.key;
return {
sel, data, children, text, elm, key
};
}
patchVnode.js
import createElement from “./createElement”;
export default function patchVnode(oldVnode, newVnode) {
// 判断新旧node是否是同一个对象
if (oldVnode === newVnode) return;
// 判断newVnode有没有text属性
if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
// 新vnode有text属性
// console.log(‘新vnode有text属性’)
if (newVnode.text !== oldVnode.text)
// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉
oldVnode.elm.innerText = newVnode.text;
} else {
// 新vnode没有text属性,有children
// console.log(‘新vnode没有text属性’)
// 判断老的有没有children
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// 老的有children,此时就是最复杂的情况。就是新老都有children
// 所有未处理的节点的开头
let un = 0;
for (let i = 0; i < newVnode.children.length; i++) {
let ch = newVnode.children[i];
// 再次遍历,看看oldVnode中有没有节点和它是same的
let isExist = false;
for (let j = 0; j < oldVnode.children.length; j++) {
if (oldVnode.children[j].sel === ch.sel && oldVnode.children[j].key === ch.key) {
isExist = true;
}
}
if (!isExist) {
let dom = createElement(ch);
ch.elm = dom;
if (un < oldVnode.children.length)
oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm);
else
oldVnode.elm.appendChild(dom);
} else {
// 让处理的节点下移一位
un++;
// 判断前后节点位置是否一致
}
}
} else {
// 老的没有children,新的有children
// 清空老的节点的内容
oldVnode.elm.innerHTML = ‘’;
// 遍历newVnode的子节点,创建dom,循环上树
newVnode.children.forEach(node => {
let dom = createElement(node);
oldVnode.elm.appendChild(dom);
})
}
}
}
如下面三种情况,所有情况非常复杂,杂糅在一起,因此需要一个优秀的更新算法
新增的情况
新创建的节点要插入到所有未处理节点之前,而不是所有已处理节点之后。
例如途中,如果插入MN,所有已处理节点为AB,则MN会依次插入到B之后,与目的不一致。
删除的情况
更新的情况
四种命中查找:
新前:新的虚拟节点当中的所有没有处理的开头的节点
新后:新的虚拟节点当中的所有没有处理的最后的节点
旧前:旧的虚拟节点当中的所有没有处理的开头的节点
旧后:旧的虚拟节点当中的所有没有处理的最后的节点
经典的diff算法优化策略
-
新前与旧前:如果是旧节点先循环完毕,说明新节点中有要插入的节点。
-
新后与旧后:如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和新后指针中间的节点),说明他们是要被删除的节点。
-
新后与旧前:(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧后之后)
-
新前与旧后:(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧前之前)
命中一种就不再进行命中判断了
如果都没有命中,就需要用循环来寻找了。移动到oldStartIdx之前。
updateChildren.js
import patchVnode from “./patchVnode”;
import createElement from “./createElement”;
// 判断是否是同一个虚拟节点
function checkSameVnode(vnodeA, vnodeB) {
return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key;
}
export default function updateChildren(parentElm, oldCh, newCh) {
// console.log(‘我是updateChildren’)
// console.log(oldCh, newCh)
// 定义旧前、新前、旧后、新后 编号
let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1;
// 定义旧前、旧后、新前、新后 节点
let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx],
newEndVnode = newCh[newEndIdx];
let keyMap = null;
// 开始大while
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(newStartVnode, oldStartVnode)) {
// 新前与旧前
console.log(“①新前和旧前命中”)
patchVnode(oldStartVnode, newStartVnode);
newStartVnode = newCh[++newStartIdx];
oldStartVnode = oldCh[++oldStartIdx];
} else if (checkSameVnode(newEndVnode, oldEndVnode)) {
// 新后与旧后
console.log(“②新后与旧后命中”)
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[–oldEndIdx];
newEndVnode = newCh[–newEndIdx];
} else if (checkSameVnode(newEndVnode, oldStartVnode)) {
// 新后与旧前
console.log(“③新后与旧前命中”)
patchVnode(oldStartVnode, newEndVnode);
// 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
// 如何移动节点??只要插入一个已经在DOm树上的节点,它就会被移动
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
newEndVnode = newCh[–newEndIdx];
oldStartVnode = oldCh[++oldStartIdx];
} else if (checkSameVnode(newStartVnode, oldEndVnode)) {
// 新前与旧后
console.log(“④新前与旧后命中”)
patchVnode(oldEndVnode, newStartVnode);
// 当④新前与旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
// 如何移动节点??只要插入一个已经在DOm树上的节点,它就会被移动
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
oldEndVnode = oldCh[–oldEndIdx];
} else {
// 四种命中都没有命中
// 制作key的map映射对象,这样就不用每次都遍历老对象了。
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);
// 寻找当前这项(nextStartIdx)这项在keyMap中的映射的位置序号
const idxInOld = keyMap[newStartVnode.key];
// console.log(idxInOld);
if (idxInOld === undefined) {
// 判断,如果idxInOlx是undefined表示它是全新的项,只需要插入即可
// 被加入的项(就是newStartVnode这项),目前还不是真实DOM节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// 如果不是undefined,需要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// 把这项设置为undefined,表示已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动,调用insertBefore也可以实现移动
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// 指针下移,只移新的头
newStartVnode = newCh[++newStartIdx];
}
}
// while结束后,需要继续看看有没有剩余,判断是否需要删除或新增节点
if (newStartIdx <= newEndIdx) {
// console.log(‘new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStart之前’);
// 循环结束后,start还是比old小
// before是插入的标杆
// const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
// 遍历新的newCh,添加到老的没有处理的之前
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
// newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
// parentElm.insertBefore(createElement(newCh[i]), before);
parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
}
} else if (oldStartIdx <= oldEndIdx) {
// console.log(‘old还有剩余节点没有处理,要删项’)
// 批量删除oldStart和oldEnd指针之间的项
for (let i = oldStartIdx; i <= oldEndIdx; i++)
if (oldCh[i]) parentElm.removeChild(oldCh[i].elm);
}
};
================================================================
import vnode from “./vnode”;
import createElement from “./createElement”;
import patchVnode from “./patchVnode”;
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点
if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// console.log(‘是同一个节点’)
patchVnode(oldVnode, newVnode);
} else {
// 不是同一个节点
let newVnodeElm = createElement(newVnode);
// 插入到老节点之前
if (oldVnode.elm.parentNode && newVnodeElm)
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
};
/**
- vnode函数的功能非常简单,就是把传入的5个参数组合对象返回
*/
export default function (sel, data, children, text, elm) {
const key = data.key;
return {
sel, data, children, text, elm, key
};
}
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
// console.log(目的是把虚拟节点${vnode}变成真正的DOM
)
// 创建一个DOM节点,这个节点目前还是孤儿节点
let domNode = document.createElement(vnode.sel);
// 有子节点还是文本
if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {
// 内部是文字
domNode.innerText = vnode.text;
// 补充elm属性
vnode.elm = domNode;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内不是子节点,就要递归创建节点
for (let i = 0; i < vnode.children.length; i++) {
// 得到当前的children
let ch = vnode.children[i];
// 创建它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点
let chDom = createElement(ch);
// 上树
domNode.appendChild(chDom);
}
}
// 补充elm属性
vnode.elm = domNode;
// 返回elm,elm是一个纯DOM对象
return vnode.elm;
};
import createElement from “./createElement”;
import updateChildren from “./updateChildren”;
// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
// 判断新旧node是否是同一个对象
if (oldVnode === newVnode) return;
// 判断newVnode有没有text属性
if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
// 新vnode有text属性
// console.log(‘新vnode有text属性’)
if (newVnode.text !== oldVnode.text)
// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉
oldVnode.elm.innerText = newVnode.text;
} else {
// 新vnode没有text属性,有children
// console.log(‘新vnode没有text属性’)
// 判断老的有没有children
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// 老的有children,新的也有children,此时就是最复杂的情况。就是新老都有children
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
} else {
// 老的没有children,新的有children
// 清空老的节点的内容
oldVnode.elm.innerHTML = ‘’;
// 遍历newVnode的子节点,创建dom,循环上树
newVnode.children.forEach(node => {
let dom = createElement(node);
oldVnode.elm.appendChild(dom);
})
}
}
}
import vnode from “./vnode”;
/*
-
编写一个低配版本的h函数,这个函数必须要接收3个参数,缺一不可 —— 重载功能较弱
-
也就是说,调用的时候形态必须是下面三种之一:
-
形态① h(‘div’, {}, ‘文字’)
-
形态② h(‘div’, {}, [])
-
形态③ h(‘div’, {}, h())
-
*/
export default function (sel, data, c) {
// 检查参数的个数
if (arguments.length !== 3)
throw new Error(‘对不起,h函数必须传入3个参数,我们是低配版h函数’);
// 检查参数c的类型
if (typeof c === ‘string’ || typeof c === ‘number’) {
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray©) {
// 说明现在调用h函数是形态②
let children = [];
// 遍历c,手机children
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象
if (!(typeof c[i] === ‘object’ && c[i].hasOwnProperty(‘sel’)))
throw new Error(‘传入的数组参数中有项不是h函数’);
// 这里不用执行c[i],因为测试语句中已经执行了
// 只需要收集好children
children.push(c[i]);
}
// 循环结束了,说明children收集完毕了,此时可以返回虚拟节点,有children节点
return vnode(sel, data, children, undefined, undefined);
} else if (typeof c === ‘object’ && c.hasOwnProperty(‘sel’)) {
// 说明现在调用h函数是形态③
// 即传入的c是唯一的children. 不用执行c,因为测试语句中已经执行了c
算法刷题
大厂面试还是很注重算法题的,尤其是字节跳动,算法是问的比较多的,关于算法,推荐《LeetCode》和《算法的乐趣》,这两本我也有电子版,字节跳动、阿里、美团等大厂面试题(含答案+解析)、学习笔记、Xmind思维导图均可以分享给大家学习。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
写在最后
最后,对所以做Java的朋友提几点建议,也是我的个人心得:
-
疯狂编程
-
学习效果可视化
-
写博客
-
阅读优秀代码
-
心态调整