blog

存在する場合は移動し、存在しない場合は挿入する。

逆順比較:終了ノードが同じ場合、新旧ポインタは移動します。...

Mar 17, 2020 · 5 min. read
シェア

仮想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)
}
Read next