复制代码
- 实例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
,如果有并且和oldVnode
的text
不同直接更新文本内容(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
同级别节点比较
的五种
情况:
-
oldStartVnode/newStartVnode
(旧开始节点/新开始节点)相同 -
oldEndVnode/newEndVnode
(旧结束节点/新结束节点)相同 -
oldStartVnode/newEndVnode
(旧开始节点/新结束节点)相同 -
oldEndVnode/newStartVnode
(旧结束节点/新开始节点)相同 -
特殊情况当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前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
算法
-
冒泡排序
-
选择排序
-
快速排序
-
二叉树查找: 最大值、最小值、固定值
-
二叉树遍历
-
二叉树的最大深度
-
给予链表中的任一节点,把它删除掉
-
链表倒叙
-
如何判断一个单链表有环
由于篇幅限制小编,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)]
算法
-
冒泡排序
-
选择排序
-
快速排序
-
二叉树查找: 最大值、最小值、固定值
-
二叉树遍历
-
二叉树的最大深度
-
给予链表中的任一节点,把它删除掉
-
链表倒叙
-
如何判断一个单链表有环
[外链图片转存中…(img-djS1Jo5j-1710592010927)]
由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!