深入学习Vue.js(六)简单Diff算法
创始人
2024-05-09 00:15:10
0

文章目录

  • 1.Diff函数的基本思路
  • 2.DOM复用和key的作用
  • 3.标记需要移动的元素
  • 4.移动元素
  • 5.添加新元素
  • 6.移除不存在的元素

1.Diff函数的基本思路

   简单来说,当新旧vonde的子节点都是一组节点,为了以最小的性能开销来完成更新操作,需要比较两组子节点,用于比较的算法就叫做Dff算法。

   首先,以最基本的更新两组子节点的方法为例,通常我们会先将旧节点全部卸载,再安装所有的新节点,那么将会执行旧节点数量+新节点数量次。但有时新节点和旧节点之间,节点类型是想相同的,只有节点内容不同,这是我们可以直接修改节点内容,而省略一次挂载操作。这样就直接将执行时间缩短为之前的一半了。

function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {// ...} else if (Array.isArray(n2.children)) {if (Array.isArray(n1.children)) {const oldChildren = n1.children;const newChildren = n2.children;for (let i = 0; i < oldChildren.length; i++) {patch(oldChildren[i], newChildren[i]); // patch函数会比较两个节点之间的差异}} else {// ...}}

   当然,上述代码的问题也很明显————只有新旧DOM的chidren一样多的时候,以上代码才能正常工作。当新一组节点的长度大于旧节点时吗,将会有新节点被直接挂载,而当旧一组节点的长度大于新节点是,将会有节点被直接卸载。

function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {// ...} else if (Array.isArray(n2.children)) {const oldChildren = n1.children;const newChildren = n2.children;const oldLen = oldChildren.length; // 旧节点子节点长度const newLen = newChildren.length; // 新节点子节点长度const commonLength = Math.max(oldLen, newLen);for (let i = 0; i < commonLength; i++) {patch(oldChildren[i], newChildren[i]);}// 如果旧节点比较长,直接卸载长出来的哪一部分if (oldLen > newLen) {for (let i = commonLength; i < oldLen; i++) {unmount(oldChildren[i]);}} else if (newLen > oldLen) {// 如果新节点比较长,直接挂载长出来哪一部分for (let i = commonLength; i < newLen; i++) {patch(null, newChildren[i], container);}}}}

   这样渲染器就能无视数量差异去渲染它们了。

2.DOM复用和key的作用

   在上节中,通过减少DOM操作降低了性能消耗,但是节点之间的比较只是最普通的顺序比较,对于存在重复节点的情况并没有考虑,比如说,对于下面两个结点:

const oldNode = {type: 'div',children: [{type: 'p'},{type: 'span'}],
}
const newNode = {type: 'div',children: [{type: 'span'},{type: 'p'}],
}

   如果使用上述代码执行,将会产生四次操作。但实际上,只需要一次交换操作就行了,以为oldNode和newNode之间只是结点的顺序不一样而已。而为了确定新老Node之间是否存在相同结点,就需要引入一个key作为标记,只要两个节点的type和key属性是一样的,那么我们就认为它们是相同节点,但是我们仍然还是要对两个节点进行打补丁的操作,因为节点的内容可能发生了变化。

const oldNode = {type: 'div',children: [{type: 'p', key: 1 },{type: 'span', key:2 }],
}
const newNode = {type: 'div',children: [{type: 'span', key: 2 },{type: 'p', key: 1 }],
}function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {// ...} else if (Array.isArray(n2.children)) {const oldChildren = n1.children;const oldChildren = n1.children;const newChildren = n2.children;for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i];for (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j];if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container);break;}}}}}

3.标记需要移动的元素

   前面我们通过key完成了元素的更新,但是元素的移动还没有实现。这里我们可以用索引的顺序,来判断节点是否需要移动。这里我们使用一个lastIndex变量来存储当前遍历过的最大索引,如果当前遍历的元素在旧children中的索引小于当前最大索引,那么就说明该元素是需要移动的。

function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {// ...} else if (Array.isArray(n2.children)) {const oldChildren = n1.children;const oldChildren = n1.children;const newChildren = n2.children;let lastIndex = 0; // 存储点前遍历的最大索引for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i];for (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j];if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container);if (j < lastIndex) {// 说明该节点需要移动} else {// 否则,更新lastIndexlastIndex = j;}break;}}}}}

4.移动元素

   标记完需要移动的元素之后,要做的就是移动真实DOM了,这里我们通过vnode获取到它所对应的真实DOM。而当前vnode所在位置,实际上就是在新children中的位置,我们只需要将它插入到当前newChildren节点队列里就行了。

function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {// ...} else if (Array.isArray(n2.children)) {const oldChildren = n1.children;const oldChildren = n1.children;const newChildren = n2.children;let lastIndex = 0; // 存储点前遍历的最大索引for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i];for (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j];if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container);if (j < lastIndex) {// 说明该节点需要移动const preNode = newChildren[i - 1];if (preNode) {// 获取preNode的下一个兄弟节点,将其作为锚点const anchor = preNode.el.nextSibling;// 调用insert方法,将newNode插入到锚点元素前面insert(newVNode.el, container, anchor);} else {// 否则,更新lastIndexlastIndex = j;}break;}}}}}

5.添加新元素

   对于需要新增的元素,我们同样也需要找到他在新元素列表中的位置。这里我们使用一个find变量,标记是否找到可复用元素,如果没有找到,说明该元素是需要新增的元素。

function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {// ...} else if (Array.isArray(n2.children)) {const oldChildren = n1.children;const oldChildren = n1.children;const newChildren = n2.children;let lastIndex = 0; // 存储点前遍历的最大索引for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i];let find = false; // 标记是否找可复用元素for (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j];if (newVNode.key === oldVNode.key) {find = true; // 一旦找到立即标记patch(oldVNode, newVNode, container);if (j < lastIndex) {// 说明该节点需要移动const preNode = newChildren[i - 1];if (preNode) {// 获取preNode的下一个兄弟节点,将其作为锚点const anchor = preNode.el.nextSibling;// 调用insert方法,将newNode插入到锚点元素前面insert(newVNode.el, container, anchor);}} else {// 否则,更新lastIndexlastIndex = j;}break;}}// 如果没有找到可复用元素,说明当前需要挂载if (!find) {const preNode = newChildren[i - 1]; let anchor = null; // 锚点if (preNode) {anchor = preNode.el.nextSibling;} else {// 如果是第一个元素,获取firstChild作为锚点anchor = preNode.el.firstChild;}patch(null, newVNode, container, anchor);}}}}

6.移除不存在的元素

   同样,对于新children中不存在的元素,我们同样需要进行移除。这里我们对oldChildren进行第二次遍历,查找newChildren是否存在oldChildren中元素的key,如果不存在,说明该元素需要被卸载。

function patchChildren(n1, n2, container) {if (typeof n2.children === "string") {if (Array.isArray(n1.children)) {n1.children.forEach((child) => unmount(child));}setElementText(container, n2.children);} else if (Array.isArray(n2.children)) {if (Array.isArray(n1.children)) {const oldChildren = n1.children;const newChildren = n2.children;let lastIndex = 0; // 存储点前遍历的最大索引for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i];let find = false; // 标记是否找可复用元素for (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j];if (newVNode.key === oldVNode.key) {find = true; // 一旦找到立即标记patch(oldVNode, newVNode, container);if (j < lastIndex) {// 说明该节点需要移动const preNode = newChildren[i - 1];if (preNode) {// 获取preNode的下一个兄弟节点,将其作为锚点const anchor = preNode.el.nextSibling;// 调用insert方法,将newNode插入到锚点元素前面insert(newVNode.el, container, anchor);}} else {// 否则,更新lastIndexlastIndex = j;}break;}}// 如果没有找到可复用元素,说明当前需要挂载if (!find) {const preNode = newChildren[i - 1];let anchor = null; // 锚点if (preNode) {anchor = preNode.el.nextSibling;} else {// 如果是第一个元素,获取firstChild作为锚点anchor = preNode.el.firstChild;}patch(null, newVNode, container, anchor);}}// 判断是否有需要移除的元素for (let i = 0; i < oldChildren.length; i++) {const oldVNode = oldChildren[i];const has = newChildren.find((vnode) => vnode.key === oldVNode.key);if (!has) {unmount(oldVNode);}}} else {setElementText(container, "");n2.children.forEach((child) => patch(null, child, container));}} else {if (Array.isArray(n1.children)) {n1.children.forEach((child) => unmount(child));} else if (typeof n1.child === "string") {setElementText(container, "");}}}

相关内容

热门资讯

MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
操作系统面试题(史上最全、持续... 尼恩面试宝典专题40:操作系统面试题(史上最全、持续更新)...
Android---Banne... 轮播图是一种很常见的UI。Banner框架能够帮助我们快速开发,完成首页轮播图效果的需...
python -- PyQt5... 控件2 本章我们继续介绍PyQt5控件。这次的有 QPixmap , QLineEdi...
Mysql SQL优化跟踪来看... 背景 使用索引字段进行筛选数据时,explain查询语句发现MySQL居然没有使用索...
UG 6.0软件安装教程 UG 6.0软件安装教程 软件简介: UG 6.0是目前网络最好用、使用最为广泛的大型...
HTML静态网页作业——关于我... 家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法,如盒子的嵌套、浮动、ma...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
NoSQL数据库之Redis2 Redis 事务 事务的基础概念 关于事务最常见的例子就是银行转账,A 账户给 B 账...
Spring Security... 前言 在 Spring Security 中,默认的登陆方式是以表单形式进行提交参数的...