JS 框架中的状态变化和变化检测

本贴最后更新于 2744 天前,其中的信息可能已经事过景迁

原文 Change And Its Detection In JavaScript Frameworks。原文很不错,有能力的同学建议看原文。我尝试着翻译一下,水平有限,主要是帮助自己更好的理解这篇文章,能帮到其他同学就更好了。

2015 年,各种 JS 框架层出不穷。Angular、Ember、 React、Backbone, 以及他们众多的竞争对手,选择太多了。

有很多种方法可以比较这些框架,但我认为最重要的区别在于它们管理状态(state)的方式。也就是说,考虑这些框架在状态发生改变的时候做了什么是很有意义的。它们给了你什么样的工具来反映用户界面的变化?

应用状态和用户界面的同步管理一直以来都是 UI 开发中最复杂的部分,而现在我们有很多种方法来处理它。本文探索了其中的几种:Ember 的数据绑定、Angular 的脏数据检查、React 的虚拟 DOM 以及它和不可变数据结构的关系。

组织数据

这里我们所说的事就是将程序中状态映射到屏幕上的可见部分。你在程序中使用对象、数组、字符串和数字,而最终展现出来的是段落、表单、超链接、按钮和图片。在网络上,前者通常是 JS 数据结构,而后者通常是 DOM(文档对象结构,以下简称 DOM)。

这个过程通常被称为渲染,你可以认为这是数据模型映射到可见的用户界面的过程。当你用数据渲染出一个模版,你得到的是数据的 DOM 形态。

When the model changes, the DOM tree needs to change too

当数据改变时,DOM 也必须改变。这比将界面渲染仅一次困难的多,因为它涉及到状态变化 。这也正是各种框架各显神通的地方。

服务端渲染:重置整个世界

“没有变化,整个世界都是不变的。”

在“大前端”时代到来之前,你对网页的每个动作都会触发对服务器的请求。每次点击,每次表单提交都意味着网页的重新加载,服务器接受一个请求返回一个新的页面然后浏览器重新渲染。

When the model changes, the whole UI is re-rendered

这种做法中,前端实际上不需要管理任何状态 ,每次事件被触发,整个周期就结束了,浏览器只需要关心这些。无论数据处于什么状态,都是后端的事。前端只是一些生成的 HTML 和 CSS,有的时候还有少许的 JS。

这种方案对于前端来说非常简单,可是它很慢。一次交互不仅代表着界面的重新渲染,还代表着远程重渲染——这需要从遥远的地方取数据。

大部分网站不再这样做了。而是在服务器端渲染应用的初始状态,然后转到前台来管理数据(这就是 IsomorphicJS 干的事)。不过还是有一些人采用这个方案的改进版

第一代 JS:手动重渲染

“我不知道应该渲染什么,你来决定”

第一代的 JS 框架,像 Backbone、Ext 和 Dojo 等,首次推出了真正意义上的浏览器端数据模型,以代替绑定在 DOM 上的轻量脚本。这意味着你可以在浏览器中改变状态。数据模型的内容是可变的,所以你需要获取变化并体现在用户界面上。

尽管这些框架给出了分离界面和数据的架构,还是有一些界面和数据的同步工作需要做。当变化发生时,事件被触发,如何处理这些事件以及如何根据事件渲染界面则需要开发者来决定。

When the model changes, an event is produced but the updating of the UI is not covered by the framework

这种模型的性能很大程度上取决于开发者。哪些部分、何时更新都由你来决定,可塑性非常高。让页面的大块内容重渲染可以让代码变的简单,仅仅重渲染需要更新的一小部分则可以提高性能,开发者经常需要在这两者之间取舍。

Ember:数据绑定

“我很清楚发生了什么变化,以及需要更新哪些部分,因为我控制了你的模型和视图”

第一代 JS 应用的复杂性在于,当应用的状态变化时,你需要手动更新界面。很多框架都想解决这个问题,Ember 就是其中一个。

Ember,和 Backbone 一样,在变化发生时触发事件。区别在于,Ember 会在事件接收端提供一些信息。你可以把界面和数据模型绑定在一起,也就是说,界面上绑定了事件监听器。当接收到事件时,监听器知道要更新哪些部分。

When the model changes, a binding updates the relevant locations in the DOM

这形成了一个相当高效的更新机制:尽管一开始设置这些绑定需要花些功夫,之后保持界面和数据的同步就不费什么事了。当变化发生时,只有应用中需要被更新的部分被更新。

这种方案的代价是,Ember 必须监控数据模型发生的变化。这就意味着开发者需要将数据放在 Ember 的生成的特定对象中,改变数据也要通过 Ember 的方法。比如说,不能 foo.x = 42,只能 foo.set('x', 42)

等到 ES6 的代理出现后,这个问题可能会改善。有了代理,Ember 就可以用它的绑定代码加工普通对象,调用这些对象就不需要依赖 setter 方法。

Angular:脏数据检查

“我不知道发生了什么变化,所以我把可能需要更新的部分都检查一遍”

和 Ember 一样,Angular 也想解决变化发生时需要手动重渲染的问题。不过它采用了另外一个角度。

当你在 Angular 模版中引用数据,比如说 {{foo.x}},Angular 不仅读取这个数据,还为这个特定的值创建了观察器。这样,无论你的应用发生了什么变化,Angular 都会检查观察器中的值是否和上次不同。如果不同,它就重渲染界面上的这个值。这个过程叫做脏数据检查

Changes in the model are being actively watched from watchers set up in view templates

这种变化检查的方式的优点在于,你可以在数据模型中使用任何结构,Angular 对此没有限制。开发者不需要继承基础类,也不需要实现接口。

缺点在于,框架无法得知数据模型内部的变化,也就不知道变化是否发生,发生在哪里。这就意味着数据模型需要检查变化,这正是 Angular 做的事:每次任何事发生都会让所有的观察器运行。点击、HTTP 响应和过期都会触发所有观察器。

每次都运行所有的观察器听上去像性能上的噩梦,令人意外的是它还挺快的。这主要是因为,在检测到变化之前,不会涉及到 DOM,而单纯的 JS 引用比较是很快的。不过当你接触到非常庞大的界面或者需要频繁渲染,可能需要一些性能优化的技巧。

和 Ember 一样,Angular 也会从即将到来的标准中受益。ES7 中的 Object.observe 特性很适合 Angular,使得它可以通过原生接口观察对象的属性变化。不过这对 Angular 需要支持的其他特性没有影响,因为 Angular 的观察器不仅仅是观察对象的属性。

即将到来的(本文写于 2015-3-2)Angular2 在变化检查上做了一些有趣的更新。Victor Savkin 最近的一篇文章中谈到了这些。

React 虚拟 DOM

"我不知道发生了什么变化,所以我把所有的东西都重渲染,看看有什不同"

React 有许多有趣的特性,其中最有趣的是虚拟 DOM。

和 Angular 一样,React 不需要你实现数据模型接口,你可以使用任何合适的对象或者数据结构。那么,React 如何保持界面和状态的同步呢?

React 实际上将我们带回了美好的服务端渲染时代:我们不需要关注状态变化,因为无论发生什么变化,它都会把整个界面重渲染。这会大大简化前端代码。你几乎不需要维护 React 组价的状态。和服务端渲染一样,你只要渲染一次就大功告成了。如果你需要改变一个组件,直接将它重渲染就行了。改变一个组件的数据和初次渲染它是一样的。

这听上去很效率很低,如果只是这样确实效率低。然而,React 的重渲染很特别。

当 React 渲染一个界面时,它会先渲染虚拟 DOM。虚拟 DOM 不是 DOM 对象图(一种数据结构),而是由普通对象和 DOM 对象图的数组构成的轻量的纯 JS 数据结构。然后根据虚拟 DOM 创建展现在屏幕上的 DOM。

The model is first projected to a Virtual DOM, which is then rendered to a real DOM

当变化发生时,React 重新创建虚拟 DOM,新的虚拟 DOM 反映了变化后的状态。这时,React 有两个虚拟 DOM,它会通过一个比较算法来比较两个虚拟 DOM 找出不同的部分,只有不同的部分会更新到实际的 DOM 上。

When the model changes, the Virtual DOM is re-rendered. The two version of the virtual DOM are compared, and the changes patched in the real DOM

React 最大的优点或者说其中一个优点就是,你不需要追踪变化。你只需要重新渲染整个界面,无论发生了什么变化,都会体现在新的结果中。虚拟 DOM 的方案让你做到这一点,同时最小化 DOM 操作。

Om: 不可变数据结构

“我很清楚什么没有变化”

尽管 React 的虚拟 DOM 方案已经很快了,当界面非常复杂或者渲染频率过高(比如说每秒 60 次),还是会有性能上的瓶颈。

问题在于没办法每次都渲染整个 DOM(即使是虚拟 DOM),除非引入一些手段限制数据模型中的变化,就像 Ember 一样。

控制变化的其中一种方法是利用不可变、持久化的数据结构。这种方式和 React 的虚拟 DOM 相当合适,David Nolen 的 Om 库证明了这一点。

顾名思义,不可变数据结构是不可以被改变的,当你改变数据时,你得到该数据的新版本。如果你想改变对象的属性,不能直接改变已有的对象,只能新建一个带有新属性的新对象。由于不可变数据结构的工作机制,它比听上去效率更高。

运用到变化检查中就是,React 组件的状态由不可变数据组成。重渲染组件的时候如果组件状态和上次渲染时的状态相同就可以跳过渲染。你可以保持上次的虚拟 DOM,而不用继续比较下去。

和 Ember 一样,Om 不允许开发者在数据中使用普通 JS 对象。你得用不可变数据建立数据模型才能享受它带来的好处。这样做不是因为框架的要求,而是因为这是一种更好的管理应用状态的方式。使用不可变数据的最大的好处不是渲染性能的提升,而是简化了应用的架构。

Om 和 ClojureScript 不是 React 和不可变数据结合的唯一例子。React 和 FaceBook 的 ImmutablJS 可以完美地配合。ImmutableJS 的作者 Lee Byron 在最近的 React 会议上就这个话题给出了很好的介绍。

总结

变化检查是 UI 开发的核心问题,很多 JS 框架用不同的方式来解决它。

Ember 在变化发生时就能得知,因为它控制了数据模型接口,当你调用时会触发事件。

Angular 通过你注册在 UI 上的所有的数据绑定来检查发生变化的值。

React 通过重渲染虚拟 DOM,然后根据虚拟 DOM 发生变化的部分更新真正的 DOM。

React 和不可变数据结构配合能很快确认未变化的组件树从而优化 React。起初这样做不是为了提升性能,而是简化应用的架构。

有什么翻译的不合适的地方,欢迎指出

  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    729 引用 • 1327 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...