非死book 的 React 框架解析

jopen 10年前

原文: http://blog.reverberate.org/2014/02/react-demystified.html
作者: Josh Haberman


这篇文章跟博客已有的其他文章有一些分离, 博客大部分是语言解析和底层编程的,
最近我对一些 JavaScript 框架有了兴趣, 包括 非死book 的 React.
我最近阅读的文章, 特别是 The Future of JavaScript MVC Frameworks,
让我相信在 React 当中有一些深入的强大的想法在里边,
然而我没找到文章或者文档能把它核心的抽象解释到我满意的.
就像我前一篇文章 LL and LR Parsing Demystified,
这篇文章尝试解释 React 里对我有意义的思想.

The 1000-Foot View

传统的 Web app 当中, 你要花费高昂的代价和 DOM 进行交互, 通常是用 jQuery:

非死book 的 React 框架解析

我把 DOM 标记成了红色, 因为更新 DOM 开销是很大的.
现在的很多 "App" 会有个 Model class 用来在内部表示状态,
但在我们这里认为只是 app 内部的实现细节.

React 主要的目标是提供一套不同的, 高效的方案来更新 DOM.
不是通过直接把 DOM 变成可变的数据, 而是通过构建 "Virtual DOM", 虚拟的 DOM,
随后 React 处理真实的 DOM 上的更新来进行模拟相应的更新:

非死book 的 React 框架解析

引入额外的一个层怎么就更快了呢?
那不是意味着浏览器的 DOM 操作不是最优的, 如果在上边加上一层能让整体变快的话?

是有这个意思, 只不过 virtual DOM 在语义上和真实的 DOM 有所差别.
最主要的是, virtual DOM 的操作, 不保证马上就会产生真实的效果.
这样就使得 React 能够等到事件循环的结尾, 而在之前完全不用操作真实的 DOM.
在这基础上, React 计算出几乎最小的 diff, 以最小的步骤将 diff 作用到真实的 DOM 上.

批量处理 DOM 操作和作用最少的 diff 是应用自身都能做到的.
任何应用做了这个, 都能变得跟 React 一样地高效.
但人工处理出来非常繁琐, 而且容易出错. React 可以替你做到.

Components

我前面提到 virtual DOM 和真实的 DOM 有着不用的语义, 但同时也有明显不同的 API.
DOM 树上的节点被称为元素, 而 virtual DOM 是完全不同的抽象, 叫做 components.

component 的使用在 React 里极为重要, 因为 components 的存在让计算 DOM diff 更高效,
比起完整通用的 tree-diff 算法消耗的 O(n^3) 高效多了.

想知道为什么, 就要深入一点 components 的设计当中.
拿 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 component class HelloMessage,
然后创建了一个 virtual DOM, 包含 component,
(<HelloMessage>, 本质是是 HelloMessage class 的一个实例)
并挂载到真实的 DOM 元素里的一个节点.

首先注意这个 virtual DOM 是由应用定义的 components 组成的(这里是 <HelloMessage>).
这和浏览器真实的 DOM 有着显著的不同, 那些都只是浏览器内建的比如 <p> <ul>.
真实的 DOM 不含应用特定的逻辑, 而仅仅是可以托管事件回调的数据结构.
而 React 里的 virtual DOM, 则是含有应用特定内在逻辑的, 专为应用定制的 components.
这远不止于一个 DOM 更新类库. React 是一种新的抽象, 新的构建 View 的框架.

另外, 如果你一直关心 HTML 的消息, 你应该知道HTML 自定义标签很快会有浏览器支持.
这将带给真实的 DOM 相似的功能: 根据应用特定逻辑定制应用需要的 DOM 元素.
不过 React 不需要等待官方的自定义标签支持, 因为 virtual DOM 不是真实的 DOM.
这使得 React 能提前应用, 嵌入类似自定义标签和 Shadow DOM 的功能,
而不用等到浏览器加上了所有这些功能才能被使用.

回到例子里, 已经能确定, 其中创建了一个叫做 <HelloMessage> 的 component 挂载到了节点上.
我想用图把最初的状态表示为下面几种形式. 先来展示 virtual DOM 和真实 DOM 之间的关系.
先假定挂载点是文档的 <body> 标签:

非死book 的 React 框架解析

里边的箭头表示 virtual 标签挂载到了真实的 DOM 元素上, 很快可以看到结果.
同时看一下现在应用的 view 的逻辑说明:

非死book 的 React 框架解析

这里是说, 整张网页内容是通过我们定制的 <HelloMessage> component 展示的.
不过, 一个 <HelloMessage> 看起来是什么样子呢?

component 的渲染通过 render() 函数定义.
React 没有明确说明什么时候或者多频繁他会去调用 render(),
只是会尽量调用, 使得正确的界面更新能看清.
render() 方法返回的内容, 表示了浏览器里真实的 DOM 看起来应该怎样.

这里例子当中, render() 返回了 <div>, 里面还有一些内容.
React 调用了 render() 函数, 得到 <div>, 并相应到真实的 DOM 做更新.
所以现在图片更像是:

非死book 的 React 框架解析

这里不仅更新了 DOM, 还保存了 component 过去被更新了怎么样.
所以 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);

所以 return 的不是真实的 DOM 元素, 而是 React 类似 Shadow DOM 的实现,
(比如说是 React.DOM.div) 对应到真实的 DOM 元素.
所以 React 的 shadow DOM 实际上没有真实的 DOM 节点.

表示状态和改变

到上面为止, 我跳过了很大一段故事, 就是 comonent 是怎样被改变的.
如果 component 不允许改变, React 顶多只是个静态渲染框架,
像是纯粹的模板引擎, 比如 Mustache 或者 HandlebarsJS.
而 React 的要点是快速进行更新. 要更新, component 就需要能更改.

React 将其 state 作为 component 的 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);

回调函数 getInitialState(), componentDidMount(), componentWillUnmount()
都会被 React 在对应的时机触发, 他们的命名根据前面提到的应该写的很清楚了.

而 component 和 state 改变背后的基本理解是这样:

  1. render() 仅仅是一个返回 component state 和 props 的函数
  2. state 只有在 setState() 调用时才改变
  3. props 不会改变, 除非父级 component 重新调用了渲染, 传入新的 props

(props 属性在前面没有明确说, 他们是渲染时从父级元素传进来的属性.)

前面我是 React 会调用渲染函数"足够频繁",
意味着 React 不会再去调用 render(), 直到 component 的 setState() 被调用,
或者被父级元素传入不同的 props 属性重新渲染.

把所有信息汇集到一起, 可以阐释 app 初始化时 virtual 改变的数据流
(比如, 响应一个 Ajax 请求):

非死book 的 React 框架解析

从 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 的 class, 那么你的事件回调是应该去相应更新 model,
还有就是调用 setState() 让 React 知道数据有更新.
如果你已经习惯了一些自动进行双向绑定的框架,
model 和 view 的数据两个方向相互传播, 这里可能有点落后了.

这个例子里有很多一眼能看见以外的东西. 虽然例子看起来是这样的,
React 实际上没有在真实的 <input> 元素上绑定 handler.
而是在整个文档的级别绑定了 handler 等待事件冒泡, 再分发到 virtual DOM 对应的元素.
这带来的好处有速度(在真实的 DOM 上绑定大量的 handler 会很慢),
还有是一致的跨浏览器兼容(即便浏览器行为遵循标准, 或者属性不全).

所有这些放在一起, 终于能看到整个图景里的数据流动,
从用户事件(比如说鼠标点击)开始, 最终完成 DOM 的更新:

非死book 的 React 框架解析

结论

通过写这篇文章我学到了不少关于 React 的东西. 下面是我主要的收获.

React 是一个 View 的类库
React 没有影响到你使用任何 model.
React 的 component 是一个 view 级别的概念, 其中 state 对应这个 UI 部分的状态.
你可以把任何 model 类库结合到 React 来使用
(当然有些 model 的处理使得更新被优化得更深入, 比如 Om 的文章里写的).

React 的 component 抽象很适合把更改作用到 DOM 上去.
component 的抽象是条理化的, 适合被复合, 这个设计带来了 DOM 更新的高效.

React component 从 DOM 上获取更新相对不那么方便
手写 event handler 让 React 看起来明显比一些自动更新 view 更改到 model 的类库低级.

React 的抽象是有漏洞的.
大多数时间你只是对 virtual DOM 进行编程, 但有时你需要能直接操作真的 DOM.
React 文档里关于这个讲了很多, 这在他们的Working With the Browser 章节是必需的.

根据我的理解, 我倾向认为在 The Future of JavaScript MVC Frameworks 里说的内容,
需要更深入去审视. 但这个不大一样, 我要等到另一篇文章写.