采用虚拟dom,可以避免大量操作dom时引起的性能消耗;虚拟dom,其实使用一个对象来描述dom元素,它包含tag、type、data、children、text等属性;domDiff过程其实是比较两个对象的过程,大致如下
patch
- 节点比对的特点:平级对比
- 比对规则:标签不同,直接替换;如果是文本,直接替换;标签相同,更新属性和孩子
function patch(oldVnode, vnode) {
const isRealElement = oldVnode.nodeType;
if (isRealElement) { // 渲染
const oldElm = oldVnode;
const parentElm = oldVnode.parentNode;
let el = createElm(vnode)
parentElm.insertBefore(el, oldElm.nextsibling);
parentElm.removeChild(oldElm)
return el
}
// 1. 标签不同,找到父级进行替换
if(oldVnode.tag !== vnode.tag) {
oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
}
// 2.都是文本 不同时,进行替换
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
oldVnode.el.textContent = vnode.text
}
}
// 3.标签相同 其他 标签一样 先比较属性,然后比较孩子
let el = vnode.el = oldVnode.el;
updateProperties(vnode, oldVnode.data)
let oldChildren = oldVnode.children || []
let newChildren = vnode.children || []
// 3.1 老和新都有孩子 3.2 老的有 新的没有 3.3 老的没有 新的有
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(el, oldChildren, newChildren)
} else if (oldChildren.length > 0) {// 将老的删除
el.innerHTML = ''
} else if (newChildren.length > 0) {// 将新的插入
for(let i = 0;i<newChildren.length;i++){
let child = newChildren[i]
el.appendChild(createElm(child))
}
}
}
createElm
function createElm(vnode) {
let {tag, children, key, data, text} = vnode
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child))
})
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
updateProperties:更新属性
function updateProperties(vnode, oldProps={}) {
let newProps = vnode.data || {};
let el = vnode.el;
// 2.style不存在,赋值为‘’
let newStyle = newProps.style || {}
let oldStyle = oldProps.style || {}
for (const key in oldStyle) {
if(!newStyle[key]) {
el.style[key] = ''
}
}
// 1. 如果新属性中不存在,就直接删除
for (const key in oldProps) {
if(!newProps[key]) {
delete el[key]
}
}
// 3. 属性存在,进行替换
for (const key in newProps) {
if (key === 'style') {
for (const styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps[key]
} else {
el.setAttribute(key, newProps[key])
}
}
}
updateChildren:更新children
- 正序比较:开头节点相同,新老指针后移;如果新节点比老节点多,将多的节点进行插入;如果老节点比新节点多,删除多余节点
- 倒序比较:结尾节点相同,新老指针迁移;...
- 交叉比较:存在两种情况,新头=老尾 | 老头=新尾
- 乱序比较:将老节点的index和key组成索引表,判断新节点是否存在key;如果存在,就移动;不存在就插入
function updateChildren(parent, oldChildren, newChildren) {
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[oldEndIndex];
function makeIndexByKey(children) { //创建索引表
let map = {};
children.forEach((item, index) => {
item.key && (map[item.key] = index);
});
return map
}
let map = makeIndexByKey(oldChildren)
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
if(!oldStartVnode) { // 老指针向后移动时可能遇到null,就跳过去
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
} else if (isSameNode(oldStartVnode, newStartVnode)) {// 正序 abc -- abcd 从前往后比
patch(oldStartVnode, newStartVnode)
oldStartVnode=oldChildren[++oldStartIndex]
newStartVnode=newChildren[++newStartIndex]
} else if (isSameNode(oldEndVnode, newEndVnode)) { // 倒序 从后往前比 abc -- dabc
patch(oldEndVnode, newEndVnode)
oldEndVnode=oldChildren[--oldEndIndex]
newEndVnode=newChildren[--newEndIndex]
} else if (isSameNode(oldStartVnode, newEndVnode)) { // 老头和新尾一样 abc--bca 交叉比
patch(oldStartVnode, newEndVnode)
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
oldStartVnode=oldChildren[++oldStartIndex]
newEndVnode=newChildren[--newEndIndex]
} else if (isSameNode(oldEndVnode, newStartVnode)) { // 老尾和新头一样 abc--cab 交叉比
patch(oldEndVnode, newStartVnode)
parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
oldEndVnode=oldChildren[--oldEndIndex]
newStartVnode=newChildren[++newStartIndex]
} else {// 两个列表乱序 abcd--eafcn
let moveIndex = map[newStartVnode.key]
if (!moveIndex) { // 新元素在老队列中不存在 直接插入老队列头指针的前面
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el)
} else { // 存在,移动
let moveNode = oldChildren[moveIndex];
patch(moveNode, newStartVnode)
oldChildren[moveIndex] = undefined // 避免数组塌陷
parent.insertBefore(moveNode.el, oldStartVnode.el)
}
newStartVnode = newChildren[++newStartIndex] // 指针后移
}
}
if (newStartIndex <= newEndIndex) { // 新的元素比旧的多就插入;可能是向后插入也可能是向前插入,将元素插入到指针的前面
for(let i = newStartIndex;i<=newEndIndex;i++) {
let ele = newChildren[newEndIndex+1] === null ? null : newChildren[newEndIndex+1].el;
parent.insertBefore(createElm(newChildren[i]), ele)
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex;i<= oldEndIndex; i++) {
let child = oldChildren[i]
if (child != undefined) {
parent.removeChild(child.el)
}
}
}
}
// 比较两个节点是否相同 标签+key都一样
function isSameNode(oldVnode, newVnode) {
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}