该方法用来真正对新旧节点进行对比,得到最小应该变化的DOM,然后直接更新DOM。下面是需要patch的几种情况,这几种情况都会有对应的真实DOM测试用例来验证。
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
方法的判断,说明他们节点本身几乎一样,所以新节点可以用旧节点的elm
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
}
2. !vnode.text
能进入到这个条件的,有两种可能:
- vnode是个文本节点,但是文本节点的text为假值
const vnode = { text: 0/false/'' }
- vnode有children子节点
const vnode = { tag: 'div', children: [{...}] }
注意: Vnode对象有很多属性,没有列出来的属性,默认值都是undefined, 所以 !vnode.text === !undefined 会进入到这个逻辑来
也就是说,文本节点和有children子节点是互斥的。
3. oldCh && ch
新旧节点都有子节点,能进入到 patchVnode 方法,说明新旧节点本身是几乎一样的,需要做的就是比较他们的children子节点哪里不同,从而更新DOM
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
}
if (oldCh && ch) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch); // updateChildren 方法有点复杂,是Diff的核心方法
}
最终的页面效果对应的DOM结构
Diff前后对应DOM的Vnode对象
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
方法的逻辑,先放上该方法的一个逻辑框架代码:
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元素
最终的页面效果对应的DOM结构
Diff前后对应DOM的Vnode对象
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' }
},
}
patchVnode逻辑
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中
最终的页面效果对应的DOM结构
Diff前后对应DOM的Vnode对象
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' }]
},
]
}
]
}
patchVnode逻辑
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);
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。