一不小心就过去了半年了,这么久以来都没有更新博客了,这半年也从开始的页面仔逐渐的变成一个jser,之前有人和我说前端没必要看算法。。。那些是后端的事情,前端的需求没有那么复杂等等。。。但随着2015年前端圈爆炸式的增长,越来越多的框架开始层出不穷的冒了出来,而对于我们前端人员的要求也从开始的切切图拼页面到现在的关注性能,关注工程化了。
而对于2016年,对抢眼的莫过于ReactJS了,从0.11开始关注和使用到现在的v15,版本迭代了多次,API改了无数,但最根本的几点却依旧没变,虚拟DOM,单项数据流,这些从React开始便存在的特点,直到现在都没有改变过,然而当我在实际项目中使用的时候却发现虽然这些框架都很好,可是推广起来却着实头疼。。。很多新手发现接手的代码和自己之前印象里的前端代码完全不在一个次元。。。
今天,借着这次机会我就总结一下自己对于React的虚拟DOM这部分的理解~说真的。。千万不要以为数据结构对前端没用。。。
首先我们来谈谈什么是虚拟DOM,以及为什么要使用虚拟DOM,其实很多时候我们都在或多或少的有过接触这个概念,我们知道当我们需要直接操作DOM节点的时候会遇到如下几个问题:
1,频繁的对DOM树进行操作会导致性能下降严重。
2,当我们需要一次修改很多地方的时候总会有地方被我们忽略或者忘记修改。
那么针对上面我们最常遇到的两点问题我们该用什么思路去解决呢?
对于第一点,我们就需要先知道为什么频繁的操作会导致性能下降,说道这个就要不得不提到前端面试中经常问到的优化方式--repaint(重绘)和reflow(渲染),那么对于这一点该具体如何理解和优化这位博友说的很好了,有兴趣的同学可以看看他的这篇文章,那么我们今天就要从js上面下手去解决频繁的操作这个问题了,那么我们该如何减少渲染次数和范围呢?最简单直接的方法就是只渲染改动的地方,那么怎么才能知道哪里是改动的地方呢?对于这个问题,熟悉angularJS的同学一定知道angularJS中使用的“脏检查”机制,就是拿前后的DOM树去做对比,然而angularJS一直以来被人诟病的也就是这个了,性能差,消耗资源高等等,无一不是硬伤,那么我们该怎么曲线救国呢?React告诉我们的是在内存中维护一颗和页面一样的DOM树,这颗DOM树不是真正渲染在html中的,而是放在内存中的,因此修改它将会特别的快,并且资源消耗也少很多,当我们render一个页面的时候首先先将我们最新的DOM去和内存中的这棵虚拟DOM树去做对比(脏检查),然后对比出差一点,然后再用这棵虚拟DOM去整体替换html中实际的那个DOM树,这样其实我们只是做了一次渲染,无论我们改了多少,改了什么,我们都只是渲染了一次(类似重新渲染一次页面)。
那么接下来我们知道了思路后就该动手去实践了,既然说到了虚拟DOM树是真实DOM树的一个镜像,那么我们该如何在内存中保存这棵树呢?又该用什么方式去存储呢?这个时候就是数据结构同学上线的时间了,相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。因此DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:
1 var element = { props: { id: 'list' 5 }, {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, 8 {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, 9 {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}, 10 ] 11 }
而这样一个javascript对象代表的html代码是什么样的呢?
Item 1Item 2Item 3
其实对比过来会发现每一种HTML结构都可以转变成对应的javascript结构的。 这就是所谓的 Virtual DOM 算法。包括几个步骤:
1,用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中。
2,当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。
3,把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。这个概念就和我们当初学操作系统一样,可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
接下来我们说说算法的具体实现吧,既然整个过程大致可以分为三部,那我们就来一步步的讲,首先就是第一部,用js对象模拟DOM树,用javascript来表示一个DOM节点的话只需要记录它的一个节点类型,属性以及子节点就可以了。
1 function Element (tagName, props, children) { 2 this.tagName = tagName 3 this.props = props 4 this.children = children 5 } 6 7 module.exports = function (tagName, props, children) { Element(tagName, props, children) 9 }
例如上面刚刚的那个DOM节点就可以用这种结构来表示:
var ul = Element('ul', {id: 'list'}, [ Element('li', {class: 'item'}, ['Item 1']), Element('li', {class: 'item'}, ['Item 2']), Element('li', {class: 'item'}, ['Item 3']) ])
现在ul只是一个 JavaScript 对象表示的 DOM 结构,页面上并没有这个结构。我们可以根据这个ul构建真正的HTML元素了:
1 Element.prototype.render = function () { props = this.props (propValue = props[propName] 7 el.setAttribute(propName, propValue) 8 } children = this.children || [] 11 12 children.forEach(function (child) { 13 var childEl = (child instanceof Element) : document.createTextNode(child) el.appendChild(childEl) 17 }) el 20 }
render 方法会根据传入的 tagName 来构建一个真正的DOM节点的,然后根据这个DOM节点的属性,递归的把自己的子节点也构建出来,因此只需要执行如下代码即可:
1 var ulRoot = ul.render() 2 document.body.appendChild(ulRoot)
上面的 ulRoot 才是真正的DOM节点,最终会在页面中插入这个真正的DOM树:
Item 1Item 2Item 3
第二步:比较两颗虚拟DOM树的差异: