Pete Hunt 讲 React 的 State 哲学

2016年4月8日

这个是 React 作者之一 Pete Hunt 的一次非常著名的演讲:Pete Hunt: React - Rethinking Best Practices (updated) - JSConf.Asia 2013 我这里是节选了 16:50-23:00 的讲 state 部分的内容。革命性思维,发人深省!

译文如下:

下面我要讲的内容很重要,是它让 React 变得非常 cool ,并且使用起来非常有意思。那就是

每次更新都去重新渲染整个 App

我们做过很多应用,我们知道状态太多使得开发 UI 变得很困难。如果你做后台,就会发现很多代码都可以进行单元测试,代码没问题就直接部署,如果单元测试出错,那么错误也会比较明确。但是对于 UI 就没有这么简单了,有非常多的变数和状态,很多时候要看 UI 感觉是不是对的,看上去够不够好。有很多事情要考虑的,首先 UI 要素的数量非常多,UI 运行的环境也很复杂,一个很大的可以修改大 DOM 树,还有你可能预想不到的用户输入内容。

数据变化是万恶之源

这其中最重要的就是,随着时间变化而带来的数据的变化会是万恶之源。我们清楚的认识到了这一点。不过我们也不是第一个发现这个问题的人,有著名的计算机科学家名叫 Dijkstra ,也同样这么认为:

我们的智力更擅长去把握静态关系,而对于一个动态过程就很不擅长去把握。

他要说明的道理就是,我们很容易去理解一个独立的函数是如何工作的,我们可以看看输入和输出就能搞清楚,但是当我们不断的去调用这个函数,并且给它设置一些我们不知道会如何去变化的一些变量值,那么这样的程序我们就很难把他装到脑子里。

正因如此,我们应该(因为明智的程序员意识到了这个限制)最大化的缩小静态程序和动态过程之间的理解鸿沟。以便让程序(以文字形式平铺展开)和过程(以时间轴顺序展开)的牵连降至最低。

这里他要说的就是,让以时间顺序不断执行的程序,变成一个时间点上就可以执行完毕的等幂函数。明显他在力挺响应式的函数式编程。

上世纪90年代的时候,一切都很简单

让我们回到 1990 年代,那时候我们都在写一些 PHP 的面条代码,运行在服务器上。所以的工作其实都比现在简单。如果我要提交一个新的 TODO 项到我的 TODO 列表,那时候我根本就不用记住以前我那几个 TODO 项都放到哪个位置,然后每次都要更新这些位置。我只需要把新 TODO 项提交到服务器,服务器代码把数据写入到数据库,然后把所有数据都从数据库中再取出来,重新渲染整个页面。所以,这样来管理状态变化的方式是非常简单的,因为每次都是整页面刷新。这样我们很容易在脑子里面勾勒出整个过程,因为整个过程就是一个时间点上就完成了,而不是随时间而不断变化的。

React 会整个刷新组件

React 就是借用了这个方式。当你的数据有变化的时候,React 就会把这个组件刷新。把原来的显示内容全部扔掉,重新呼叫你自己定义的 render 方法,然后给出全新的显示内容。 换句话说,

React 组件就基本上是个等幂函数

React 组件会描述每一个时间点上你的组件应该显示成什么样,就行从服务器端渲染的应用一样。因为这样,你可以得到的一个副作用是,程序变得非常容易进行单元测试。你不用专门写测试代码,去点一下这个,再点一下那个,更新一下这个,然后验证 DOM 的最终显示效果。因为 React 就是拿到一些输入,然后就给出输出,其他的事情交给系统来负责。

DEMO

好,请让我展示一个例子,说明一下我刚才的东西。


var Clicker = React.createClass({
  getInitialState: function() {
    return {count: 0};
  },
  handleClick: function() {
    this.setState({count: this.state.count + 1});
  },
  render: function() {
    return React.DOM.a(
      {onClick: this.handleClick},
      'You have clicked' + this.state.count + ' times'
    );
  }
});

React.renderComponent(Clicker(), mountNode);

这里的是一个 Clicker 组件,你点一下链接,组件就会把计数加一,然后显示这个计数。第一个你要注意的事情就是,这里没有搜索 DOM 节点,然后重写计数值( count )的代码。我们只是声明式的表达:嗨,这个 render 方法中给出了组件应该长成什么样的代码,而并没有去说,这个是最初状态值,这个是怎么去更新这个状态值,而是一旦 state 值有变化,自动去重新呼叫 render 方法。

我们说过了,可以修改的 state 是万恶之源,随时间变化的数据是万恶之源。但是我们也不可能不去修改 state ,react 的方式就是对它进行最大化的隔离。你可以看到,唯一的变化的 state 就是 count 这个变量,它的初始值是0。代码也很清楚,我们设计这个框架的时候,也专门考虑了让初始值的位置比较明显。

另外一个很重要的东西是,不仅仅是对数据 state 的最简化处理,而是看 state 随着时间如何变化。你可以非常简单的去搜一下 setState 这个单词,就可以找到所有你的 state 变化的位置。我们让这一切都变得非常明显。所以当你打开代码,想要调试错误的时候,所有需要查看的地方都很清楚。

每次都重新刷新,世界变得简单

每次数据变化,都重新渲染,这个让一切都变得简单了。你不需要再自己去定位到底 count 都在哪些位置被显示,React 可以始终保证显示出来的 count 值就是最新的。实现这个效果,我们也没用 data binding ,所以代码中不用设置被计算出来的属性,也不用专门注册一个东西来跟踪修改,或者设置任何形式的 data-binding 。具体的实现方式,我们没有采用 ditry checking 这样的昂贵的方式,而只是重新渲染。当然也不会有明文的 DOM 操作了。所有的东西都是声明式的。

但是每次重新渲染不是很昂贵吗?

不过每次都重新渲染听起来似乎也很傻,操作不是很昂贵吗?如果每次数据变化我们都先摧毁 DOM ,这时候如果你正在发表一个评论,你的文字就会消失,你的滚动位置也会消失。也可能会看到闪过的还没有来得及加样式的内容。执行效率怎么保证呢?所以我们不能对真正的浏览器 DOM 做这样的操作,我们不能真正的去重新渲染,所以解决方案就是:

我们开发了 Virtual DOM

虚拟 DOM ,是一切的答案。

(演讲的后续部分会有 Virtual DOM 的讲解,但是 Peter 的翻译就先到这里了)