vue源码-vue虚拟dom和diff对比
Posted suwu150
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue源码-vue虚拟dom和diff对比相关的知识,希望对你有一定的参考价值。
vue源码(五)-vue虚拟dom和diff对比
一、虚拟DOM
1、概念
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用程序 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
2、优点
虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从 而提升性能和用户体验。
跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
兼容性:还可以加入兼容性代码增强操作的兼容性
3、必要性
vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大 型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例, 这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
4、源码实现
通过文章vue源码(四)-vue项目配置和入口文件,数据响应化处理分析发现,有虚拟dom挂载真实dom是由new Vue() => _init() => $mount()
这样一个执行流程进行的。而$mount
最终的实现代码来源于src/core/instance/lifecycle.js文件的mountComponent
查看该方法,发现196-202行处理了updateComponent
方法,updateComponent
的定义是189行。
在updateComponent
方法中发现调用顺序是_render
然后是_update
。 其中_render
就是生成虚拟dom,然后由_update
更新到真实dom中。继续查找_render
方法,打开文件src/core/instance/render.js
,查看_render
定义,可以看到83行定义了vnode,在89行通过传递参数vm.$createElement
调用render方法生成虚拟dom。
再来查看文件src/core/vdom/create-element.js
,99行开始进行虚拟dom的创建,首先对传入的标签tag进行判断,由于创建时会存在原生标签和自定义标签两种情况,所以通过判断是否为字符串进行处理。具体如下所示实现:
当tag为字符串的时,判断是否为原生标签,通过new Node()
方式进行创建虚拟dom,如果不是原生标签,则进行判断是否为自定义组件,按照自定义组件进行处理,当以上条件不符合时,直接通过createComponent
进行创建虚拟dom。查看src/core/vdom/create-component.js
文件中createComponent方法,最终仍然是返回通过new VNode()
创建的虚拟dom。
下面我们使用真实代码生成虚拟dom打印查看下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Vue.js elastic header example</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
message
</div>
<script>
new Vue(
el: '#app',
data:
message: 'hello world!初始化流程和入口文件!'
)
</script>
</body>
</html>
最终生成虚拟dom如下:
二、diff对比
生成的虚拟dom,最终是需要更新到真实dom中的,从之前代码查看过程中,我们能够知道最终的diff位于文件core\\vdom\\patch.js
,通过调用方法createPatchFunction进行渲染。
- patch中通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只 有O(n),是一种相当高效的算法。
- 同层级只做三件事:增删改。具体规则是:new VNode不存在就删;old VNode不存在就增;都存在就 比较类型,类型不同直接替换、类型相同执行更新;
该文件中方法patchVnode
进行执行更新操作:
两个VNode类型相同,就执行更新操作,包括三种类型操作:属性更新PROPS、文本更新TEXT、子节点更新REORDER
patchVnode具体规则如下:
- 如果新旧VNode都是静态的,那么只需要替换elm以及componentInstance即可。
- 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
- 如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节
点加入子节点。 - 当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
- 当新老节点都无子节点的时候,只是文本的替换。
文件中updateChildren
主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化
在新老两组VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。 当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。
下面是遍历规则:
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉比较,共有4种比较 方法。
- 当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满足sameVnode,直接将该 VNode节点进行patchVnode即可,不需再遍历就完成了一次循环。
- 如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode 后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
- 如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode 后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
- 如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前 面,进行patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前面。
如果以上情况均不符合,则在old VNode中找与newStartVnode满足sameVnode的vnodeToMove,若 存在执行patchVnode,同时将vnodeToMove对应DOM移动到oldStartVnode对应的DOM的前面。当然也有可能newStartVnode在old VNode节点中找不到一致的key,或者是即便key相同却不是 sameVnode,这个时候会调用createElm创建一个新的DOM节点。459行是执行createElm过程。
至此循环结束,但是我们还需要处理剩下的节点。
当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM 中,此时调用addVnodes(批量调用createElm接口)。
但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有 剩余,需要从文档中删 的节点删除。
以上是关于vue源码-vue虚拟dom和diff对比的主要内容,如果未能解决你的问题,请参考以下文章