认识虚拟DOM和diff算法

899 阅读7分钟

一、认识虚拟dom

1、在没有vuereact之前,我们都使用的是原生的javascriptjq,我们都是通过操作dom,来达到视图更新的效果。(操作dom->视图更新)

2、而在vue中,我们只需更改data中的数据,视图就会更新,所以在vue中,简单说就是数据改变,让视图更新。

可我们有没有想过为什么数据改变,视图就更新呢?其实vue更新视图,也还是操作了dom,只不过是在vuereact框架内部来完成的。(数据改变->操作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有什么好处呢?它好在什么地方呢?别着急,这就来。

在这之前,先介绍一个东西。Snabbdomvue里面的虚拟dom就是借助它来完成的

在官方github上,我们可以看到他下面的例子

主要的目的,就是通过之前那个js对象结构来表示页面的一些节点,来插入到他的空的容器里。

var container = document.getElementById('container')

他里面是用vnode这个变量来表示的,那个vnode的结构,就是之前所说的js dom结构。然后表示出来的节点之后,会通过一个叫Patch的方法,将虚拟节点vnode塞到某一个容器里。

我们可以照着它这个来写一个小demo,认识snabbdomvnode的好处:

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。那么它会怎么做呢?

  1. 遍历旧的虚拟dom

  2. 遍历新的虚拟dom

  3. 重新排序

比如上图,有一个改变,和一个新增,那么他会根据这个变化,来重新编排这个虚拟dom。

but……,没错,可恶的但是来了,如果我们只是为了改一小部分,可是节点数又非常多,有1000个节点,那么他就会计算1000^3,也就是10亿次,显然这不是我们的初衷也是不可接受的。

所以vuereact内部里做diff算法的时候,内部是做过优化的,我们可以来see see他们是怎么做的优化。

原本的diff算法比较,是先遍历旧的vnode,然后遍历新的vnode,然后对比,最后再重新编排。

vuereact的是,

  1. 只比较同一层级,不做跨级比较。

  2. 比较标签名,如果标签名不同,直接删除,不会继续深度比较。

  3. 标签名相同,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: childrentext只能有一个,要么是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的时候为什么要有了。

最后如果大家有什么好的建议或想法,也欢迎交流。