Learning to be Giant.

React浅析(React Demystified)

|

本文翻译自:React Demystified by Josh Haberman,已得到原作本人授权。

我通常在这个博客当中写一些关于parsing和底层编程的内容,但这篇文章跟平时的那些主题不太相关。最近我对诸多JavaScript的框架产生了些兴趣,包括Facebook发布的React。我最近读的一些文章,尤其是The Future of JavaScript MVC Frameworks,让我确信在React当中蕴含着一些非常强大的深层次思想,但是我至今没能找到一篇让我满意的解释React的核心概念的文章。如同之前的LL and LR Parsing Demystified一样,本文将试图用一种我个人能够认可的方式来解释React当中的这些核心思想。

整体印象(The 1000-Foot View)

在传统的Web应用中,你通常会通过jQuery大量的与DOM打交道:

1

我故意把DOM画成红色的,因为更新DOM的开销非常大。现在有时候“App”会用model类在内部表示状态,但是对我们来说那只是应用内部的实现细节。

React的主要目标是提供一个与以往不同的更高效的更新DOM的方法。与以往直接修改DOM的方法不同,你的app会创建一个“Virtual DOM(虚拟DOM)”,React会操作真实的DOM来与Virtual DOM进行匹配:

2

额外引入一层为什么提高性能呢?如果额外引入的这一层可以提高性能的话,这岂不是说浏览器的DOM实现并不是最优的?

是有这个么意思,但是Virtual DOM和真实的DOM之间有一些语义上的差别。最明显的就是对于Virtual DOM的修改并不能保证即可生效。这就使得React可以等到Event Loop结束的时候才去操作真实的DOM。这时候,它会计算一个Virtual DOM和真实DOM之间近乎于最小的diff并把它使用尽可能少得步骤应用到真实DOM上。

事实上,我们也可以通过手动编写程序使之批量处理DOM的更新并且使用最小的diff。任何使用这种方法的应用都可以达到和React一样的高效率。但是让程序员手动完成这些非常的麻烦并且容易出错。React把这一切麻烦都接管了。

组件化

除了我前面提到过的Virtual DOM和真实DOM之间存在的一些语义上的差别,他们之间同时也有着非常不同的API。DOM tree上的节点是元素(elements),而Virtual DOM上的节点则是一种完全不同的抽象,被称为组件(components)

对于React来说,组件的使用非常的重要,因为组件就是设计来提高DOM diff的效率的,使之好于一般的树比较算法的$O(n^3)$的复杂度。

为了了解其中的原因,我们稍微看一下组件的设计。以在React官网上的”Hello World”程序为例:

/** @jsx React.DOM */
var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});
 
React.renderComponent(<HelloMessage name="John" />, mountNode);

这里面有一大堆东西没有完整的解释清楚。别看这个例子很小,里面却蕴含了许多非常重要的思想,所以我想在这上面稍微多花点时间。

在这个例子中,我们创建了一个React组件类HelloMessage,之后创建了一个只包含一个组件(<HelloMessage>,本质上就是HelloMessage类的一个实例)的Virtual DOM,并且将它挂载到真实DOM的mountNode元素下。

我们需要注意的第一件事情是React的Virtual DOM是由应用自定义的组件组成的(在本例中就是<HelloMessage>),这和真实DOM有着巨大的区别:真实DOM中所有的元素都是浏览器内建支持的,比如<p><ul>等等。真实的DOM不含有任何和应用相关的逻辑,它只是一个挂载事件回调的被动的数据结构。而React Virtual DOM则是由与应用相关的组件组成的:这些应用相关的组件包含了应用相关的API和内部逻辑。这不仅仅是一个用来跟新DOM的库,而更是一个全新的抽象和构建页面的框架

岔开一下:如果你一直在跟进HTML的最新进展,你或许知道HTML自定义标签很快就会得到浏览器支持。这将给真实的DOM带来类似的能力:定义包含应用逻辑的与应用相关的DOM元素。但是React并不需要等待官方对于自定义标签的支持,因为Virtual DOM并不是真实的DOM。这使得React可以赶在浏览器把它们加入到真实DOM之前集成类似于自定义标签和Shadow DOM的特性。

回到我们的例子。我们已经知道它会创建一个叫做<HelloMessage>的组件并挂载到mountNode上。我想用不同的方法来图示这一开始的状态。首先,我们来画一下Virtual DOM和真实DOM之间的关系。我们假设mountNode是文档的<body>标签:

3

图中箭头表示虚拟元素挂在在真实DOM元素下,我们马上就会看到结果。但同时我们也来看看我们的页面现在的逻辑表示:

4

这幅图说明,我们整个网页的内容由我们自定义的<HelloMessage>组件表示。但是<HelloMessage>组件又是什么样呢?

组建的渲染方法定义在组建的render()方法中。React没有明确说它到底什么时候或者以什么频率来调用render(),只是说调用会足够频繁来反映有效的变化。你的render()方法的返回值代表着在你的页面真实DOM下的样子。

在本例中,render()返回了一个包含一些内容的<div>。React调用我们的render()方法,得到<div>,然后更新真实的DOM来与之匹配。所以现在图示大概是这样:

5

然而React不只是更新DOM,它还记录了它将DOM更新之后的样子。这就是React如何在之后进行快速diff。

我忽略了一件事情,就是render()方法是怎么返回DOM元素的。在这个问题上JSX的存在反而让事情复杂化了,因为它并不是纯JavaScript。看看JSX编译后的结果对理解这件事情更有帮助:

/** @jsx React.DOM */
var HelloMessage = React.createClass({displayName: 'HelloMessage',
  render: function() {
    return React.DOM.div(null, "Hello ", this.props.name);
  }
});
 
React.renderComponent(HelloMessage( {name:"John"} ), mountNode);

啊哈!所以我们返回的其实并不是真正的DOM元素,而是React对应于真实DOM的类似Shadow DOM的实现(如React.DOM.div)。所以React的Shadow DOM并没有真实的DOM节点。

表示状态和更改

到目前为止,我省略了一大段关于组件是如何改变的故事。如果组件不允许被更改,那么React也就不过是一个静态渲染框架,类似于纯粹的模板引擎如Mustache或者HandlebarsJS。但是React的全部意义就是高效更新。为了能够更新,组件必须可以被修改。

React用组件的state属性来表示它的状态。React网站上的第二个例子阐释了这一点:

/** @jsx React.DOM */
var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: function() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
    );
  }
});
 
React.renderComponent(<Timer />, mountNode);

React会在适当的时机调用getInitialState()componentDidMount()componentWillUnmount()这些函数。基于我们上文已经谈到的,从这些函数的名字里我们就能知道这些函数是用来做什么的。

所以,在组件和组件状态更改背后的基本假设就是:

  1. render()是组件stateprops的函数
  2. 除非调用setState()方法,否则组件状态不会更改
  3. 除非父组件使用不同的属性来重新渲染当前组件,否则组件的属性不会更改

(我之前没有明确的提到props,但是props就是父组件在渲染时向当前组件传入的属性)

所以之前我提到说React将会以“足够高的频率”来调用render()其实就意味着React在有人调用组件的setState()方法或者被父组件使用不同的属性重新渲染之前没有理由再次调用render()

我们可以把所有的这些信息放到一起来阐释启动一次对Virtual DOM修改产生的数据流(例如响应一个AJAX请求):

6

从DOM获取数据

目前为止我们值谈了怎么把修改应用到真实的DOM上。但是在现实应用场景中,我们通常也希望从DOM上获取数据,我们就是通过这种方法获得用户的输入的。我们可以通过React网站上的第三个例子来看一下这是怎么做到的:

/** @jsx React.DOM */
var TodoList = React.createClass({
  render: function() {
    var createItem = function(itemText) {
      return <li>{itemText}</li>;
    };
    return <ul>{this.props.items.map(createItem)}</ul>;
  }
});
var TodoApp = React.createClass({
  getInitialState: function() {
    return {items: [], text: ''};
  },
  onChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var nextItems = this.state.items.concat([this.state.text]);
    var nextText = '';
    this.setState({items: nextItems, text: nextText});
  },
  render: function() {
    return (
      <div>
        <h3>TODO</h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <input onChange={this.onChange} value={this.state.text} />
          <button>{'Add #' + (this.state.items.length + 1)}</button>
        </form>
      </div>
    );
  }
});
React.renderComponent(<TodoApp />, mountNode);

简单地说,就是你手动地处理DOM事件(如上例中的onChange()),而你的事件回调也可以调用setState()来更新你的UI。如果你的应用程序有model类,你的事件回调可能在调用setState()通知React有更改发生的同时还需要更新你的model。如果你已经适应了提供自动的双向数据绑定的框架的话(对于model的修改会自动应用到view上,反之亦然),React看起来就如同技术倒退(事实上单项数据绑定在大多数情况下是足够的,而且能够让数据流更加清晰。如果你执意觉得需要双向数据绑定的话,React提供了一个插件ReactLink来帮助你。——译者注)

这个例子当中包含的内容远比表面上看起来多。不管这个例子看起来如何,React并不会真的在真实DOM的<input>元素上绑定一个onChange事件回调,而是在整个文档级别绑定事件监听,让事件冒泡,然后再将事件分发给Virtual DOM当中适当的元素。这带来了许多好处,如速度(在真实DOM上绑定许多事件监听会拖慢速度)和跨浏览器表现的一致性(即便浏览器行为不遵循标准, 或者属性不全)。

所以,当我们把所有者一起放到一起,我们得到了一个完整的从用户事件到完成DOM更新的数据流图:

7

总结

我在写这篇文章的过程当中学习到了很多。下面是我主要的收货。

React是一个界面。React并没有对你的model产生任何影响。一个React组件是一个界面级别的概念,而组件的状态则只是这一部分UI的状态。你可以把任何的model的库结合到React中(然而有些model的做法可以进一步优化更新的效能,如在Om文章中提到的)。

React的组件抽象很适合把更改应用到DOM上。组件的抽象十分有条理,适合被复合,高效的DOM更新便得益于这个设计。

从DOM上获取更新对React组件来说并不方便。手动编写回调让React看起来明显比一些自动更新界面更改到model的库低级.

React的抽象不完备。大多数时间你只是对Virtual DOM进行编程, 但有时你需要能直接操作真实DOM。关于这个问题React在其文档中有更多说明,并且在Working With the Browser一文中讲到了何时这种直接对DOM的操作是必需的.

根据我的理解, 我倾向认为在The Future of JavaScript MVC Frameworks里说的内容, 需要更深入去审视。但这就和本文主题有点不同了,我会在其他文章当中写到。

我并不是一个React的专家,所以如果你发现我文章中的任何错误请不吝赐教。

Comments