一、snabbdom简介和测试环境搭建
1.0 课程大纲
本篇的目的就是: 真正的、彻底的弄懂虚拟DOM和diff算法 何为真正、彻底弄懂呢? 把它们的底层动手敲出来!
1.1 snabbdom简介
snabbdom是瑞典语单词,单词原意“速度”;
snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;
官方git:github.com/snabbdom/sn…
1.2 创建项目
新建文件夹,vscode打开,npm init
// package.json
{
"name": "study-snabbdom",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server"
},
"author": "",
"license": "ISC",
"dependencies": {
"snabbdom": "^2.1.0"
},
"devDependencies": {
"webpack": "^5.11.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}
1.3 安装snabbdom
在git上的snabbdom源码是用TypeScript写的,git上并不提供编译好的 JavaScript版本;
如果要直接使用build出来的JavaScript版的snabbdom库,可以从npm上下载: npm i -D snabbdom,注意: -D是开发依赖,-S是项目真正的依赖
学习库底层时,建议大家阅读原汁原味的TS代码,最好带有库作者原注释, 这样对你的源码阅读能力会有很大的提升。
1.4 snabbdom的测试环境搭建
snabbdom库是DOM库,当然不能在nodejs环境运行,所以我们需要搭建webpack 和webpack-dev-server开发环境,好消息是不需要安装任何loader
这里需要注意,必须安装最新版webpack@5,不能安装webpack@4,这是因为webpack4没有读取身份证中exports的能力,只有webpack@5可以使用snabbdom的别名,建议大家使用这样的版本:npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
参考webpack官网,书写好webpack.config.js文件
// webpack.config.js文件
// 从https://www.webpackjs.com/官网照着配置
const path = require('path');
module.exports = {
// 入口
entry: './src/index.js',
// 出口
output: {
// 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成
publicPath: 'xuni',
// 打包出来的文件名,不会真正的物理生成
filename: 'bundle.js'
},
devServer: {
// 端口号
port: 8080,
// 静态资源文件夹
contentBase: 'www'
}
};
<!-- www/index.html文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">按我改变DOM</button>
<div id="container"></div>
<script src="/xuni/bundle.js"></script><!-- 对应webpack里的配置 -->
</body>
</html>
1.5 跑通官方git首页的demo程序
跑通snabbdom官方git首页的demo程序,即证明调试环境已经搭建成功 github.com/snabbdom/sn… 不要忘记在index.html中放置一个div#container
// src/index.js
// 官方demo
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: function() {} } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: function() {} } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
二、snabbdom的h函数如何工作
2.1 虚拟dom
2.2 diff是发生在虚拟DOM上的
2.3 本次课不研究真实DOM如何变为虚拟DOM
2.4 本次课研究什么
研究1:虚拟DOM如何被渲染函数(h 函数)产生?
我们要手写 h 函数
研究2:diff算法原理?
我们要手写diff算法
研究3:虚拟DOM如何通过diff变为真正的DOM的
事实上,虚拟DOM变回真正的DOM,是涵盖在diff算法里面的
2.5 h函数用来产生虚拟节点
h 函数用来产生虚拟节点(vnode)
// 比如这样调用h函数:
h('a', { props: { href: 'http://www.lll.com' }}, '葡葡');
// 将得到这样的虚拟节点:
{ "sel": "a", "data": { props: { href: 'http://www.lll.com' } }, "text": "葡葡" }
// 它表示的真正的DOM节点:
<a href= "http://www.lll.com"> 葡葡 </a>
2.6 一个虚拟节点有哪些属性
{
children: undefined //表示该节点的子元素
data: {} //表示该节点身上的样式属性
elm: undefined //对应真正的dom节点,undefined表示还没有上树
key: undefined //该节点的唯一标识
sel: "div" //表示选择器selector
text: "我是一个盒子"//内容文字
}
2.7 h函数可以嵌套使用,从而得到虚拟DOM树(重要)
比如这样嵌套使用h函数:
h('ul', {}, [
h('li', {}, '牛奶'),
h('li', {}, '咖啡'),
h('li', {}, '可乐')
]);
将得到这样的虚拟DOM树:
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "text": "牛奶" },
{ "sel": "li", "text": "咖啡" },
{ "sel": "li", "text": "可乐" }
]
}
2.8 h函数用法很活
演示一下h函数的多种用法
const myVnodes = h('ul', [
h('li', {}, '苹果'),
h('li', '西瓜'),
h('li', [
h('div',[
h('p', '哈哈'),
h('p', '嘻嘻')
]
]),
h('li', h('p','火龙果'))
])
2.9 光说不练假把式
// src/index.js
import { init } from 'snabbdom/init';
import { classModule } from 'snabbdom/modules/class';
import { propsModule } from 'snabbdom/modules/props';
import { styleModule } from 'snabbdom/modules/style';
import { eventListenersModule } from 'snabbdom/modules/eventlisteners';
import { h } from 'snabbdom/h';
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
// 创建虚拟节点
const myVnode1 = h('a', {
props: {
href: 'http://www.atguigu.com',
target: '_blank'
}
}, '尚硅谷');
const myVnode2 = h('div', '我是一个盒子');
const myVnode3 = h('ul', [
h('li', {}, '苹果'),
h('li', '西瓜'),
h('li', [
h('div', [
h('p', '哈哈'),
h('p', '嘻嘻')
])
]),
h('li', h('p', '火龙果'))
]);
console.log(myVnode3);
// 让虚拟节点上树
const container = document.getElementById('container');
patch(container, myVnode3);
2.10 手写 h 函数
看TS代码,写JS代码
看源码的TS版代码,然后仿写JS代码 • 只要主干功能,放弃实现一些细节,保证能够让大家理解核心是如何实现的
// src/mysnabbdom/vnode.js
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function(sel, data, children, text, elm) {
const key = data.key;
return {
sel, data, children, text, elm, key
};
}
// src/mysnabbdom/h.js
import vnode from './vnode.js';
// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可
// 相当于它的重载功能较弱。
// 也就是说,调用的时候形态必须是下面的三种之一:
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
export default function (sel, data, c) {
// 检查参数的个数
if (arguments.length != 3)
throw new Error('对不起,h函数必须传入3个参数,我们是低配版h函数');
// 检查参数c的类型
if (typeof c == 'string' || typeof c == 'number') {
// 说明现在调用h函数是形态①
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray(c)) {
// 说明现在调用h函数是形态②
let children = [];
// 遍历c,收集children
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象,如果不满足
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
throw new Error('传入的数组参数中有项不是h函数');
// 这里不用执行c[i],因为你的测试语句中已经有了执行
// 此时只需要收集好就可以了
children.push(c[i]);
}
// 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它有children属性的
return vnode(sel, data, children, undefined, undefined);
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// 说明现在调用h函数是形态③
// 即,传入的c是唯一的children。不用执行c,因为测试语句中已经执行了c。
let children = [c];
return vnode(sel, data, children, undefined, undefined);
} else {
throw new Error('传入的第三个参数类型不对');
}
};
最后,用src/index.js文件去测试一下自己写的h函数
三、diff算法原理、手写diff算法
3.1 感受diff算法
1. 最小量更新太厉害啦! 真的是最小量更新! 当然, key很重要。 key是这个节点的唯一标识, 告诉diff算法, 在更改前后它们是同一个DOM节点。
2. 只有是同一个虚拟节点, 才进行精细化比较, 否则就是暴力删除旧的、 插入新的。延伸问题: 如何定义是同一个虚拟节点?
答: 选择器相同且key相同。
3. 只进行同层比较, 不会进行跨层比较。 即使是同一片虚拟节点, 但是跨层了, 对不起, 精细化比较不diff你, 而是暴力删除旧的、 然后插入新的。
4. diff并不是那么的“无微不至” 啊! 真的影响效率么??
5. 答: 上面2、 3操作在实际Vue开发中, 基本不会遇见, 所以这是合理的优化机制。
// src/index.js
import { init } from 'snabbdom/init';
import { classModule } from 'snabbdom/modules/class';
import { propsModule } from 'snabbdom/modules/props';
import { styleModule } from 'snabbdom/modules/style';
import { eventListenersModule } from 'snabbdom/modules/eventlisteners';
import { h } from 'snabbdom/h';
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const vnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D')
]);
patch(container, vnode1);
const vnode2 = h('ul', {}, [
h('li', { key: 'D' }, 'D'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'B' }, 'B')
]);
// 点击按钮时,将vnode1变为vnode2
btn.onclick = function () {
patch(vnode1, vnode2);
};
3.2 diff处理新旧节点不是同一个节点时
区分虚拟节点、DOM节点:
DOM节点,即HTMLElement,即document.createElement(vnode.sel)得到的domNode,也就是elm属性所指,elm属性是一个纯dom对象。
DOM节点上树了才能说,是真实DOM
以免脑子混乱,再看一遍这个关系
// 比如这样调用h函数:
h('a', { props: { href: 'http://www.lll.com' }}, '葡葡');
// 将得到这样的虚拟节点:
{ "sel": "a", "data": { props: { href: 'http://www.lll.com' } }, "text": "葡葡" }
// 它表示的真正的DOM节点:
<a href= "http://www.lll.com"> 葡葡 </a>
3.3 如何定义“同一个节点” 这个事儿
旧节点的key要和新节点的key相同,且
旧节点的选择器要和新节点的选择器相同
3.4 创建节点时, 所有子节点需要递归创建的
3.5 手写第一次上树时 + 递归创建子节点
上树有三种方式:
insertBefore(),有标杆时用这个;
appendChild(),子元素没有标杆,直接上树。
oldVnode.elm.innerText = newVnode.text
// src/index.js
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';
const myVnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
]);
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 第一次上树
patch(container, myVnode1);
// 新节点2 选择器相同且key相同
const myVnode2 = h('ul', {}, [
h('li', { key: 'Q' }, 'Q'),
h('li', { key: 'T' }, 'T'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'Z' }, 'Z'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
]);
// 新节点3 假如多一层,会怎么样,不属于最小量更新了,
// 无克隆无移动,直接暴力拆解,新建,插入新的
const myVnode2 = h('ul', {}, h('section', {}, [
[
h('li', { key: 'Q' }, 'Q'),
h('li', { key: 'T' }, 'T'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'Z' }, 'Z'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
]
]));
btn.onclick = function () {
patch(myVnode1, myVnode2);
}
// src/mysnabbdom/h.js
import vnode from './vnode.js';
// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可
// 相当于它的重载功能较弱。
// 也就是说,调用的时候形态必须是下面的三种之一:
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
export default function (sel, data, c) {
// 检查参数的个数
if (arguments.length != 3)
throw new Error('对不起,h函数必须传入3个参数,我们是低配版h函数');
// 检查参数c的类型
if (typeof c == 'string' || typeof c == 'number') {
// 说明现在调用h函数是形态①
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray(c)) {
// 说明现在调用h函数是形态②
let children = [];
// 遍历c,收集children
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象,如果不满足
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
throw new Error('传入的数组参数中有项不是h函数');
// 这里不用执行c[i],因为你的测试语句中已经有了执行
// 此时只需要收集好就可以了
children.push(c[i]);
}
// 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它有children属性的
return vnode(sel, data, children, undefined, undefined);
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// 说明现在调用h函数是形态③
// 即,传入的c是唯一的children。不用执行c,因为测试语句中已经执行了c。
let children = [c];
return vnode(sel, data, children, undefined, undefined);
} else {
throw new Error('传入的第三个参数类型不对');
}
};
// src/mysnabbdom/vnode.js
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function(sel, data, children, text, elm) {
const key = data.key;
return {
sel, data, children, text, elm, key
};
}
// src/mysnabbdom/patch.js
import vnode from './vnode.js';
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点(即HTMLElement)还是虚拟节点?
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
console.log('是同一个节点');
patchVnode(oldVnode, newVnode);
} else {
console.log('不是同一个节点,暴力插入新的,删除旧的');
let newVnodeElm = createElement(newVnode);
// 插入到老节点之前
if (oldVnode.elm.parentNode && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
};
// src/mysnabbdom/createElement.js
// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function createElement(vnode) {
// console.log('目的是把虚拟节点', vnode, '真正变为DOM');
// 创建一个DOM节点,这个节点现在还是孤儿节点
let domNode = document.createElement(vnode.sel);
// 有子节点还是有文本??
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 它内部是文字
domNode.innerText = vnode.text;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内部是子节点,就要递归创建节点
for (let i = 0; i < vnode.children.length; i++) {
// 得到当前这个children
let ch = vnode.children[i];
// 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
let chDOM = createElement(ch);
// 上树
domNode.appendChild(chDOM);
}
}
// 补充elm属性
vnode.elm = domNode;
// 返回elm,elm属性是一个纯DOM对象
return vnode.elm;
};
3.6 diff处理新旧节点是同一个节点时
此处一个细节,为什么最后红色那里的①要强调清空oldVnode的text,因为文字可以清空dom,但是追加dom是不能清空文字的
3.7 手写新旧节点text的不同情况
// src/mysnabbdom/patchVnode.js
import createElement from "./createElement";
import updateChildren from './updateChildren.js';
// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
// 判断新旧vnode是否是同一个对象,oldVnode === newVnode指的是内存相等
if (oldVnode === newVnode) return;
// 判断新vnode有没有text属性
if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
// 新vnode有text属性
console.log('新vnode有text属性');
if (newVnode.text != oldVnode.text) {
// 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。
oldVnode.elm.innerText = newVnode.text;
}
} else {
// 新vnode没有text属性,有children
console.log('新vnode没有text属性');
// 判断老的有没有children
if (oldVnode.children != undefined && oldVnode.children.length > 0) {
// 老的有children,新的也有children,此时就是最复杂的情况。
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
} else {
// 老的没有children,新的有children
// 清空老的节点的内容
oldVnode.elm.innerHTML = '';
// 遍历新的vnode的子节点,创建DOM,上树
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i]);
oldVnode.elm.appendChild(dom);
}
}
}
}
3.8 尝试书写diff更新子节点
有几种可能的情况需要提前考虑到:新增、删除、更新
新增
删除
更新
3.9 diff算法的子节点更新策略
四种命中查找:
① 新前与旧前
② 新后与旧后
③ 新后与旧前(此种发生了, 涉及移动节点, 那么旧前指向的节点, 移动到旧后之后)
④ 新前与旧后(此种发生了, 涉及移动节点, 那么新前指向的节点, 移动的旧前之前)
遵从①到④的顺序,命中一种就不再进行命中判断了
如果都没有命中, 就需要用循环来寻找了。 移动到oldStartIdx之前。
新前、旧前、新后、旧后,分别是四个指针
新前指针、旧前指针只往后移
新后指针、旧后指针只往前移
新增的情况
删除的情况
多删除的情况
复杂的情况
-
上面两个图,第④步,把新后(或者是旧前,因为都命中了,是一样的)移动到旧前的前面
-
别忘了移动之前,把对应旧子节点设置成undefined,移动之后,各自指针会移动,但是不会进行第④步,而是重新开始比较,即回到【①新前与旧前】这一步开始比较
-
上面两个图有错误,第③步,应该是把新后(或者旧前,因为都命中了,是一样的)移动到旧后的后面
-
特别留意,此种情况下,B在A上面(其余类似),充分明白,要把对应旧子节点设成undefined之后,新节点要放在未处理的旧节点,旧后指针的后面
3.10 手写子节点更新策略
// src/mysnabbdom/updateChildren.js
import patchVnode from './patchVnode.js';
import createElement from './createElement.js';
// 判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
return a.sel == b.sel && a.key == b.key;
};
export default function updateChildren(parentElm, oldCh, newCh) {
console.log('我是updateChildren');
console.log(oldCh, newCh);
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
// 开始大while了
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log('★');
// 首先不是判断①②③④命中,而是要略过已经加undefined标记的东西
// 用===,就用undefined
// 其实新的可以不用判断,这里老师有迷之操作,也许是老师也懵了,哈哈哈
if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newStartVnode)) {
// 新前和旧前
console.log('①新前和旧前命中');
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// 新后和旧后
console.log('②新后和旧后命中');
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
// 新后和旧前
console.log('③新后和旧前命中');
patchVnode(oldStartVnode, newEndVnode);
// 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
// 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// 新前和旧后
console.log('④新前和旧后命中');
patchVnode(oldEndVnode, newStartVnode);
// 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
// 如何移动节点??只要你插入一个已经在DOM树上的节点,它就会被移动
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 四种命中都没有命中
// 制作keyMap一个映射对象,这样就不用每次都遍历老对象了。
if (!keyMap) {
keyMap = {};
// 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key != undefined) {
keyMap[key] = i;
}
}
}
console.log(keyMap);
// 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
const idxInOld = keyMap[newStartVnode.key];
console.log(idxInOld);
if (idxInOld == undefined) {
// 判断,如果idxInOld是undefined表示它是全新的项
// 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// 如果不是undefined,不是全新的项,而是要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// 把这项设置为undefined,表示我已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动,调用insertBefore也可以实现移动。
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// 指针下移,只移动新的头
newStartVnode = newCh[++newStartIdx];
}
}
// 继续看看有没有剩余的。循环结束了start还是比old小
if (newStartIdx <= newEndIdx) {
console.log('new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStartIdx之前');
// 遍历新的newCh,添加到老的没有处理的之前
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
// newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
}
} else if (oldStartIdx <= oldEndIdx) {
console.log('old还有剩余节点没有处理,要删除项');
// 批量删除oldStart和oldEnd指针之间的项
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm);
}
}
}
};
为了方便看,再贴一次
// src/index.js
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';
const myVnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
]);
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 第一次上树
patch(container, myVnode1);
// 新节点2 选择器相同且key相同
const myVnode2 = h('ul', {}, [
h('li', { key: 'Q' }, 'Q'),
h('li', { key: 'T' }, 'T'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'Z' }, 'Z'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
]);
// 新节点3 假如多一层,会怎么样,不属于最小量更新了,
// 无克隆无移动,直接暴力拆解,新建,插入新的
const myVnode2 = h('ul', {}, h('section', {}, [
[
h('li', { key: 'Q' }, 'Q'),
h('li', { key: 'T' }, 'T'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'Z' }, 'Z'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
]
]));
btn.onclick = function () {
patch(myVnode1, myVnode2);
}