详解Vue中的虚拟DOM
摘要: 什么是虚拟dom?
- 作者:
fundebug经授权转载,版权归原作者所有。
前言
vue.js 2.0引入virtual dom,比vue.js 1.0的初始渲染速度提升了2-4倍,并大大降低了内存消耗。那么,什么是virtual dom?为什么需要virtual dom?它是通过什么方式去提升页面渲染效率的呢?这是本文所要探讨的问题。
模板转换成视图的过程
在正式介绍 virtual dom之前,我们有必要先了解下模板转换成视图的过程整个过程(如下图):
- vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
- 在对 model 进行操作的时候,会触发对应 dep 中的 watcher 对象。watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行dom操作来更新视图。
简单点讲,在vue的底层实现上,vue将模板编译成虚拟dom渲染函数。结合vue自带的响应系统,在状态改变时,vue能够智能地计算出重新渲染组件的最小代价并应到dom操作上。
我们先对上图几个概念加以解释:
- 渲染函数:渲染函数是用来生成virtual dom的。vue推荐使用模板来构建我们的应用界面,在底层实现中vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
- vnode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createelement 方法能将 vnode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的dom节点。
- patch(也叫做patching算法):虚拟dom最核心的部分,它可以将vnode渲染成真实的dom,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有dom上进行修改来实现更新视图的目的。vue的virtual dom patching算法是基于snabbdom的实现,并在些基础上作了很多的调整和改进。
virtual dom 是什么?
virtual dom 其实就是一棵以 javascript 对象( vnode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 dom 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
简单来说,可以把virtual dom 理解为一个简单的js对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。
对于虚拟dom,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟dom树 → 真实dom
的一个过程
virtual dom 作用是什么?
虚拟dom的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的dom操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的dom操作而造成了性能上的浪费。
为了避免不必要的dom操作,虚拟dom在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldvnode)做对比,找出真正需要更新的节点来进行dom操作,从而避免操作其他无需改动的dom。
其实虚拟dom在vue.js主要做了两件事:
- 提供与真实dom节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldvnode进行对比,然后更新视图
给大家推荐一个好用的bug监控工具fundebug,欢迎免费试用!
为何需要virtual dom?
- 具备跨平台的优势
由于 virtual dom 是以 javascript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、weex、node 等。
- 操作 dom 慢,js运行效率高。我们可以将dom对比操作放在js层,提高效率。
因为dom操作的执行速度远不如javascript的运算速度快,因此,把大量的dom操作搬运到javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少dom操作,从而显著提高性能。
virtual dom 本质上就是在 js 和 dom 之间做了一个缓存。可以类比 cpu 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 dom 这么慢,我们就在它们 js 和 dom 之间加个缓存。cpu(js)只操作内存(virtual dom),最后的时候再把变更写入硬盘(dom)
- 提升渲染性能
virtual dom的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
为了实现高效的dom操作,一套高效的虚拟dom diff算法显得很有必要。我们通过patch 的核心----diff 算法,找出本次dom需要更新的节点来更新,其他的不更新。比如修改某个model 100次,从1加到100,那么有了virtual dom的缓存之后,只会把最后一次修改patch到view上。那diff 算法的实现过程是怎样的?
diff 算法
vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个dom树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从o(n3)变成o(n)。
diff 算法包括几个步骤:
- 用 javascript 对象结构表示 dom 树的结构;然后用这个树构建一个真正的 dom 树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把所记录的差异应用到所构建的真正的dom树上,视图就更新了
diff 算法的实现过程
diff 算法本身非常复杂,实现难度很大。本文去繁就简,粗略介绍以下两个核心函数实现流程:
- patch(container,vnode) :初次渲染的时候,将vdom渲染成真正的dom然后插入到容器里面。
- patch(vnode,newvnode):再次渲染的时候,将新的vnode和旧的vnode相对比,然后之间差异应用到所构建的真正的dom树上。
1. patch(container,vnode)
通过这个函数可以让vnode渲染成真正的dom,我们通过以下模拟代码,可以了解大致过程:
function createelement(vnode) { var tag = vnode.tag var attrs = vnode.attrs || {} var children = vnode.children || [] if (!tag) { return null } // 创建真实的 dom 元素 var elem = document.createelement(tag) // 属性 var attrname for (attrname in attrs) { if (attrs.hasownproperty(attrname)) { // 给 elem 添加属性 elem.setattribute(attrname, attrs[attrname]) } } // 子元素 children.foreach(function (childvnode) { // 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。 elem.appendchild(createelement(childvnode)) // 递归 }) // 返回真实的 dom 元素 return elem }
2. patch(vnode,newvnode)
这里我们只考虑vnode与newvnode如何对比的情况:
function updatechildren(vnode, newvnode) { var children = vnode.children || [] var newchildren = newvnode.children || [] // 遍历现有的children children.foreach(function (childvnode, index) { var newchildvnode = newchildren[index] // 两者tag一样 if (childvnode.tag === newchildvnode.tag) { // 深层次对比,递归 updatechildren(childvnode, newchildvnode) } else { // 两者tag不一样 replacenode(childvnode, newchildvnode) } } )}
给大家推荐一个好用的bug监控工具fundebug,欢迎免费试用!
参考
关于fundebug
fundebug专注于javascript、微信小程序、微信小游戏、支付宝小程序、react native、node.js和java线上应用实时bug监控。 自从2016年双十一正式上线,fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝fm、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家!