进入2015年以后,关于JS框架,开发人员有了更多选择了。除了Angular,Ember,React,Backbone,还出现了大量的竞争者,现在有太多的框架可选。
每个人可以从不同的角度对比这些框架,但是我认为最有趣的区别之一是它们管理状态的方法。尤其,思考一下当状态经常发生改变的时候,这些框架是如何做的,这是很一件有意义的事情,在这些框架中,他们各自都用什么方法来反应用户界面的变化?
管理应用的状态和用户界面的一致在很长的时间里都是一个引发UI开发复杂性的根源。到目前为止,我们有了几个不同的处理方式,本篇文章会浏览一下它们之中的几个:Ember的数据绑定,angular的脏检查,React的虚拟DOM以及它和不可变数据结构的关系。
显示数据
我们要说的基本任务是关于程序的内部状态以及怎么把它放到屏幕上显示出来的可见元素。你拿到一套对象,数组,字符串和数字,再把它们变成由一个文本组成的树图结构的东西,表单,链接,按钮和图片。在网页开发中,前者经常表达为 JavaScript数据结构,后面几个表现为DOM。
我们经常叫这个过程为“渲染”,你可以想象成是把你的数据模型”映射”成可见的用户界面内容。当你用模板渲染数据时,你得到一个DOM(或者HTML)来表现数据。
这个过程本身听起来够简单的:尽管映射表单数据模型到UI可能不平凡,但它毕竟是一个很直接的从输入到输出的转换过程。
当我们提到数据经常改变的时候,事情变的有挑战了。当用户和UI交互,或者不知道世界上什么东西的改变更新了数据,反正UI需要反映这些变化。而且,因为重建DOM树的操作是一个昂贵的操作,消耗较大,我们愿意做尽可能少的工作来让数据更新到屏幕。
相对于仅仅渲染一次UI,这里面有一个更有难度的问题,因为它涉及到状态更新。这也是产生几个不同方案的地方。
服务器端渲染:一切重来
“没有变化,宇宙是不可变的”
在大JavaScript纪元以前,每一个你和网页的交互都会触发一个服务器端的来回交互,每次点击,每次表单的提交,意味着网页的重新加载,一个请求被发送到服务器端,服务器处理并响应一个崭新的页面,浏览器再重新渲染它。
这种情况下,前端不用管理任何状态,每次一有什么事情发生了,一切都结束,浏览器什么也不管。无论什么状态,都是服务器端来管理。前端就是一些服务器端产生的HTML和CSS,可能还有浅浅的一点javascript。
在前端角度来看,这是一个非常简单的办法,这样处理也很慢。不仅仅每次交互意味着一个UI的重新渲染,也是一个数据返回远程的数据中心,再从远程回到前台界面的远程交互过程。
现在,我们大多数已经不再这么做了。我们可以在服务器端初始化我们的应用的状态,然后在前端来管理这些状态(这篇同构JavaScript很大的篇幅都在说这个),尽管仍然还有一些人成功的使用着这种更复杂的方式。
第一代JS:人工重绘界面
“我不知道要重新绘制哪里,你指出来吧”
第一代javascript框架,像Backbone.js,ExtJS,和Dojo,第一次向浏览器引入了真实的数据模型,代替那些只在DOM上修饰的轻量级脚本。这也意味着,你第一次在浏览器上可以改变状态。数据模型的内容改变了,然后你把这些改变反应到用户界面上。
尽管这些框架都在架构上从模型中分离出了UI代码,可同步两者还是要靠你自己来完成。当变动发生时,你可以得到一套事件,但是应该由你指出那一部分需要重新渲染,和具体怎么渲染。
在这种模型的性能方面上留给应用开发者很大的发展空间。既然你控制什么时候什么内容应当更新,只要你愿意,你可以很好的调优它。简单的重新渲染页面上的大块区域,还是只更新页面上的需要更新的那一小部分,这经常是需要做出一些权衡。
Ember.js数据绑定
“因为我控制着模型和视图,我能精确的知道那些应该被重新绘制。”
可以人工的指出什么状态改变了需要重新渲染,是第一代javascript应用的复杂性的主要来源。大量的框架的目的就是消除这部分问题。Embe.js就是其中之一。
Ember和Backbone相似,当变动发生时,从模型往外发出事件。不同在于Ember还为事件的接受端提供一些功能。你可以把UI和数据模型绑定在一起,这意味着,有一个监听器附加到UI去侦听变更事件。这个监听器接到事件以后知道应该更新什么。
这就产生了一个很有效的变更机制:通过一开始设置所有的绑定,这样以后同步的花费就变得少了。当某些东西变了,只有应用里面的那些确实需要改变的那一部分会变化。
这种方式里面,最大的折中是当数据模型发生变化,必须通知Ember知道这些变化。这就使你的数据必须要继承Ember的特定API,你要修改你的数据添加特别的set方法。你不能用foo.x=42你必须用foo.set(‘x’,42),诸如此类。
将来,这个方式可能在ECMAScript6的到来后获益。它可用绑定方法来让Ember修饰一般对象,那么所有的与这个对象交互的代码不必再使用这种set的转换。
AngularJS:脏检查
“我不知道什么改变了,我只是检查需要更新的所有一切”
和Ember相似,Angular的目标也是解决有变更以后的不得不手工重新渲染的问题。可是,它用的是另一个方式。
当你参考你的Angular模板代码,比如这个表达式{{foo.x}},Angular不仅监听这个数据,还创建一个这个值的观察器。在此之后,不论应用中什么发生变动,Angular都检查观察器中的这个值和上一次比是不是变了。如果变了,就重新渲染这个值到UI。这个处理检查观察器的方式就叫脏检查。
这种检测方式的最大好处就是,你可以在模型中随便用什么,angular对此没有约束—它也不关心这个。不需要继承基础对象也不需要实现特定的API。
缺点方面就是既然数据模型没有内建的探测器来告诉框架什么变了,框架也就没办法知道是不是有变化,或者究竟哪里变了。这就意味着需要通过外部来检查模型变更,Angular就是这么做的:不论什么东西变了,所有的观察器都跑一遍:点击事件的处理,HTTP响应的处理,时间超时等等,都会产生一个摘要,这就是负责运行观察器的过程。
每次都运行所有的观察器听上去像是一个性能噩梦,但是它实际上是飞快的。这个通常是因为直到实际检测出一个改变,才会有DOM访问发生,而纯javascript检查引用的消耗还是相当低的。但是当你遇到大的UI或者需要经常性的重新渲染,额外的优化措施就是必不可少的了。
象Ember,Angular也会受益于即将到来的标准:EMACScript7有Object.observe方法很适合Angular,它给你一个本地API来观察对象的属性改变。尽管这并不能满足Angular所有需求,因为观察器不仅仅观察简单对象属性。
即将到来的Angular2也会带来一些有趣的关于前端更新检查的更新内容,最近有篇文章说道了这个VictorSavkin发表的文章,也可以看看这个Victor在ng-conf说的话
React:虚拟DOM
“我不知道什么变了,所以我将渲染一切,看看有什么不同”
React有很多有趣的特性,其中最有趣的就是虚拟DOM。
React和Angular相似,并不强制你使用一个模型API,你可以使用认为合适的任何对象和数据结构。那么,它是通过什么来保持UI在改变以后的更新呢?
React做的就是让我们退回到老的服务器端渲染的那些日子,我们可以简单的不管什么状态改变:每当某个地方有变动发生,它就重新绘制整个UI。这个可以极大的简化UI代码。你并不关心怎么在React组件里面维护状态。就像服务器端渲染,你渲染一次就行了。当一个组件需要改变,它就再次重新渲染。在第一次渲染和以后的数据更新渲染没有什么不同。
这个听上去极其没有效率。如果React仅仅做到此,那当然就是这样了。然而,React使用了特殊的方式来重新渲染。
当ReactUI渲染的时候,它首先渲染到一个虚拟DOM,它不是一个实际的DOM对象,而是一个轻量的纯javascript的对象结构,里面是简单对象和数组来表达真实的DOM对象。会有一个特殊处理过程来取得这个虚拟DOM,来创建能在屏幕上显示的真实的DOM元素。
然后,当有改变的时候,一个新的虚拟DOM就从变化中产生。这个新的虚拟DOM反应了数据模型的新状态。React现在有2个虚拟DOM:新的和老的。它对这两个虚拟DOM用一个差异比较算法得到变动的集合。这些变动,也仅仅是这些变动会被应用到真实的DOM:新增加元素,元素的属性值改变等等。
用React一个非常大的好处,或者至少好处之一就是你不需要跟踪变动。无论新的结果里面何时何处有变动,你只需要重新渲染整个UI。虚拟DOM的差异检查方式自动为你做这些,这样就减少了很多的昂贵DOM操作。
Om:不可变数据结构
“我能很明确的知道那些东西没变”
尽管React的虚拟DOM技术已经很快了,但当你想渲染的页面很大或者很频繁的时候(超过每秒60次),仍然会有性能瓶颈。
重新渲染整个(虚拟的和真实的)DOM这件事是真的没有办法避免的,除非你在变动数据模型的时候,做一些特别的控制,就像Ember那样。
一个控制变动的途径是不可变的持久数据结构。
他们看起来能够很好的和React的虚拟DOM方法一起协作,就像DavidNolen的工作用Om库演示的那样,演示基于React和ClojureScript.。
关于不可变数据结构的说法是这样,顾名思义,你不能改变一个对象,你只能产生它的一个新的版本:当你想改变一个对象的属性的时候,如果你不能改变它,那么你只能产生一个新的对象并且设置为这个新的属性。因为持久化数据的工作方式,这个在实际工作中比听起来更有效。
当React组件状态都是用不可变数据组成的,变动检查就会变成这样的情形:当你重新渲染一个组件的时候,如果组件的状态仍指向上次你渲染过的同样的数据结构,那么你可以跳过这次渲染,你可以继续使用这个组件上一次的虚拟DOM,以及整个以这个未变化组件为树枝节点的内部组件。这时候不需要继续深入检测了,因为没有任何状态变化。
就像Ember,那些像Om这样的类库不允许你在数据中使用任何老的javascript对象图。你只能用不可变数据结构从底层开始来构建你的模型才行。我会争辩说区别就是这一次你不用为了满足框架的要求来做这件事。你这么做就是简单因为这是一个更好的管理应用状态的办法。使用不可变数据结构的好处不是为了提示渲染的性能,而是简化你的应用架构。
Om和ClojureScript在组合React和不可变数据结构上也是有用的,但他们不是必不可少的。可能仅仅使用纯粹React和一个像Facebook的Immutable-js那样的库也就足够了。LeeByron,这个库的作者,在React.jsConf上给出了一个关于这个主题的精彩介绍。
我推荐你去看看RichHickey的持久化数据结构和引用管理,这也是一篇关于状态管理方法的介绍。
我在waxingpoetic用过不可变数据结构有一段时间了。尽管我不能想当然地预见到它会被应用在前端UI架构。不过看起来这个事情正在发生,Angular团队的人也在做增加支持这些内容的事情。
总结
变动检测是UI开发中的中心问题,各种javascript框架都采用各自的办法来给出不同的解决方式。
EmberJS在当变动发生的当时就可以检测到改变,是因为他控制了数据模型API,当你调用API的做出数据改变时候,就会触发相应的事件。
Angular.js在变动发生以后检测到改变,它的做法是重新跑一遍你注册在UI上的数据绑定,然后看是不是有值发生了改变。
纯React通过重新渲染整个UI到一个新的虚拟DOM,然后和旧的虚拟DOM做比较来检测数据改变。发现有什么改变了,然后作为修订发送给真实DOM。
React和不可变数据结构可以作为纯React解决方案的加强版,它可以快速的标记组件树为未变化状态,因为在React组件内部是不允许状态改变的。不允许改变内部状态并不是为了性能原因,而是这么做对你的应用程序架构有积极影响。
译者信息
译者:李炳辰,就职HP软件开发部门,10年以上JAVA产品开发经验,熟悉C#, Python, Nodejs。在互联网电商平台,企业软件开发管理方面均有丰富的经验。目前兴趣在于前端开发,数据统计分析在金融业务方面的应用。
|