2015年,JavaScript框架的选择并不缺乏。在Angular,Ember,React,Backbone及其众多竞争对手之间,有很多可供选择。
可以用各种方式比较这些框架,但我认为它们之间最有趣的区别之一就是它们管理状态的方式。特别是,当状态随时间变化时,考虑这些框架的作用是有用的。他们为您提供了哪些工具来反映用户界面中的变化?
管理应用程序状态和用户界面的同步一直是UI开发复杂性的主要来源,到目前为止,我们有几种不同的方法来处理它。本文探讨了其中的一些:Ember的数据绑定,Angular的脏检查,React的虚拟DOM及其与不可变数据结构的关系。
投影数据
我们谈论的基本任务是获取程序的内部状态并将其映射到屏幕上可见的内容。您可以使用各种各样的对象,数组,字符串和数字,最后得到一个文本段落,表单,链接,按钮和图像。在Web上,前者通常表示为JavaScript数据结构,后者表示为文档对象模型
我们经常将此过程称为渲染,您可以将其视为数据模型到可见用户界面的投影。使用数据呈现模板时,您将获得该数据的DOM(或HTML)表示。
这本身很简单:虽然从数据模型到UI的映射可能并不总是微不足道,但它基本上仍然只是一个具有简单输入和输出的函数。
当我们开始讨论随时间变化的数据时,事情开始变得更具挑战性。当用户与UI交互时,或者在更新数据的世界中发生其他事情时,就会发生这种情况。UI需要反映这种变化。此外,由于重建DOM树的成本很高,我们希望尽可能少地在屏幕上获取更新的数据。
这比仅仅渲染一次UI要困难得多,因为它涉及状态变化。这也是那里的解决方案开始显示其差异的地方。
服务器端渲染:重置宇宙
“没有变化。宇宙是不变的。”
在Big JavaScript时代之前,您使用Web应用程序进行的每次交互都会触发服务器往返。每次点击和每个表单提交意味着网页被卸载,请求被发送到服务器,服务器响应一个浏览器然后呈现的新页面。
通过这种方法,前端实际上没有任何州可以管理。每次发生事情时,就浏览器而言,这就是宇宙的终结。无论状态如何,这都是一个后端问题。前端只是一些生成的HTML和CSS,可能还有一点点JavaScript。
虽然从前端角度来看这是一个非常简单的方法,但它也很慢。每个交互不仅意味着完全重新呈现UI,它还是一个远程重新渲染,完全往返于遥远的数据中心。
我们大多数人不再在我们的应用程序中执行此操作。我们可以呈现我们的应用程序服务器端的初始状态,但然后切换到前端管理事物(这是同形JavaScript主要是关于什么)。尽管如此,有些人仍在继续使用这种模式的更复杂版本。
第一代JS:手动重新渲染
“我不知道我应该重新渲染什么。你弄清楚了。”
第一代JavaScript框架,如Backbone.js,Ext JS和Dojo,首次在浏览器中引入了一个实际的数据模型,而不是在DOM之上只有一些轻量级的脚本。这也意味着你第一次在浏览器中改变了状态。数据模型的内容可能会发生变化,然后您必须将这些更改反映在用户界面中。
虽然这些框架为您提供了将UI代码与模型分离的体系结构,但它们仍然保留了两者之间的同步。您可以在发生更改时获得某种事件,但您有责任确定要重新呈现的内容以及如何进行更改。
作为应用程序开发人员,此模型的性能注意事项也很大程度上取决于您。由于您可以控制更新的内容以及何时更新,因此您可以根据需要进行调整。它通常成为重新渲染页面大部分的简单性和重新渲染需要更新的位的性能之间的平衡行为。
Ember.js:数据绑定
“我确切地知道改变了什么以及应该重新渲染什么,因为我控制了你的模型和视图。”
必须在应用程序状态更改时手动确定重新呈现是第一代JavaScript应用程序中偶然复杂性的主要来源之一。许多框架旨在消除这一特定问题。Ember.js就是其中之一。
就像Backbone一样,Ember会在发生更改时从数据模型中发出事件。与Ember不同的是,框架还为事件的接收端提供了一些东西。您可以将UI绑定到数据模型,这意味着有一个用于附加到UI的更新事件的侦听器。该侦听器知道在收到事件时要进行的更新。
这使得一个非常有效的更新机制:虽然设置所有绑定最初需要一些工作,但在此之后保持同步所需的努力是最小的。当某些内容发生变化时,只会激活实际需要执行某些操作的应用程序部分。
这种方法的一个重要权衡是,Ember必须始终了解数据模型中发生的变化。这意味着您需要将数据放在继承自Ember API的特殊对象中,并且需要使用特殊的setter方法更改数据。你不能说foo.x = 42
。你必须说foo.set('x', 42)
,等等。
将来,这可能会受到代理人在ECMAScript 6中的到来的影响。它允许Ember使用其绑定代码装饰常规对象,以便与这些对象交互的所有代码不一定需要遵守setter约定。
AngularJS:脏检查
“我不知道发生了什么变化,所以我只会查看可能需要更新的所有内容。”
与Ember一样,Angular也旨在解决在事情发生变化时必须手动重新渲染的问题。但是,它从不同的角度解决了这个问题。
当您在Angular模板中引用数据时,例如在表达式中{{foo.x}}
,Angular不仅会渲染该数据,还会为该特定值创建一个观察者。之后,只要您的应用程序发生任何事情,Angular就会检查该观察程序中的值是否从上次更改。如果有,则重新呈现UI中的值。运行这些观察者的过程称为脏检查。
这种变化检测方式的巨大好处是现在您可以在数据模型中使用任何内容。Angular对此没有任何限制 - 它并不关心。没有要扩展的基础对象,也没有要实现的API。
缺点是,由于数据模型现在没有任何可以告诉框架有关更改的内置探测器,因此框架无法了解它是否以及可能发生的位置。这意味着需要对模型进行外部检查,这正是Angular所做的:所有观察者都会在每次发生任何事情时运行。点击处理程序,HTTP响应处理器和超时都触发摘要,这是负责运行观察者的过程。
一直运行所有观察者听起来像是一场表演噩梦,但它可以惊人地快。这主要是因为在实际检测到更改之前通常不会发生DOM访问,并且纯JavaScript引用相等性检查很便宜。但是当您进入非常大的UI或需要经常渲染时,可能需要额外的性能优化技巧。
与Ember一样,Angular也将受益于即将推出的标准:特别是,ECMAScript 7中的Object.observe功能非常适合Angular,因为它为您提供了一个本机API,用于观察对象的属性以进行更改。它不会涵盖Angular需要支持的所有情况,因为Angular的观察者可以做的不仅仅是观察简单的对象属性。
即将推出的Angular 2还将在变更检测方面带来一些有趣的更新,正如最近在Victor Savkin的一篇文章中所描述的那样。更新7.3.2015:另见Victor的ng-conf关于这个主题的讨论。
反应:虚拟DOM
“我不知道改变了什么,所以我只是重新渲染一切,看看现在有什么不同。”
React有许多有趣的功能,但就我们的讨论而言,最有趣的是虚拟DOM。
与Angular一样,React不会对您强制执行数据模型API,并允许您使用您认为合适的任何对象和数据结构。那么,它如何解决在存在变化的情况下保持UI最新的问题?
React所做的是有效地将我们带回到旧的服务器端呈现日,当我们根本不关心状态更改时:每当在其中某处发生更改时,它就会从头开始呈现整个UI。这可以显着简化UI代码。您(大多数)不关心在React组件中维护状态。就像服务器端渲染一样,你渲染一次就完成了。当需要更改的组件时,它会再次渲染。组件的初始渲染与更改数据的更新之间没有区别。
这听起来非常低效,如果那是故事的结尾。但是,重新渲染React确实是一种特殊的类型。
呈现React UI时,它首先呈现为虚拟DOM,它不是实际的DOM对象图,而是简单纯净的JavaScript数据结构,包括普通对象和表示真实DOM对象图的数组。然后,一个单独的进程采用该虚拟DOM结构,并创建屏幕上显示的相应真实DOM元素。
然后,当发生更改时,将从头开始创建新的虚拟DOM。新的虚拟DOM将反映数据模型的新状态。React现在有两个虚拟DOM数据结构:新的和旧的。然后它在两个虚拟DOM上运行一个diffing算法,以获得它们之间的一组变化。这些更改,只有那些更改,应用于真正的DOM:添加了此元素,更改了此属性的值等。
因此,React或其中至少一个的最大好处是您不需要跟踪变化。您只需每次重新渲染整个UI,并且新结果中的任何更改都将重新生成。虚拟DOM差异化方法可以帮助您实现这一目标,同时最大限度地减少昂贵的DOM操作量。
Om:不可变数据结构
“我确切地知道什么没有改变。”
虽然React的虚拟DOM方法非常快,但当您的UI很大或想要经常渲染时(例如,每秒最多60次),它仍然可能成为瓶颈。
问题在于,每次都没有办法渲染整个(虚拟)DOM,除非你将一些控制重新引入你让数据模型中发生变化的方式,就像Ember那样。
控制变更的一种方法是支持不可变的持久数据结构。它们似乎特别适合React的虚拟DOM方法,正如David Nolen在基于React和ClojureScript构建的Om库上的工作所表明的那样。
关于不可变数据结构的事情是,顾名思义,你永远不能改变一个,但只生成它的新版本。如果要更改对象的属性,则需要使用new属性创建一个新对象,因为您无法更改现有属性。由于持久数据结构的工作方式,这实际上比听起来更有效。
这意味着在变化检测方面,当React组件的状态仅由不可变数据组成时,就会出现一个逃逸出现:当你重新渲染一个组件时,组件的状态仍然指向与最后一个相同的数据结构。你渲染它的时间,你可以跳过重新渲染。您可以使用该组件的前一个虚拟DOM以及源自它的整个组件树。没有必要进一步挖掘,因为在该州没有任何可能发生的变化。
就像Ember一样,像Om这样的库不允许你在数据中使用任何旧的JavaScript对象图。您必须从不可变的数据结构构建您的模型,以获得好处。我认为不同之处在于,这次你不要只是为了让框架变得快乐。你这样做是因为它只是管理应用程序状态的一种更好的方法。使用不可变数据结构的主要好处不是为了获得渲染性能,而是为了简化应用程序架构。
虽然Om和ClojureScript在结合React和不可变数据结构方面发挥了重要作用,但它们并不是城里唯一的选择。完全可以使用简单的React和像Facebook的Immutable-js这样的库。
概要
变更检测是UI开发中的核心问题,JavaScript框架尝试以各种方式解决它。
EmberJS会在发生变化时检测到变化,因为它会控制您的数据模型API并在您调用时发出事件。
Angular.js会在事后检测到更改,方法是重新运行您在UI中注册的所有数据绑定,以查看它们的值是否已更改。
Plain React通过将整个UI重新呈现为虚拟DOM,然后将其与旧版本进行比较来检测更改。无论发生什么变化,都可以修补到真正的DOM。
使用不可变数据结构进行反应可以通过允许组件树快速标记为未更改来增强简单的React,因为组件状态中的突变是不允许的。但是,这主要不是出于性能原因,而是因为它对您的应用程序架构产生了积极的影响。
原文:http://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html