一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情。
patch函数介绍
- patch函数用来更新节点,当节点发生改变,对比旧节点与新节点的不同,采用不同的更新方式
- 它接受两个参数,一个旧节点,一个新节点 一步一步实现他
patch函数判断tag标签是否相同
tag标签不相同
如果标签不一样,vue实现的就很简单,把旧节点直接删除,在把新节点挂载
html
const vnode = h("div",{class:"scc",id:"iii",onClick:function(){console.log(2);}},[
h("span",null,"当前计数:100"),
h("button",{onClick:function(){console.log(+1);}},"+1"),
h("button",{onClick:function(){console.log(-1);}},"-1")
])
mount(vnode,document.querySelector("#app"))
const vnode1 = h("h1",{class:"aaa"},"哈哈哈")
patch(vnode,vnode1)
我们之前已经完成了h函数和mount函数,不知道的同学可以移步这里,这是一篇实现h函数和mount函数的文章 我们可以看到,此时patch参数为两个虚拟节点,他们的tag不同,那么我们可以直接删除div添加h1
const patch = (n1,n2)=>{
//n1是旧节点,n2是新节点
const el = n2.el = n1.el //我们必须把节点传出去,方便操作
if (n1.tag !== n2.tag) { //如果tag不一样
const n1ElParent = n1.el.parentElement //获取此节点的父节点
n1ElParent.removeChild(n1.el) //通过父节点移除子节点
mount(n2,n1ElParent) //挂载新节点
}
}
这样简单实现了tag标签不同的处理方式,简单粗暴
tag标签相同
tag标签如果一样,那么我们就要处理标签的属性和方法
添加新的属性方法
先把新的属性方法全部放到标签里面
const newProps = n2.props
const oldProps = n1.props
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
if (newValue !== oldValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(),newValue)
} else {
el.setAttribute(key,newValue)
}
}
}
这个代码还是很简单的,对newProps的key进行循环,拿到key对应的value,判断oldProps的value否和newProps一样,如果一样,判断是事件,还是属性,是事件,调用addEventListener添加事件;是属性调用 setAttribute添加属性,很简单哈。
删除旧的属性和方法
这样就实现了把newProps里面的属性全部添加到节点中,接下来就要删除oldProps里面有的,而newProps里面没有的属性和事件
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(),oldProps[key])
} else {
el.removeAttribute(key)
}
}
}
对旧的属性进行遍历,如果key不在新的属性中,那么就代表它是不需要存在的,,如果是事件通过removeEventListener移出事件,如果是属性通过removeAttribute移除属性
这样我们就把tag和props处理完成,下一步处理children
children的处理
这里逻辑稍微繁琐一点,需要判断很多
当新节点的children是string类型
当新节点的children是string类型说明我们不需要管之前标签里面是什么,直接调用innerHTML就可以
但是如果oldChildren也是字符串,调用textContent会更加快速
const newChildren = n2.children
const oldChildren = n1.children
// 判断newChildren是不是字符串
if (typeof newChildren === "string") {
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) { //判断两者是否一样,一样就不需要改
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
}
当新节点的children是array类型
是array类型说明他有子节点
此时,需要先判断旧节点的children是什么类型,如果是string类型,就把innerHTML设置为空,然后遍历newChildren,调用mount函数,挂载
旧节点的children为string类型
const newChildren = n2.children
const oldChildren = n1.children
if (typeof newChildren === "string") {
//此处为判断newChildren是字符串代码,在上文
} else {
if (typeof oldChildren === "string") {
el.innerHTML = ""
newChildren.forEach(item => {
mount(item,el)
});
}
}
旧节点的children为array类型
到这个时候就需要进行diff算法,但是我只懂皮毛,而且我们也没传key,所以只能简单实现一下,见谅
我们默认新旧children的值一一对应,即oldChildren的第一个值为1,那么下一次传值来的时候newChildren第一个值也是1,如果不是1,就是不相同,需要更改,不去考虑后面是否有相同的
简单说就是相同的下标代表同一个key
思路:
- 取出新旧的children的长度,拿到最小的长度,然后对新旧节点进行patch算法,也就也是回调我们的patch函数,
- 如果newChildren.length > oldChildren.length 那么代表我们新传进来节点多于oldChildren,就可以把多出来的全部加入到节点中
- 如果newChildren.length < oldChildren.length 那么代表我们的oldChildren多了,就要把多余的全部移除
const newChildren = n2.children
const oldChildren = n1.children
// 判断newChildren是不是字符串
if (typeof newChildren === "string") {
//此处为判断newChildren是字符串代码,在上文
} else {
if (typeof oldChildren === "string") {
//旧节点的children为string类型
} else { //通过Math.min方法获取最小的长度
const commonLength = Math.min(newChildren.length,oldChildren.length)
// 取出两个的公共长度,进行diff算法
for (let i = 0; i < commonLength; i++){
patch(oldChildren[i],newChildren[i])
}
// 如果newChildren > oldChildren newChildren多余的添加
if (newChildren.length > oldChildren.length) {
//截取数组,遍历添加节点
newChildren.slice(commonLength).forEach((item)=>{
mount(item,el)
})
}
// 如果newChildren < oldChildren oldChildren多余的删除
if (newChildren.length < oldChildren.length) {
//截取数组,遍历删除节点
oldChildren.slice(commonLength).forEach((item)=>{
el.removeChild(item.el)
})
}
}
}
patch总代码
const patch = (n1,n2)=>{
const el = n2.el = n1.el
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement
n1ElParent.removeChild(n1.el)
mount(n2,n1ElParent)
} else {
// 把newProps全部添加到el
const newProps = n2.props
const oldProps = n1.props
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
if (newValue !== oldValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(),newValue)
} else {
el.setAttribute(key,newValue)
}
}
}
// 把oldProps多余的属性删除
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(),oldProps[key])
} else {
el.removeAttribute(key)
}
}
}
const newChildren = n2.children
const oldChildren = n1.children
// 判断newChildren是不是字符串
if (typeof newChildren === "string") {
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else {
if (typeof oldChildren === "string") {
el.innerHTML = ""
newChildren.forEach(item => {
mount(item,el)
});
} else {
const commonLength = Math.min(newChildren.length,oldChildren.length)
// 取出两个的公共长度,把不同的重新设置
for (let i = 0; i < commonLength; i++){
patch(oldChildren[i],newChildren[i])
}
// 如果newChildren > oldChildren newChildren多余的添加
if (newChildren.length > oldChildren.length) {
newChildren.slice(commonLength).forEach((item)=>{
mount(item,el)
})
}
// 如果newChildren < oldChildren oldChildren多余的删除
if (newChildren.length < oldChildren.length) {
oldChildren.slice(commonLength).forEach((item)=>{
el.removeChild(item.el)
})
}
}
}
}
}
界面效果
我们设置个定时器,等到3秒后执行patch函数
<script>
const vnode = h("div",{class:"scc",id:"iii",onClick:function(){console.log(2);}},[
h("span",null,"当前计数:100"),
h("button",{onClick:function(){console.log(+1);}},"+1"),
h("button",{onClick:function(){console.log(-1);}},"-1")
])
mount(vnode,document.querySelector("#app"))
setTimeout(()=>{
const vnode1 = h("div",{class:"aaa"},[
h("span",null,"求和:100"),
h("button",{onClick:function(){console.log("patch已执行");}},"+1"),
h("button",{onClick:function(){console.log("patch已执行");}},"-1"),
h("button",{onClick:function(){console.log("patch已执行");}},"patch")
])
patch(vnode,vnode1)
},3000)
</script>
问题
但是还有一个问题,不知道大家发现了没有,就是之前按钮的click函数没有取消
他判断我们的newValue !== oldValue,所以会把新的事件添加,但是后续的对oldValue删除时,判断在newChildren的children中是存在的,没有删除,不知道有没有知道怎么解决的小伙伴,请教一下
添加事件
此处的key为onClick
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key]
if (newValue !== oldValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(),newValue)
} else {
el.setAttribute(key,newValue)
}
}
}
删除事件
此处的key为onClick
for (const key in oldProps) {
if (!(key in newProps)) { 此处判断出问题,绑定了多个click事件
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(),oldProps[key])
} else {
el.removeAttribute(key)
}
}
}