虚拟dom和diff算法--2(手写h函数)

530 阅读6分钟

写在前面:文章来源于Vue源码解析系列课程_哔哩哔哩_bilibili侵删

手写h函数:看ts代码,写js代码

1.看ts代码

首先在node_modules中找到snabbdom,src中存放的是ts代码,build中存放的是js代码
 找到h.js
 

image.png 最终是使用vnode函数返回了一个东西,接下来看vnode.ts

image.png 所以h函数最终返回的是一个对象
接着往上看,因为上一个笔记中写到的,children属性中是子元素,子元素又可以嵌套:

image.png 所以:

image.png 一层一层去调用vnode()
在往上走,一整块都在进行函数的重载(一个函数有多个用法,不同情况下不同的入参)

image.png h函数可能的重载情况:

image.png 改写的时候只书写三种情况(不写那么多重载)

image.png 第三种情况的第三个参数的h()函数可能又囊括到了第一种和第二种情况
写好以后的逻辑

export default function(sel,data,c){

// 第一个参数一定是选择器,第二个一定是属性对象

// h('div',,{},'text')

// h('div',{},[])

// h('div',{},h())

// 也就是说,调用的时候形态必须是上面这三种之一

// debugger

if(arguments.length !=3){

throw new Error('对不起,h函数必须传入三个参数,我们是低配版的h函数')

}

if(typeof c =='string'||typeof c =='number'){

// 说明h函数是形态1

return vnode(sel,data,undefined,c,undefined);

}else if(Array.isArray(c)){

// 形态2

// 遍历c

let children = [];

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

// 检查c[i]必须是一个对象

if(!(typeof c[i] =='object' && c[i].hasOwnProperty('sel')))

throw new Error('传入的数组参数中有项不是h函数');

children.push(c[i])

}

// 循环结束了就说明children收集完毕了,此时就可以返回虚拟节点了。它有children属性的

\


return vnode(sel,data,children,undefined,undefined)//我们做了一个简化版,只有三个参数

\


}else if(typeof c == 'object' && c.hasOwnProperty('sel')){

// 形态3

// 就是传入的c是唯一的children,因为调用h函数的时候(index.js)就直接给的参数是h函数,所以不用特意去调用h函数了

let children = [c];

return vnode(sel,data,children,undefined,undefined)

}else{

throw new Error("传入的参数不对")

}

}

到目前为止呢,三种不同入参的h函数就写好了

接下来研究diff算法

假设一个场景:页面中原本有如下布局:(场景1)

image.png
单机button按钮之后,页面更改为(场景2)

image.png
我们前面有说过,我们的思路是:
1.使用h函数得到虚拟节点
2.使用patch函数使得得到的虚拟节点上树
我们先来完成下场景1:
1.先得到容器container

image.png

image.png 2.得到场景1的虚拟节点

const vnode1 = h('ul', {}, [

h('li', {}, 'A'),

h('li', {}, 'B'),

h('li', {}, 'C'),

h('li', {}, 'D'),

])

3.让vnode1上树

patch(container, vnode1)

4.得到场景2的虚拟节点

const vnode2 = h('ul', {}, [

h('li', {}, 'A'),

h('li', {}, 'B'),

h('li', {}, 'C'),

h('li', {}, 'D'),

h('li', {}, 'E'),

])

5.编写button按钮

<button id="btn">按我改变dom</button>

6.编写button按钮的单击事件

btn.onclick = function () {

patch(vnode1, vnode2)//vnode2替换掉vnode1了,但是这时候的代码还是有bug的,多次单击按钮,会重复增加E(这个问题留到学习patch函数的底层逻辑的时候去解决)

}

就可以得到我们想要的结果了。需要注意的是,我们在前面有说过,patch函数一次只能有一个容器(container),在这里容器并没有发生改变,发生改变的是容器内的内容。
那我们是怎么完成对容器内的内容的改变的呢,在这里就使用的是diff算法(最小量更新)
就是说场景1和场景2的ABCD(一样的内容)是一样的,只有E是场景2追加的

咋证明呢

来一个最简单的证明方法:

image.png 在单击按钮之前,我们在浏览器上进行简单的更改(邵山欢老师称之为丑八怪实验法...哈哈哈)

image.png 现在单击按钮:

image.png 页面并没有重新进行渲染,是在场景1的基础上进行最小量更新,保留了场景1的ABCD,在这个基础上更新了场景2的E!
但是要注意,假设我场景1还是场景1,场景2更改为如下:

const vnode2 = h('ul', {}, [

h('li', {}, 'E'),

h('li', {}, 'A'),

h('li', {}, 'B'),

h('li', {}, 'C'),

h('li', {}, 'D'),

])

此时再用刚才的方法测试,你会发现此时整个ABCDE都进行了重新渲染,原本的A变为场景2的E,场景1的B变为场景2的A...以此类推(就是说这个函数并不是我们以为的那种智能的函数,不符合我们的要求)
那要怎么办呢
给虚拟节点增加key值
我们将场景1更改为如下代码:

const vnode1 = h('ul', {}, [

h('li', {key:'A'}, 'A'),

h('li', {key:'B'}, 'B'),

h('li', {key:'C'}, 'C'),

h('li', {key:'D'}, 'D'),

])

将场景2更改为如下代码:

const vnode2 = h('ul', {}, [

h('li', {key:'E'}, 'E'),

h('li', {key:'A'}, 'A'),

h('li', {key:'B'}, 'B'),

h('li', {key:'C'}, 'C'),

h('li', {key:'D'}, 'D'),

])

(注意此时的E还是在ABCD前面)
再来进行测试

image.png 单击按钮

image.png 牛蛙牛蛙牛蛙牛蛙!!!!!!!!瞬间智能了(小小的key大大的功能)最小量更新绝绝子

心得1:key(唯一标识)服务于最小量更新的(diff),非常非常非常非常关键~!

再来进行下一个:
场景1:\

const vnode1 = h('ul', {}, [

h('li', {key:'A'}, 'A'),

h('li', {key:'B'}, 'B'),

h('li', {key:'C'}, 'C'),

h('li', {key:'D'}, 'D'),

])

场景2:

const vnode2 = h('ol', {}, [

h('li', {key:'A'}, 'A'),

h('li', {key:'B'}, 'B'),

h('li', {key:'C'}, 'C'),

h('li', {key:'E'}, 'D'),

])

注意了,我们是将场景1的ul更改为ol标签了
还是通过丑八怪实验法得知,此时的ABCD节点已经不是原来的ABCD节点了(哪怕我们给节点进行了key值绑定),因为ABCD节点们的父节点ul和ol不同,导致认为不是同一个节点。

心得2:只有是同一个虚拟节点的前提下,才能进行精细化比较,否则就是暴力删除旧的,插入新的。

延伸问题:如何定义是同一个虚拟节点? 答案:选择器相同并且key值相同 什么意思呢,就是说我们上面这个例子,没有办法进行diff算法了。因为就算你对两个场景都设置了同样的key值,但是因为选择器必然会从ul更改为ol,所以不能进行最小量更新啦。
也就是说如果更改了父级,子级会整个重新渲染

心得3 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你。而是暴力删除旧的,然后再插入新的。

这又是啥意思呢,我们的场景1:\

const vnode1 = h('div', {key:'ul-ol'}, [

h('p', {key:'A'}, 'A'),

h('p', {key:'B'}, 'B'),

h('p', {key:'C'}, 'C'),

h('p', {key:'D'}, 'D'),

])

我们的场景2:

const vnode2 = h('div', {key:'ul-ol'}, h('section',{},[

h('p', {key:'E'}, 'E'),

h('p', {key:'A'}, 'A'),

h('p', {key:'B'}, 'B'),

h('p', {key:'C'}, 'C'),

h('p', {key:'D'}, 'D'),

]))

就是说在原本的节点基础上,增加了一层section标签,将ABCD节点使用section节点包裹起来了
还是根据丑八怪实验法,得到结论:ABCD不是原本的ABCD了
而在同一层节点上,不管你是ABCD还是BADC巴拉巴拉,是可以随便比较的
比方说:
场景1:

const vnode1 = h('ul', {key:'ul-ol'}, [

h('li', {key:'A'}, 'A'),

h('li', {key:'B'}, 'B'),

h('li', {key:'C'}, 'C'),

h('li', {key:'D'}, 'D'),

])

场景2:

const vnode2 = h('ul', {key:'ul-ol'}, [

h('li', {key:'E'}, 'E'),

h('li', {key:'D'}, 'D'),

h('li', {key:'C'}, 'C'),

h('li', {key:'B'}, 'B'),

h('li', {key:'A'}, 'A'),

])

再经过丑八怪实验法,会发现哪怕顺序发生了改变,但是实际上节点还是那个节点,是没有发生改变的
单击按钮前:

image.png

image.png 单击按钮后:

image.png

如此看来,diff并不是那么的"无微不至"智能化啊,那么会不会影响效率呢?
这种操作在实际的vue开发中,基本不会遇见,所以这是合理的优化机制。

最后,假设说面试官问你,你来聊一聊diff算法,你可以这么聊
1.虚拟节点
2.key值的重要性,只有同一个虚拟节点才能进行diff比较,那什么是同一个虚拟节点呢:选择器和key都要相同
3.diff只进行同层比较,不会进行跨层比较(哪怕有同样的key值)在同一层,最小量更新是最省的!!!!效率是最高的!!!(只要是同一层,哪怕顺序变了,但是只是同一个节点前后顺序发生改变罢了,不会暴力删除旧的再写新的,是前后就是同一个)