该方法用来真正对新旧节点进行对比,获得最小应该变化的DOM,而后直接更新DOM。下面是须要patch的几种状况,这几种状况都会有对应的真实DOM测试用例来验证。node
function patchVnode(oldVnode, vnode) { const elm = vnode.elm = oldVnode.elm; const { children: oldCh } = oldVnode; const { children: ch } = vnode; if (!vnode.text) { if (oldCh && ch) { // 新旧节点都有子节点【子节点就是vnode对象中的 children】 } else if (oldCh) { // 旧节点有子节点,而新节点没有子节点 } else if (ch) { // 新节点有子节点,而旧节点没有子节点 } else if (oldVnode.text) { // 旧节点是一个文本节点,可是新节点的文本为空 } } else if (oldVnode.text !== vnode.text) { // 新旧节点都是文本节点,而且文本不同 } }
1. const elm = vnode.elm = oldVnode.elm;
vnode表示新节点,此时是没有elm属性的。而在通过createElm
方法后,vnode.children中的子节点都有了elm属性,此时只有vnode没有elm属性,而能进到 patchVnode 方法来的新旧节点,必定通过了sameVnode
方法的判断,说明他们节点自己几乎同样,因此新节点能够用旧节点的elmapp
if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) }
2. !vnode.text
能进入到这个条件的,有两种可能:框架
const vnode = { text: 0/false/'' }
const vnode = { tag: 'div', children: [{...}] }
注意: Vnode对象有不少属性,没有列出来的属性,默认值都是undefined, 因此 !vnode.text === !undefined 会进入到这个逻辑来测试
也就是说,文本节点和有children子节点是互斥的。spa
3. oldCh && ch
新旧节点都有子节点,能进入到 patchVnode 方法,说明新旧节点自己是几乎同样的,须要作的就是比较他们的children子节点哪里不一样,从而更新DOM3d
if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) }
if (oldCh && ch) { if (oldCh !== ch) updateChildren(elm, oldCh, ch); // updateChildren 方法有点复杂,是Diff的核心方法 }
const app = document.getElementById('app'); const span = document.querySelector('span'); const span_text = span.childNodes[0]; const comment = [...app.childNodes].filter(el => el.nodeType === 8)[0] const ul = document.getElementsByTagName('ul')[0]; const lis = ul.children; const oldVnode = { tag: 'div', data: { attrs: { id: 'app' } }, elm: app, // 旧节点的Vnode对象上都会有一个 elm 属性, 表示该Vnode对应的真实DOM元素 children: [ { tag: 'span', elm: span, children: [{ text: '一去二三里', elm: span_text }] }, { text: '我是一个注释', isComment: true, elm: comment }, { tag: 'ul', elm: ul, children: [ { tag: 'li', elm: lis[0], children: [{ text: 'item1', elm: lis[0].childNodes[0] }] }, { tag: 'li', elm: lis[1], children: [{ text: 'item2', elm: lis[1].childNodes[0] }] }, { tag: 'li', elm: lis[2], children: [{ text: 'item3', elm: lis[2].childNodes[0] }] }, ] } ] }
// 新节点是没有 elm 属性的 const vnode = { tag: 'div', data: { attrs: { id: 'app' } }, children: [ { tag: 'span', children: [{ text: '烟村四五家' }] }, ] }
从图例和新旧vnode中能够看出,他们都有chidlren子节点,因此这种状况,就会进入到 patchVnode
方法的 oldCh && ch
逻辑中来,下面举例说一下 updateChildren
方法的逻辑,先放上该方法的一个逻辑框架代码:code
function updateChildren(parentElm, oldCh, newCh) { let oldStartIdx = 0; let oldEndIdx = oldCh.length - 1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newStartIdx = 0; let newEndIdx = newCh.length - 1; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; let oldKeyToIdx, idxInOld, vnodeToMove, refElm; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (sameVnode(oldStartVnode, newStartVnode)) { // 头头相同 自己位置不动,只用patch子节点,更新子节点DOM便可 } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相同 自己位置不动,只用patch子节点,更新子节点DOM便可 } else if (sameVnode(oldStartVnode, newEndVnode)) { // 旧头 == 新尾 DOM位置须要移动, 从第一个移动到末尾 使用 insertBefore API } else if (sameVnode(oldEndVnode, newStartVnode)) { // 旧尾 == 新头 DOM位置须要移动,从最后一个移动到第一个 } else { // 上面四种都不符合,单个查找 } } if (oldStartIdx > oldEndIdx) { } else if (newStartIdx > newEndIdx) { } }
这就说全部讲 Diff 文章中的头头相同、尾尾相同、旧头===新头....等,刚开始我看到这样的描述时是迷糊的...每种状况我都会以一个例子来讲明
3.1. 新头 === 旧头
意思是: 新节点的头部vnode跟旧节点的头部vnode是近似相等的,须要作的就是比较他们的子节点有什么不一样,从而更新须要更新的子节点DOM。如图:
从图例能够看出,对于头头相等的状况,相同的那个节点(span)在DOM中的位置是不用动的,将旧节点中剩余的子节点(comment、ul)删除便可。对象
4. oldCh
新节点没有,而旧节点有的,须要删除旧节点中的这些DOM元素blog
const oldVnode = { tag: 'div', data: { attrs: { id: 'app' } }, elm: app, children: [ { tag: 'span', elm: span, children: [{ text: '一去二三里', elm: span_text }] }, { text: '我是一个注释', isComment: true, elm: comment }, { tag: 'ul', elm: ul, children: [ { tag: 'li', elm: lis[0], children: [{ text: 'item1', elm: lis[0].childNodes[0] }] }, { tag: 'li', elm: lis[1], children: [{ text: 'item2', elm: lis[1].childNodes[0] }] }, { tag: 'li', elm: lis[2], children: [{ text: 'item3', elm: lis[2].childNodes[0] }] }, ] } ] }
const vnode = { tag: 'div', data: { attrs: { id: 'app' } }, }
function patchVnode(oldVnode, vnode) { const elm = vnode.elm = oldVnode.elm; const { children: oldCh } = oldVnode; const { children: ch } = vnode; if (!vnode.text) { if (oldCh && ch) { } else if (oldCh) { // 旧节点有子节点,而新节点没有子节点 for (const child of oldCh) { if (child) { oldVnode.elm.removeChild(child.elm); } } } else if (ch) { } else if (oldVnode.text) { } } else if (oldVnode.text !== vnode.text) { } }
5. ch
新节点有,而旧节点没有的,须要建立成节点插入到DOM中rem
const oldVnode = { tag: 'div', data: { attrs: { id: 'app' } }, elm: app }
const vnode = { tag: 'div', data: { attrs: { id: 'app' } }, children: [ { tag: 'span', data: { attrs: { class: 'first' } }, children: [{ text: '一去二三里' }] }, { text: '我是一个注释', isComment: true, }, { tag: 'ul', data: { attrs: { class: 'list' } }, children: [ { tag: 'li', children: [{ text: 'item1' }] }, { tag: 'li', children: [{ text: 'item2' }] }, { tag: 'li', children: [{ text: 'item3' }] }, ] } ] }
function patchVnode(oldVnode, vnode) { const elm = vnode.elm = oldVnode.elm; const { children: oldCh } = oldVnode; const { children: ch } = vnode; if (!vnode.text) { if (oldCh && ch) { } else if (oldCh) { } else if (ch) { // 新节点有子节点,旧节点没有 for (const child of ch) { createElm(child, elm, null); // 建立并插入到父元素中 } } else if (oldVnode.text) { } } else if (oldVnode.text !== vnode.text) { } }
function createElm(vnode, parentNode, refNode) { const { text, tag, children, data, isComment } = vnode; if (tag) { vnode.elm = document.createElement(tag); // 生成子节点 createChildren(vnode, children); // 将属性添加上去 if (data) { const { attrs } = data; if (attrs) { for (const k in attrs) { vnode.elm.setAttribute(k, attrs[k]); } } } // 将子节点插入到父节点 insert(parentNode, vnode.elm, refNode); } else if (isComment) { vnode.elm = document.createComment(text); // 新增 注释节点 并添加到其父元素中 insert(parentNode, vnode.elm, refNode); } else { vnode.elm = document.createTextNode(text) // 新增 文本节点 并添加到其父元素中 insert(parentNode, vnode.elm, refNode); } }
function createChildren(vnode, children) { if (Array.isArray(children)) { for (const child of children) { createElm(child, vnode.elm); } } }
function insert(parent, newNode, refNode) { if (parent) { if (refNode) { if (refNode.parentNode === parent) { // 看下图 parent.insertBefore(newNode, refNode); } } else { parent.appendChild(newNode); } } }