一、认识虚拟dom
1、在没有vue
和react
之前,我们都使用的是原生的javascript
或jq
,我们都是通过操作dom
,来达到视图更新的效果。(操作dom->视图更新)
2、而在vue
中,我们只需更改data
中的数据,视图就会更新,所以在vue
中,简单说就是数据改变,让视图更新。
可我们有没有想过为什么数据改变,视图就更新呢?其实vue
更新视图,也还是操作了dom
,只不过是在vue
或react
框架内部来完成的。(数据改变->操作dom->视图更新)
但这就引出了一个问题,难道vue
每次数据更改,都会操作dom
呢,显然这不是最优解,所以就推出了虚拟dom
这个东西。
什么是虚拟dom
?虚拟dom
就是用js
来模拟dom
结构。
举个栗子:
<div id="app">
<h1>认识虚拟dom</h1>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
以上例子中的dom结构,如果用js来表达是什么样的呢?
用js来表达dom,他会先声明一个对象,然后有三个属性:
tag : 元素;
props: 属性或者事件;
children: 子节点或者内容;
{ tag: 'div',
props: { id: 'app', className: 'container' },
children: [
{ tag: 'h1', children: '认识虚拟dom' },
{ tag: 'ul', props: {}, children: [
{ tag: 'li', props: '1', },
{ tag: 'li', props: '2', },
{ tag: 'li', props: '3', },
]
}
]
}
这样一来,我们就用js来表达了dom结构。
那我们要怎么样用虚拟dom来计算我们的变更,来操作真实的dom呢?
二、虚拟DOM的好处
扯了这么多,其实就是为了让我们知道虚拟dom,那到底虚拟dom有什么好处呢?它好在什么地方呢?别着急,这就来。
在这之前,先介绍一个东西。Snabbdom
,vue
里面的虚拟dom就是借助它来完成的
在官方github
上,我们可以看到他下面的例子
主要的目的,就是通过之前那个js对象结构来表示页面的一些节点,来插入到他的空的容器里。
var container = document.getElementById('container')
他里面是用vnode
这个变量来表示的,那个vnode
的结构,就是之前所说的js dom
结构。然后表示出来的节点之后,会通过一个叫Patch
的方法,将虚拟节点vnode塞到某一个容器里。
我们可以照着它这个来写一个小demo,认识snabbdom
和vnode
的好处:
1、先引入snabbdom
这个库
const snabbdom = window.snabbdom;
在这之前,偷了一下懒,直接cnd
引入了
<script src="https://lib.baomitu.com/snabbdom/0.7.4/h.js"></script>
<script src="https://lib.baomitu.com/snabbdom/0.7.4/snabbdom-class.js"></script>
<script src="https://lib.baomitu.com/snabbdom/0.7.4/snabbdom-eventlisteners.js"></script>
<script src="https://lib.baomitu.com/snabbdom/0.7.4/snabbdom-props.js"></script>
<script src="https://lib.baomitu.com/snabbdom/0.7.4/snabbdom-style.js"></script>
<script src="https://lib.baomitu.com/snabbdom/0.7.4/snabbdom.js"></script>
2、将vnode(虚拟节点)塞到容器中
const patch = snabbdom.init({ classModule, propsModule, styleModule, eventListenersModule,})
3、创建vnode,也就是虚拟dom,(引入h函数,通过h函数来创建)
const h = snabbdom.h
4、在页面上声明一个空的容器,然后在js选中这个容器
<div id="container"></div>//一个空的容器
const container = document.getElementById('container')
5、创建vnode
let vnode = h("ul#list", {}, [ h('li.item', {}, '第一个'), h('li.item', {}, '第二个'), h('li.item', {}, '第三个'),])
6、使用patch函数,将vnode塞到容器中,第一个参数:容器。第二个参数:虚拟节点
patch(container, vnode)
7、可以看到已经将那几个元素插入空的container里了
如果我需要点击一个按钮,来改变第二个的内容,需要怎么做呢?
8、先在页面上添加一个button
,然后js选中他,添加click监听事件
<button id="btn">点击</button>const btn = document.getElementById('btn')btn.addEventListener('click', ()=> { const newVnode = h('ul#list', {}, [ h('li.item', {}, '第一个'), h('li.item', {}, '第二个,我变了'), h('li.item', {}, '第三个'), h('li.item', {}, '第四个'), ]) patch(vnode, newVnode) vnode = newVnode})
值得注意的是,点击之后,需传入一个新的vnode
,然后在创建完vnode
之后,再执行patch函数,用新的vnode
来更新老的vnode
,同时将新的vnode
赋值vnode
,方便下次用最新结果比较。
点击之前:
点击之后:
可以看到第二个和第四个已经变了,因为第一个和第三个是没变了,所以他并没有重新渲染那两个节点。
结论:所以到这我们就知道了,虚拟dom会去计算你有没有变化,从而去决定要不要去操作你那个dom
三、认识diff算法
之前说过,虚拟dom
就是计算节点是否变更修改,来判断是否操作dom
元素来更新视图。
那么这个计算,就是用的diff
算法计算的。所以diff
算法是虚拟dom
的核心。
我们用虚拟dom来表述真实的dom,这样做的目的,就是为了计算最小的变化,根据这个最小的变化,来更新真实的dom结构。
如上图,有两个虚拟dom,我们既要用diff算法,来比较更新我们的虚拟dom。那么它会怎么做呢?
-
遍历旧的虚拟dom
-
遍历新的虚拟dom
-
重新排序
比如上图,有一个改变,和一个新增,那么他会根据这个变化,来重新编排这个虚拟dom。
but……,没错,可恶的但是来了,如果我们只是为了改一小部分,可是节点数又非常多,有1000个节点,那么他就会计算1000^3,也就是10亿次,显然这不是我们的初衷也是不可接受的。
所以vue
和react
内部里做diff
算法的时候,内部是做过优化的,我们可以来see see他们是怎么做的优化。
原本的diff
算法比较,是先遍历旧的vnode
,然后遍历新的vnode
,然后对比,最后再重新编排。
而vue
和react
的是,
-
只比较同一层级,不做跨级比较。
-
比较标签名,如果标签名不同,直接删除,不会继续深度比较。
-
标签名相同,key相同,就认为是相同节点,不继续深度比较。(所以大家知道我们写v-for循环的时候,为啥提示要加上key了吧)
所以通过以上的步骤,vue将diff算法给优化了一波
现在如果有1000个节点,就只需计算1000次了,简直nice。如下图:
四、snabbdom之生成vnode源码简述
我们可以看到snabbdom/src/package/h.ts
我们是通过h函数来生产vnode的,可以看到这个h方法,能接受好几种情况的传参,
再看github上官方的栗子:
可以看到,这里传了sel
data
children
,最后通过
return vnode(sel, data, children, text, undefined)
来返回。我们执行这个h函数后,然后他往vnode里面传递参数,包括sel, data, children, text, undefined
,我们可以进一步看看vnode
函数的代码:路径:snabbdom/src/package/vnode.ts
可以看到这个vnode
函数,接受了一堆参数,那堆参数就是上面传递进来的参数。
最终,这个函数会返回一个对象,这个对象就是表示节点的对象。
我们来看下这个节点对象,需要的组成部分:
sel
: 选择器,比如div
data
: 比如style
onClick
children
: children
和text
只能有一个,要么是children
数组包含子元素,要么text
字符串内容
elm
: 对应的真实的dom
元素,比如将new vnode
替换掉old vnode
,那个old vnode
就是elm
key
: v-for
所需声明的那个属性
执行完后,会将这些参数用对象的形式返回回去,返回到h函数那里,所以我们执行了h函数之后,就会生成对应的vnode
结构,然后我们就把vnode
结构,保存在变量中使用。
五、总结
我们今天所学的虚拟dom和diff算法,主要围绕snabbdom来学习虚拟dom,它主要的一些核心方法有这几个:vnode h patch,如果大家掌握了这些,面试的时候如果有虚拟dom的问题,也应该心里有底了。还有patch函数中的diff算法,还有那个key,大家也应该知道v-for的时候为什么要有了。
最后如果大家有什么好的建议或想法,也欢迎交流。