虚拟资本与虚拟概论(如何理解虚拟DOM)
一 、js 操作DOM
假如现在你需要写一个像下面一样的表格的应用程序 ,这个表格可以根据不同的字段进行升序或者降序的展示 。
这个应用程序看起来很简单 ,你可以想出好几种不同的方式来写 。最容易想到的可能是,在你的 JavaScript 代码里面存储这样的数据:
var sortKey = "new" // 排序的字段 ,新增(new) 、取消(cancel) 、净关注(gain) 、累积(cumulate)人数 var sortType = 1 // 升序还是逆序 var data = [{...}, {...}, {..}, ..] // 表格数据用三个字段分别存储当前排序的字段 、排序方向 、还有表格数据;然后给表格头部加点击事件:当用户点击特定的字段的时候 ,根据上面几个字段存储的内容来对内容进行排序 ,然后用 JS 或者 jQuery 操作 DOM ,更新页面的排序状态(表头的那几个箭头表示当前排序状态 ,也需要更新)和表格内容。
这样做会导致的后果就是 ,随着应用程序越来越复杂 ,需要在JS里面维护的字段也越来越多 ,需要监听事件和在事件回调用更新页面的DOM操作也越来越多 ,应用程序会变得非常难维护 。后来人们使用了 MVC 、MVP 的架构模式,希望能从代码组织方式来降低维护这种复杂应用程序的难度 。但是 MVC 架构没办法减少你所维护的状态 ,也没有降低状态更新你需要对页面的更新操作(前端来说就是DOM操作) ,你需要操作的DOM还是需要操作,只是换了个地方 。
既然状态改变了要操作相应的DOM元素 ,为什么不做一个东西可以让视图和状态进行绑定 ,状态变更了视图自动变更,就不用手动更新页面了 。这就是后来人们想出了 MVVM 模式 ,只要在模版中声明视图组件是和什么状态进行绑定的 ,双向绑定引擎就会在状态更新的时候自动更新视图 。
MVVM 可以很好的降低我们维护状态 -> 视图的复杂程度(大大减少代码中的视图更新逻辑) 。但是这不是唯一的办法 ,还有一个非常直观的方法 ,可以大大降低视图更新的操作:一旦状态发生了变化 ,就用模板引擎重新渲染整个视图 ,然后用新的视图更换掉旧的视图 。就像上面的表格 ,当用户点击的时候 ,还是在JS里面更新状态 ,但是页面更新就不用手动操作 DOM 了,直接把整个表格用模版引擎重新渲染一遍 ,然后设置一下innerHTML就完事了 。
听到这样的做法 ,经验丰富的你一定第一时间意识这样的做法会导致很多的问题 。最大的问题就是这样做会很慢,因为即使一个小小的状态变更都要重新构造整棵 DOM ,性价比太低;而且这样做的话 ,input和textarea的会失去原有的焦点。最后的结论会是:对于局部的小视图的更新,没有问题(Backbone就是这么干的);但是对于大型视图 ,如全局应用状态变更的时候 ,需要更新页面较多局部视图的时候 ,这样的做法不可取 。
但是这里要明白和记住这种做法 ,因为后面你会发现 ,其实 Virtual DOM 就是这么做的 ,只是加了一些特别的步骤来避免了整棵 DOM 树变更 。
另外一点需要注意的就是 ,上面提供的几种方法 ,其实都在解决同一个问题:维护状态 ,更新视图。在一般的应用当中,如果能够很好方案来应对这个问题 ,那么就几乎降低了大部分复杂性 。
DOM是很慢的 。如果我们把一个简单的div元素的属性都打印出来 ,你会看到:
而这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的 。而且操作它们的时候你要小心翼翼 ,轻微的触碰可能就会导致页面重排 ,这可是杀死性能的罪魁祸首 。
相对于 DOM 对象,原生的 JavaScript 对象处理起来更快 ,而且更简单 。DOM 树上的结构 、属性信息我们都可以很容易地用 JavaScript 对象表示出来:
var element = { tagName: ul, // 节点标签名 props: { // DOM的属性 ,用一个对象存储键值对 id: list }, children: [ // 该节点的子节点 {tagName: li, props: {class: item}, children: ["Item 1"]}, {tagName: li, props: {class: item}, children: ["Item 2"]}, {tagName: li, props: {class: item}, children: ["Item 3"]}, ] }上面对应的HTML写法是:
<ul id=list> <li class=item>Item 1</li> <li class=item>Item 2</li> <li class=item>Item 3</li> </ul>既然原来 DOM 树的信息都可以用 JavaScript 对象来表示 ,反过来 ,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树 。
之前的章节所说的 ,状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构 ,当状态变更的时候 ,重新渲染这个 JavaScript 的对象结构 。当然这样做其实没什么卵用 ,因为真正的页面其实没有改变 。
但是可以用新渲染的对象树去和旧的树进行对比 ,记录这两棵树差异 。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上 ,页面就变更了 。这样就可以做到:视图的结构确实是整个全新渲染了 ,但是最后操作DOM的时候确实只变更有不同的地方 。
这就是所谓的 Virtual DOM 算法。包括几个步骤:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 当状态变更的时候 ,重新构造一棵新的对象树 。然后用新的树和旧的树进行比较 ,记录两棵树差异 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存 。可以类比 CPU 和硬盘 ,既然硬盘这么慢 ,我们就在它们之间加个缓存:既然 DOM 这么慢 ,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM) ,最后的时候再把变更写入硬盘(DOM) 。
二 、虚拟DOM
虚拟 DOM (Virtual DOM )是由普通的JS对象来描述DOM对象 ,因为不是真实的DOM对象 ,所以叫Virtual DOM 。
实际上它只是一层对真实DOM的抽象 ,以JavaScript 对象 (VNode 节点) 作为基础的树 ,用对象的属性来描述节点 ,最终可以通过一系列操作使这棵树映射到真实环境上。
在Javascript对象中,虚拟DOM 表现为一个 Object对象 。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性 ,不同框架对这三个属性的名命可能会有差别 。
创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中 ,所以虚拟DOM对象的节点与真实DOM的属性一一照应 。
在vue中同样使用到了虚拟DOM技术,
定义真实DOM
<div id="app"> <p class="p">节点内容</p> <h3>{{ foo }}</h3> </div>实例化vue
const app = new Vue({ el:"#app", data:{ foo:"foo" } })观察render的render ,我们能得到虚拟DOM
(function anonymous() { with(this){return _c(div,{attrs:{"id":"app"}},[_c(p,{staticClass:"p"}, [_v("节点内容")]),_v(" "),_c(h3,[_v(_s(foo))])])}})通过VNode ,vue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单位,再更新视图 ,减少了dom操作 ,提高了性能 。
官网: 渲染函数 & JSX — Vue.js (vuejs.org)
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM 。请仔细看这行代码:
return createElement(h1, this.blogTitle)createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素 。它更准确的名字可能是 createNodeDescription ,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点 ,包括及其子节点的描述信息 。我们把这样的节点描述为“虚拟节点 (virtual node) ” ,也常简写它为“VNode ” 。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼 。
三 、为什么需要虚拟DOM?
模板引擎没有解决跟踪状态变化的问题是因为 ,当数据发生变化后 ,无法获取上一次的状态 ,只好把界面上的元素删除重新创建(可能造成闪烁 ,性能较低)。
具备跨平台的优势由于虚拟DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力 ,比如说浏览器平台 、Weex、Node 等 ,是实现ssr 、小程序等的基础 。
提升渲染性能 。因为DOM是一个很大的对象,直接操作DOM ,即便是一个空的 div 也要付出昂贵的代价 ,执行速度远不如我们抽象出来的 Javascript 对象的速度快,因此 ,把大量的DOM操作搬运到 Javascript 中 ,运用diff算法来计算出真正需要更新的节点 ,最大限度地减少DOM操作 ,从而显著提高性能。虚拟DOM的优势不在于单次的操作 ,而是在大量 、频繁的数据更新下 ,能够对视图进行合理、高效的更新 。
四 、如何利用虚拟DOM来更新真实DOM?—— diff
diff(different) ,顾名思义 ,在构建DOM的过程中 ,会由diff过程就是比对计算DOM变动的地方,核心是由patch算法将变动映射到真实DOM上 ,所以视图的创建更新流程就是下面这样:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树 ,插到文 档当中 当状态变更的时候,重新构造一棵新的对象树 。然后用新的树和旧的树进行比较(diff过程) ,记录两棵树差异 把2所记录的差异应用到步骤1所构建的真正的DOM树上(patch) ,视图就更新了创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!