DIff算法看不懂就一起来锤我(带图)

复制代码

  • 实例2(函数重载-参数类型)

function add(a:number,b:number){

console.log(a+b)

}

function add(a:number,b:string){

console.log(a+b)

}

add(1,2)

add(1,‘2’)

复制代码


patch函数(核心)

src=http___shp.qpic.cn_qqvideo_ori_0_e3012t7v643_496_280_0&refer=http___shp.qpic.jpeg

要是看完前面的铺垫,看到这里你可能走神了,醒醒啊,这是核心啊,上高地了兄弟;

  • pactch(oldVnode,newVnode)

  • 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点(核心)

  • 对比新旧VNode是否相同节点(节点的key和sel相同)

  • 如果不是相同节点,删除之前的内容,重新渲染

  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnodetext不同直接更新文本内容(patchVnode)

  • 如果新的VNode有children,判断子节点是否有变化(updateChildren,最麻烦,最难实现)

源码:

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {

let i: number, elm: Node, parent: Node

const insertedVnodeQueue: VNodeQueue = []

// cbs.pre就是所有模块的pre钩子函数集合

for (i = 0; i < cbs.pre.length; ++i) cbs.prei

// isVnode函数时判断oldVnode是否是一个虚拟DOM对象

if (!isVnode(oldVnode)) {

// 若不是即把Element转换成一个虚拟DOM对象

oldVnode = emptyNodeAt(oldVnode)

}

// sameVnode函数用于判断两个虚拟DOM是否是相同的,源码见补充1;

if (sameVnode(oldVnode, vnode)) {

// 相同则运行patchVnode对比两个节点,关于patchVnode后面会重点说明(核心)

patchVnode(oldVnode, vnode, insertedVnodeQueue)

} else {

elm = oldVnode.elm! // !是ts的一种写法代码oldVnode.elm肯定有值

// parentNode就是获取父元素

parent = api.parentNode(elm) as Node

// createElm是用于创建一个dom元素插入到vnode中(新的虚拟DOM)

createElm(vnode, insertedVnodeQueue)

if (parent !== null) {

// 把dom元素插入到父元素中,并且把旧的dom删除

api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))// 把新创建的元素放在旧的dom后面

removeVnodes(parent, [oldVnode], 0, 0)

}

}

for (i = 0; i < insertedVnodeQueue.length; ++i) {

insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])

}

for (i = 0; i < cbs.post.length; ++i) cbs.posti

return vnode

}

复制代码

补充1: sameVnode函数

function sameVnode(vnode1: VNode, vnode2: VNode): boolean { 通过key和sel选择器判断是否是相同节点

return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel

}

复制代码


patchVnode

  • 第一阶段触发prepatch函数以及update函数(都会触发prepatch函数,两者不完全相同才会触发update函数)

  • 第二阶段,真正对比新旧vnode差异的地方

  • 第三阶段,触发postpatch函数更新节点

源码:

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {

const hook = vnode.data?.hook

hook?.prepatch?.(oldVnode, vnode)

const elm = vnode.elm = oldVnode.elm!

const oldCh = oldVnode.children as VNode[]

const ch = vnode.children as VNode[]

if (oldVnode === vnode) return

if (vnode.data !== undefined) {

for (let i = 0; i < cbs.update.length; ++i) cbs.updatei

vnode.data.hook?.update?.(oldVnode, vnode)

}

if (isUndef(vnode.text)) { // 新节点的text属性是undefined

if (isDef(oldCh) && isDef(ch)) { // 当新旧节点都存在子节点

if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) //并且他们的子节点不相同执行updateChildren函数,后续会重点说明(核心)

} else if (isDef(ch)) { // 只有新节点有子节点

// 当旧节点有text属性就会把’'赋予给真实dom的text属性

if (isDef(oldVnode.text)) api.setTextContent(elm, ‘’)

// 并且把新节点的所有子节点插入到真实dom中

addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

} else if (isDef(oldCh)) { // 清除真实dom的所有子节点

removeVnodes(elm, oldCh, 0, oldCh.length - 1)

} else if (isDef(oldVnode.text)) { // 把’'赋予给真实dom的text属性

api.setTextContent(elm, ‘’)

}

} else if (oldVnode.text !== vnode.text) { //若旧节点的text与新节点的text不相同

if (isDef(oldCh)) { // 若旧节点有子节点,就把所有的子节点删除

removeVnodes(elm, oldCh, 0, oldCh.length - 1)

}

api.setTextContent(elm, vnode.text!) // 把新节点的text赋予给真实dom

}

hook?.postpatch?.(oldVnode, vnode) // 更新视图

}

复制代码

看得可能有点蒙蔽,下面再上一副思维导图:

image.png


题外话:diff算法简介

传统diff算法

  • 虚拟DOM中的Diff算法

  • 传统算法查找两颗树每一个节点的差异

  • 会运行n1(dom1的节点数)*n2(dom2的节点数)次方去对比,找到差异的部分再去更新

image.png

snabbdom的diff算法优化

  • Snbbdom根据DOM的特点对传统的diff算法做了优化

  • DOM操作时候很少会跨级别操作节点

  • 只比较同级别的节点

image.png

src=http___img.wxcha.com_file_202004_03_1ed2e19e4f.jpg&refer=http___img.wxcha.jpeg

下面我们就会介绍updateChildren函数怎么去对比子节点的异同,也是Diff算法里面的一个核心以及难点;


updateChildren(核中核:判断子节点的差异)

  • 这个函数我分为三个部分,部分1:声明变量,部分2:同级别节点比较,部分3:循环结束的收尾工作(见下图);

image.png

  • 同级别节点比较五种情况:
  1. oldStartVnode/newStartVnode(旧开始节点/新开始节点)相同

  2. oldEndVnode/newEndVnode(旧结束节点/新结束节点)相同

  3. oldStartVnode/newEndVnode(旧开始节点/新结束节点)相同

  4. oldEndVnode/newStartVnode(旧结束节点/新开始节点)相同

  5. 特殊情况当1,2,3,4的情况都不符合的时候就会执行,在oldVnodes里面寻找跟newStartVnode一样的节点然后位移到oldStartVnode,若没有找到在就oldStartVnode创建一个

  • 执行过程是一个循环,在每次循环里,只要执行了上述的情况的五种之一就会结束一次循环

  • 循环结束的收尾工作:直到oldStartIdx>oldEndIdx || newStartIdx>newEndIdx(代表旧节点或者新节点已经遍历完)

  • 为了更加直观的了解,我们再来看看同级别节点比较五种情况的实现细节:

新开始节点和旧开始节点(情况1)

image.png

  • 情况1符合:(从新旧节点的开始节点开始对比,oldCh[oldStartIdx]和newCh[newStartIdx]进行sameVnode(key和sel相同)判断是否相同节点)

  • 则执行patchVnode找出两者之间的差异,更新图;如没有差异则什么都不操作,结束一次循环

  • oldStartIdx++/newStartIdx++

新结束节点和旧结束节点(情况2)

image.png

  • 情况1不符合就判断情况2,若符合:(从新旧节点的结束节点开始对比,oldCh[oldEndIdx]和newCh[newEndIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

  • 执行patchVnode找出两者之间的差异,更新视图,;如没有差异则什么都不操作,结束一次循环

  • oldEndIdx--/newEndIdx--

旧开始节点/新结束节点(情况3)

image.png

  • 情况1,2都不符合,就会尝试情况3:(旧节点的开始节点与新节点的结束节点开始对比,oldCh[oldStartIdx]和newCh[newEndIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

  • 执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

  • oldCh[oldStartIdx]对应的真实dom位移到oldCh[oldEndIdx]对应的真实dom

  • oldStartIdx++/newEndIdx--;

旧结束节点/新开始节点(情况4)

image.png

  • 情况1,2,3都不符合,就会尝试情况4:(旧节点的结束节点与新节点的开始节点开始对比,oldCh[oldEndIdx]和newCh[newStartIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

  • 执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

  • oldCh[oldEndIdx]对应的真实dom位移到oldCh[oldStartIdx]对应的真实dom

  • oldEndIdx--/newStartIdx++;

新开始节点/旧节点数组中寻找节点(情况5)

image.png

  • 从旧节点里面寻找,若寻找到与newCh[newStartIdx]相同的节点(且叫对应节点[1]),执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

  • 对应节点[1]对应的真实dom位移到oldCh[oldStartIdx]对应的真实dom

image.png

  • 若没有寻找到相同的节点,则创建一个与newCh[newStartIdx]节点对应的真实dom插入到oldCh[oldStartIdx]对应的真实dom

  • newStartIdx++

379426071b8130075b11ba142f9468e2.jpeg


下面我们再介绍一下结束循环的收尾工作(oldStartIdx>oldEndIdx || newStartIdx>newEndIdx):

image.png

  • 新节点的所有子节点先遍历完(newStartIdx>newEndIdx),循环结束

  • 新节点的所有子节点遍历结束就是把没有对应相同节点的子节点删除

image.png

  • 旧节点的所有子节点先遍历完(oldStartIdx>oldEndIdx),循环结束

  • 旧节点的所有子节点遍历结束就是在多出来的子节点插入到旧节点结束节点前;(源码:newCh[newEndIdx + 1].elm),就是对应的旧结束节点的真实dom,newEndIdx+1是因为在匹配到相同的节点需要-1,所以需要加回来就是结束节点

最后附上源码:

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {

let oldStartIdx = 0;                // 旧节点开始节点索引

let newStartIdx = 0;                // 新节点开始节点索引

let oldEndIdx = oldCh.length - 1;   // 旧节点结束节点索引

let oldStartVnode = oldCh[0];       // 旧节点开始节点

let oldEndVnode = oldCh[oldEndIdx]; // 旧节点结束节点

let newEndIdx = newCh.length - 1;   // 新节点结束节点索引

let newStartVnode = newCh[0];       // 新节点开始节点

let newEndVnode = newCh[newEndIdx]; // 新节点结束节点

let oldKeyToIdx;                    // 节点移动相关

let idxInOld;                       // 节点移动相关

let elmToMove;                      // 节点移动相关

let before;

// 同级别节点比较

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

if (oldStartVnode == null) {

oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left

}

else if (oldEndVnode == null) {

oldEndVnode = oldCh[–oldEndIdx];

}

else if (newStartVnode == null) {

newStartVnode = newCh[++newStartIdx];

}

else if (newEndVnode == null) {

newEndVnode = newCh[–newEndIdx];

}

else if (sameVnode(oldStartVnode, newStartVnode)) { // 判断情况1

patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);

oldStartVnode = oldCh[++oldStartIdx];

newStartVnode = newCh[++newStartIdx];

}

else if (sameVnode(oldEndVnode, newEndVnode)) {   // 情况2

patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);

oldEndVnode = oldCh[–oldEndIdx];

newEndVnode = newCh[–newEndIdx];

}

else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right情况3

patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);

api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));

oldStartVnode = oldCh[++oldStartIdx];

newEndVnode = newCh[–newEndIdx];

}

else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left情况4

patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);

api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);

oldEndVnode = oldCh[–oldEndIdx];

newStartVnode = newCh[++newStartIdx];

}

else {                                             // 情况5

if (oldKeyToIdx === undefined) {

oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);

}

idxInOld = oldKeyToIdx[newStartVnode.key];

if (isUndef(idxInOld)) { // New element        // 创建新的节点在旧节点的新节点前

api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);

}

else {

elmToMove = oldCh[idxInOld];

if (elmToMove.sel !== newStartVnode.sel) { // 创建新的节点在旧节点的新节点前

api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);

}

else {

// 在旧节点数组中找到相同的节点就对比差异更新视图,然后移动位置

patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);

oldCh[idxInOld] = undefined;

api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);

}

}

newStartVnode = newCh[++newStartIdx];

}

}

// 循环结束的收尾工作

if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {

if (oldStartIdx > oldEndIdx) {

// newCh[newEndIdx + 1].elm就是旧节点数组中的结束节点对应的dom元素

// newEndIdx+1是因为在之前成功匹配了newEndIdx需要-1

// newCh[newEndIdx + 1].elm,因为已经匹配过有相同的节点了,它就是等于旧节点数组中的结束节点对应的dom元素(oldCh[oldEndIdx + 1].elm)

before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;

// 把新节点数组中多出来的节点插入到before前

addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);

}

else {

// 这里就是把没有匹配到相同节点的节点删除掉

removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);

}

}

}

复制代码


key的作用
  • Diff操作可以更加快速;

  • Diff操作可以更加准确;(避免渲染错误)

  • 不推荐使用索引作为key

以下我们看看这些作用的实例:

Diff操作可以更加准确;(避免渲染错误):

实例:a,b,c三个dom元素中的b,c间插入一个z元素

没有设置key当设置了key:

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
img

算法

  1. 冒泡排序

  2. 选择排序

  3. 快速排序

  4. 二叉树查找: 最大值、最小值、固定值

  5. 二叉树遍历

  6. 二叉树的最大深度

  7. 给予链表中的任一节点,把它删除掉

  8. 链表倒叙

  9. 如何判断一个单链表有环

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

8c505cec3ac4d3f27a856a.png)当设置了key:

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-ORjQY3Kp-1710592010925)]
[外链图片转存中…(img-b9GexYVK-1710592010925)]
[外链图片转存中…(img-3r3zd3aI-1710592010926)]
[外链图片转存中…(img-VA10VP0d-1710592010926)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
[外链图片转存中…(img-r9WuRcse-1710592010927)]

算法

  1. 冒泡排序

  2. 选择排序

  3. 快速排序

  4. 二叉树查找: 最大值、最小值、固定值

  5. 二叉树遍历

  6. 二叉树的最大深度

  7. 给予链表中的任一节点,把它删除掉

  8. 链表倒叙

  9. 如何判断一个单链表有环

    [外链图片转存中…(img-djS1Jo5j-1710592010927)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值