仮想domの使用は、パフォーマンスの消費によって引き起こされる操作のdomの多数を避けることができます。仮想domは、実際には、タグ、タイプ、データ、子、テキストおよびその他の属性が含まれているdom要素を記述するオブジェクトを使用して、domDiffのプロセスは、実際には、プロセスの2つのオブジェクトの比較は、おおよそ次のとおりです。
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 旧ノードも新ノードも子ノードを持つ。.2 旧ノードには存在し、新ノードには存在しない。.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存在すれば移動し、存在しなければ挿入します。
- 新しいノードが古いノードより多い場合、より多くのノードが挿入されます。古いノードが新しいノードより多い場合、冗長なノードを削除します。
- 逆順比較:エンドノードは同じ、新旧ポインターは移行;...
- クロス比較:2つのケースが存在、新頭部=旧尾部|旧頭部=新尾部
- 無秩序比較:古いノードのインデックスとキーでインデックス・テーブルを形成し、そのキーが新しいノードに存在するかどうかを判断します。
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 {// 2つのリストがabcd--eafで乱れる
let moveIndex = map[newStartVnode.key]
if (!moveIndex) { // 新しい要素が古いキューに存在しない場合、headポインタの前の古いキューに直接挿入する。
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)
}
}
}
}
// 2つのノードが同じラベルかどうかを比較する+key
function isSameNode(oldVnode, newVnode) {
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}