前言
上篇文章咱们了解到了虚拟节点的大概思路,其实就是利用递归将一层层的dom节点用对象来表示。但是有前提,就是一开始渲染的时候可以那么写,或是第一次挂载的时候。那在更新的时候又该怎么处理呢,这时就需要用了diff算法了。
patch() 方法
- (1). 类型不同
/**
* 补丁方法,更新标签
* @param {*} oldVnode 旧标签
* @param {*} newVnode 新标签
*/
function patch(oldVnode, newVnode){
// 如果标签不一致直接替换成新节点
if(oldVnode.type !== newVnode.type){
return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode), oldVnode.domElement);
}
}
测试下我们写的代码:
let oldVD = createElement(
'div',
{id: 'testDom', data: '测试属性', key: 'testKey'},
createElement('span', {style: {color: 'red'}}, '我是span内容'),
'测试内容'
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'p',
{},
'我是新内容'
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒后标签内容换成了新的标签内容
- (2). 类型相同且是文本
/**
* 补丁方法,更新标签
* @param {*} oldVnode 旧标签
* @param {*} newVnode 新标签
*/
function patch(oldVnode, newVnode){
// 如果标签不一致直接替换成新节点
if(oldVnode.type !== newVnode.type){
return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode), oldVnode.domElement);
}
// 类型相同 ==> 文本
if(oldVnode.text){
if(oldVnode.text === newVnode.text) return;
return oldVnode.domElement.textContent = newVnode.text;
}
}
- (3). 类型相同且是标签
/**
* 补丁方法,更新标签
* @param {*} oldVnode 旧标签
* @param {*} newVnode 新标签
*/
function patch(oldVnode, newVnode){
// 如果标签不一致直接替换成新节点
if(oldVnode.type !== newVnode.type){
return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode), oldVnode.domElement);
}
// 类型相同 ==> 文本
if(oldVnode.text){
if(oldVnode.text === newVnode.text) return;
return oldVnode.domElement.textContent = newVnode.text;
}
// 类型相同 ==> 是标签:需要根据新节点的属性更新老节点的属性
let domElement = newVnode.domElement = oldVnode.domElement;
// 根据最新的虚拟节点更新老节点的属性
updateProperties(newVnode, oldVnode.props)
}
测试下我们写的代码:
let oldVD = createElement(
'div',
{id: 'testDom', style:{color: 'blue'}},
'测试内容'
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'div',
{id: 'testDom', style:{color: 'yellow'}},
'测试内容'
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒后内容由蓝色变成了黄色
上述测试只是新旧节点的属性不同,由新节点的属性更新就节点的属性即可。但还有复杂的子节点的比对。这时又分为三种情况:
- (3.1). 类型相同且是标签 -- 老的有子节点,新的没有
/**
* 补丁方法,更新标签
* @param {*} oldVnode 旧标签
* @param {*} newVnode 新标签
*/
function patch(oldVnode, newVnode){
// 如果标签不一致直接替换成新节点
if(oldVnode.type !== newVnode.type){
return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode), oldVnode.domElement);
}
// 类型相同 ==> 文本
if(oldVnode.text){
if(oldVnode.text === newVnode.text) return;
return oldVnode.domElement.textContent = newVnode.text;
}
// 类型相同 ==> 是标签:需要根据新节点的属性更新老节点的属性
let domElement = newVnode.domElement = oldVnode.domElement;
// 根据最新的虚拟节点更新老节点的属性
updateProperties(newVnode, oldVnode.props);
let oldChildren = oldVnode.children; // 老子标签
let newChildren = newVnode.children; // 新子标签
// 3.1. 老的有子节点,新的没有
if(oldChildren.length > 0 && newChildren.length == 0){
domElement.innerHTML = ''
}
}
测试我们写的代码
let oldVD = createElement(
'div',
{id: 'testDom', style:{color: 'blue'}},
'测试内容'
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'div',
{id: 'testDom', style:{color: 'yellow'}}
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒钟后内容被清空了
- (3.2). 类型相同且是标签 -- 老的没子节点,新的有即新增
/**
* 补丁方法,更新标签
* @param {*} oldVnode 旧标签
* @param {*} newVnode 新标签
*/
function patch(oldVnode, newVnode){
// 如果标签不一致直接替换成新节点
if(oldVnode.type !== newVnode.type){
return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode), oldVnode.domElement);
}
// 类型相同 ==> 文本
if(oldVnode.text){
if(oldVnode.text === newVnode.text) return;
return oldVnode.domElement.textContent = newVnode.text;
}
// 类型相同 ==> 是标签:需要根据新节点的属性更新老节点的属性
let domElement = newVnode.domElement = oldVnode.domElement;
// 根据最新的虚拟节点更新老节点的属性
updateProperties(newVnode, oldVnode.props);
let oldChildren = oldVnode.children; // 老子标签
let newChildren = newVnode.children; // 新子标签
// 3.1. 老的有子节点,新的没有
if(oldChildren.length > 0 && newChildren.length == 0){
domElement.innerHTML = ''
}
// 3.2. 老的没子节点,新的有即新增
else if(newChildren.length > 0 && oldChildren.length == 0){
for(let i = 0; i < newChildren.length; i++){
domElement.appendChild(createDomElementFromVnode(newChildren[i]))
}
}
}
测试下我们写的代码
let oldVD = createElement(
'div',
{id: 'testDom', style:{color: 'blue'}}
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'div',
{id: 'testDom', style:{color: 'yellow'}},
'测试内容'
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒钟后内容被渲染上去了
- (3.3). 类型相同且是标签 -- 老的有子节点,新的也有 ==> diff
这第三种比较复杂,就是我们常说的diff算法了,
/**
* 补丁方法,更新标签
* @param {*} oldVnode 旧标签
* @param {*} newVnode 新标签
*/
function patch(oldVnode, newVnode){
// 如果标签不一致直接替换成新节点
if(oldVnode.type !== newVnode.type){
return oldVnode.domElement.parentNode.replaceChild(createDomElementFromVnode(newVnode), oldVnode.domElement);
}
// 类型相同 ==> 文本
if(oldVnode.text){
if(oldVnode.text === newVnode.text) return;
return oldVnode.domElement.textContent = newVnode.text;
}
// 类型相同 ==> 是标签:需要根据新节点的属性更新老节点的属性
let domElement = newVnode.domElement = oldVnode.domElement;
// 根据最新的虚拟节点更新老节点的属性
updateProperties(newVnode, oldVnode.props);
let oldChildren = oldVnode.children; // 老子标签
let newChildren = newVnode.children; // 新子标签
// 3.1. 老的有子节点,新的没有
if(oldChildren.length > 0 && newChildren.length == 0){
domElement.innerHTML = ''
}
// 3.2. 老的没子节点,新的有即新增
else if(newChildren.length > 0 && oldChildren.length == 0){
for(let i = 0; i < newChildren.length; i++){
domElement.appendChild(createDomElementFromVnode(newChildren[i]))
}
}
// 3.3. 老的有子节点,新的也有 ==> diff
else if(newChildren.length > 0 && oldChildren.length > 0){
updateChildren(domElement, oldChildren, newChildren);
}
}
updateChildren() 方法 : diff 算法
为了方便我们观察,我们采用列表的形式,如大家所了解的那样:diff算法是双指针比对法,即头部指针和尾部指针。所以我们先现取出新旧节点的头尾两个指针,如下:
/**
* diff 算法
* @param {*} parent 父节点
* @param {*} oldChildren 旧的子节点
* @param {*} newChildren 新的子节点
*/
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 = newdChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[newEndIndex];
}
具体的diff又分为几种情况,如下:
1.顺序没变的情况:头部一样,尾部新增/更新属性
/**
* diff 算法
* @param {*} parent 父节点
* @param {*} oldChildren 旧的子节点
* @param {*} newChildren 新的子节点
*/
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[newEndIndex];
// 循环时不管新旧节点哪个先结束都会停止循环
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
// 如果新旧节点的 头和头 相同
if(isSameVnode(oldStartVnode, newStartVnode)){
// 更新属性
patch(oldStartVnode, newStartVnode);
// 向后移动指针并取得下一个的虚拟节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
}
// 循环结束时剩余节点则插入到父节点上
if(newStartIndex <= newEndIndex){
for(let i = newStartIndex; i <= newEndIndex; i++){
parent.appendChild(createDomElementFromVnode(newChildren[i]))
}
}
}
// 判断是不是同一个节点
function isSameVnode(oldVnode, newVnode){
return oldVnode.key === newVnode.key && oldVnode.type === newVnode.type;
}
测试我们的代码:
let oldVD = createElement(
'ul', {},
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A'),
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D')
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'ul', {},
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A1'),
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B1'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C1'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D1'),
createElement('li', {style: {color: 'pink'}, key: 'E'}, 'E1')
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
两秒钟后可以看到节点更新了
2.顺序没变的情况:尾部一样,头部新增/更新属性
/**
* diff 算法
* @param {*} parent 父节点
* @param {*} oldChildren 旧的子节点
* @param {*} newChildren 新的子节点
*/
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[newEndIndex];
// 循环时不管新旧节点哪个先结束都会停止循环
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
// 如果新旧节点的 头和头 相同
if(isSameVnode(oldStartVnode, newStartVnode)){
// 更新属性
patch(oldStartVnode, newStartVnode);
// 头部向后移动指针并取得下一个的虚拟节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
// 如果新旧节点的 尾和尾 相同
else if(isSameVnode(oldEndVnode, newEndVnode)){
// 更新属性
patch(oldEndVnode, newEndVnode);
// 尾部向前移动指针并取得下一个的虚拟节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
}
// 循环结束时剩余节点则插入到父节点上
if(newStartIndex <= newEndIndex){
for(let i = newStartIndex; i <= newEndIndex; i++){
let beforeElement = newChildren[newStartIndex + 1] == null ?null :newChildren[newStartIndex + 1] .domElement;
parent.insertBefore( createDomElementFromVnode(newChildren[i]),beforeElement);
}
}
}
测试下我们的代码:
let oldVD = createElement(
'ul', {},
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A'),
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D')
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'ul', {},
createElement('li', {style: {color: 'pink'}, key: 'E'}, 'E1'),
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A1'),
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B1'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C1'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D1'),
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒钟后在前面追加内容
3.旧节点的头和新节点的尾相同 或 旧节点的尾和新节点的头相同
/**
* diff 算法
* @param {*} parent 父节点
* @param {*} oldChildren 旧的子节点
* @param {*} newChildren 新的子节点
*/
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[newEndIndex];
// 循环时不管新旧节点哪个先结束都会停止循环
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
// 如果新旧节点的 头和头 相同
if(isSameVnode(oldStartVnode, newStartVnode)){
// 更新属性
patch(oldStartVnode, newStartVnode);
// 头部向后移动指针并取得下一个的虚拟节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
// 如果新旧节点的 尾和尾 相同
else if(isSameVnode(oldEndVnode, newEndVnode)){
// 更新属性
patch(oldEndVnode, newEndVnode);
// 尾部向前移动指针并取得下一个的虚拟节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
// 如果旧节点的头和新节点的尾相同
else if(isSameVnode(oldStartVnode, newEndVnode)){
// 更新属性
patch(oldStartVnode, newEndVnode);
// 将头部的移动尾部
parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSibling);
// 旧节点的头部指针向后移动一个
oldStartVnode = oldChildren[++oldStartIndex];
// 新节点的尾部指针向前移动一个
newEndVnode = newChildren[--newEndIndex];
}
// 如果旧节点的尾和新节点的头相同
else if(isSameVnode(oldEndVnode, newStartVnode)){
// 更新属性
patch(oldEndVnode, oldEndVnode);
// 将头部的移动尾部
parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement);
// 旧节点的头部指针向后移动一个
oldEndtVnode = oldChildren[--oldEndIndex];
// 新节点的尾部指针向前移动一个
newStartVnode = newChildren[++newStartIndex];
}
}
// 循环结束时剩余节点则插入到父节点上
if(newStartIndex <= newEndIndex){
for(let i = newStartIndex; i <= newEndIndex; i++){
let beforeElement = newChildren[newStartIndex + 1] == null ?null :newChildren[newStartIndex + 1] .domElement;
parent.insertBefore( createDomElementFromVnode(newChildren[i]),beforeElement);
}
}
}
测试下我们的代码
let oldVD = createElement(
'ul', {},
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A'),
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D')
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'ul', {},
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D'),
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A')
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒钟后重新渲染了
4.前后都不相同,只能暴力比对
暴力比对也可能会有复用的节点,所以为了方便比对取值,我们创建一个方法将旧节点对一个映射表,方法如下:
function createMapByKeyToIndex(oldChildren){
let map = {};
for(let i= 0; i < oldChildren.length; i++){
let current = oldChildren[i];
if(current.key)
map[current.key] = i;
}
return map;
}
同样也需要在`updateChildren`方法中将旧节点转成映射表
function updateChildren(parent,oldChildren,newChildren){
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
// 将旧节点转成映射表
let map = createMapByKeyToIndex(oldChildren);
....
}
整体代码如下:
/**
* diff 算法
* @param {*} parent 父节点
* @param {*} oldChildren 旧的子节点
* @param {*} newChildren 新的子节点
*/
function updateChildren(parent,oldChildren,newChildren){
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let map = createMapByKeyToIndex(oldChildren);
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[newEndIndex];
// 循环时不管新旧节点哪个先结束都会停止循环
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
// 因为在暴力比对时有可能会被置为 undefined 所以要判断下
if(!oldStartVnode){
oldStartVnode = oldChildren[++oldStartIndex];
}else if(!oldEndVnode){
oldEndVnode = oldChildren[--oldEndIndex]
}else{
// 如果新旧节点的 头和头 相同
if(isSameVnode(oldStartVnode, newStartVnode)){
// 更新属性
patch(oldStartVnode, newStartVnode);
// 头部向后移动指针并取得下一个的虚拟节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
// 如果新旧节点的 尾和尾 相同
else if(isSameVnode(oldEndVnode, newEndVnode)){
// 更新属性
patch(oldEndVnode, newEndVnode);
// 尾部向前移动指针并取得下一个的虚拟节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
// 如果旧节点的头和新节点的尾相同
else if(isSameVnode(oldStartVnode, newEndVnode)){
// 更新属性
patch(oldStartVnode, newEndVnode);
// 将头部的移动尾部
parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSibling);
// 旧节点的头部指针向后移动一个
oldStartVnode = oldChildren[++oldStartIndex];
// 新节点的尾部指针向前移动一个
newEndVnode = newChildren[--newEndIndex];
}
// 如果旧节点的尾和新节点的头相同
else if(isSameVnode(oldEndVnode, newStartVnode)){
// 更新属性
patch(oldEndVnode, oldEndVnode);
// 将头部的移动尾部
parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement);
// 旧节点的头部指针向后移动一个
oldEndVnode = oldChildren[--oldEndIndex];
// 新节点的尾部指针向前移动一个
newStartVnode = newChildren[++newStartIndex];
}
// 都不相同,暴力比对
else{
let index = map[newStartVnode.key];
// 新节点中没有此项
if(index == null){
parent.insertBefore(createDomElementFromVnode(newStartVnode), oldStartVnode.domElement)
}
// 若有此项,则复用
else{
let toMoveVnode = oldChildren[index];
patch(toMoveVnode, newStartVnode);
// 将符合项移动到前头复用
parent.insertBefore(toMoveVnode.domElement, oldStartVnode.domElement);
// 并将移动项所在位置变为空
oldChildren[index] = undefined;
}
newStartVnode = newChildren[++newStartIndex];
}
}
}
// 循环结束时剩余节点则插入到父节点上
if(newStartIndex <= newEndIndex){
for(let i = newStartIndex; i <= newEndIndex; i++){
let beforeElement = newChildren[newStartIndex + 1] == null ?null :newChildren[newStartIndex + 1].domElement;
parent.insertBefore(createDomElementFromVnode(newChildren[i]), beforeElement);
}
}
// 暴力比对时清空旧节点剩余的节点
if(oldStartIndex <= oldEndIndex){
for(let i = oldStartIndex; i <= oldEndIndex; i++){
if(oldChildren[i])
parent.removeChild(oldChildren[i].domElement);
}
}
}
测试下我们的代码:
let oldVD = createElement(
'ul', {},
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A'),
createElement('li', {style: {color: 'blue'}, key: 'B'}, 'B'),
createElement('li', {style: {color: 'yellow'}, key: 'C'}, 'C'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D')
)
window.onload = function(){
let div = document.getElementById('test_container');
render(oldVD, div)
let newVD = createElement(
'ul', {},
createElement('li', {style: {color: 'blue'}, key: 'G'}, 'G'),
createElement('li', {style: {color: 'yellow'}, key: 'E'}, 'E'),
createElement('li', {style: {color: 'green'}, key: 'D'}, 'D'),
createElement('li', {style: {color: 'red'}, key: 'A'}, 'A'),
createElement('li', {style: {color: 'red'}, key: 'F'}, 'F'),
createElement('li', {style: {color: 'red'}, key: 'M'}, 'M')
)
setTimeout(() => {
patch(oldVD, newVD)
}, 2000);
}
可以看到两秒钟后重新渲染了
到此虚拟节点以及Diff算法的一些大概逻辑咱们就说完了,希望对你有所帮助
欢迎点赞收藏评论留言~~~