先前有一篇文章探索了 React 组件里的数据。数据由两个结构组成——属性与状态。前者用来代表不可变的数据,而后者代表 通过与 UI 交互或通过其他外部手段而改变的数据 。
当使用状态数据时,UI 会在调用 setState 函数使得状态改变时进行更新。传统地,这个函数会被调用来响应一个使用事件处理器的事件。
在这篇文章中,我们将进一步探索状态的更新,包括表单输入组件 以及通过子组件属性的状态值传递。最后,我们会讲解 Facebook 创建的 Immutable JavaScript 库, 以便在需要重新呈现组件时有效地通知 React。
看看下面的 React 组件,它包含一个表单,表单中有一个 input 元素,input 元素上注册了一个 change 事件。
var Message = React.createClass({ getInitialState: function() { return { message: this.props.message }; }, _messageChange: function(e) { this.setState({ message: e.target.value }); }, render: function() { return ( <div> <span>Message: {this.state.message}</span> <br /> Message: <input type="text" value={this.state.message} onChange={this._messageChange} /> </div> ); }, });
用户在 input 框中输入文本的时候,change 事件的处理函数被执行,从 textbox 获得值,并更新状态。这个示例中的 change 事件处理函数是 _messageChange。如果没有注册 onChange 事件处理函数,仍然会读取字段,但不会用输入的数据更新 state。在 React 中,输入控件不会进行自我更新,它们更新 state,然后 state 触发一次重绘来更新输入控件。表面上看这种方式有点绕,但是关键在于 React 总是保持组件的 state 并与 DOM 同步。
标准 HTML 表单中的 input 元素,如 input , select 和 textarea 等,被 React 认为是输入组件。因为这些控件可以改变值,React 提供了一套控制机制,通过该机制可以初始化控件、接收输入、更新并在 UI 中反映出来。
输入组件可以是 受控的或不受控的 。受控的组件由 React 通过 value 和 onChange 属性来管理。当用户在 input 元素中输入文本的时候,已注册的 onChange 事件处理函数会被执行,输入的文本作为参数被传递给事件对象。文本参数用于更新 state,它会通过 props 回传给受控的组件。一个有 value 属性的表单组件,如果没有 onChange 属性,它会像之前提到的那样,是只读的。
然而为什么输入组件是只读的呢?之前说过,受控的组件并不能通过交互来更新,而是直接更新,并触发 change 事件。新值必须通过这个事件的处理函数来获取,通过传入处理函数的事件对象来访问新值。然后,新值会用于更新输入组件父组件的 state。在上面的示例中,父组件是 Message。调用父组件的 setState 函数重绘输入组件,更新的 state 值会由 props 回传给输入组件。为什么要用这种方式?React 组件的视图(在这里就是 DOM)和 state 必须总是保持一致,这不可能使用传统的不受控的输入元素。
思考下面这段没用 React 的代码。
<main> <input name="message" value="Hello World!"> <button>Get Value!</button> </main>
当用户在 input 控件中输入文本的时候,input 控件会显示输入的文本。而在这之后,点击了 button,你认为会有什么样的输出呢?
document.getElementsByTagName("button")[0].addEventListener("click", function() { console.log(document.querySelector("[name='message']").getAttribute("value")); });
很有意思,输出的并 不是 输入更新的内容,而是最初 input 控件渲染时的值。在新文本显示的时候,DOM 却没有同步 input 控件的 state。
在 CodePen 上可以看到这个情况发生。
对于多数 JavaScript 库和框架来说,这并不是问题。但对于 React 来说,虚拟 DOM 和组件的 state 必须总是保持同步。
看看下面的 CodePen 演示。
参阅由 SitePoint ( @SitePoint ) 在 CodePen 上写的 React.js Controlled / Uncontrolled Input 演示 。
在第一个输入框中输入文本,可以观察到只有第一个输入框有更新。因为第二个输入框没有绑定 value 属性,当 message 更新的时候,并不会影响到第二个输入框。第二个输入框通过 onChange 属性处理 change 事件,当 state 更新,message 值更新到第一个输入框,然后显示在屏幕上。第二个输入框的 defaultValue 属性只在输入组件首次渲染的时候会用到。
不受控的 输入组件没有 value 属性集,而且一般在组件上发生交互的时候在 UI 中更新,但不会因为 state 变化而进行重绘。
要探索输入组件的其它功能,就看看下面两段关于 ColorList 的演示。
React 的新手常常想搞明白数据应该存储在 props 中还是 state 中。以前的文章中提到 props 是一个不可变的结构,它是向组件传递数据的首选方式。State 是可变结构,它变化时会造成组件重绘。之前的问题是,数据存储在 props 中还是 state 中 — 答案是,都在。关于是选择 props 还是 state,大家很少看数据是什么,而是花更多时间看数据与整个组件结构的关系。通过 props 传递的数据会用于初始化 state。state 中的数据又会通过 props 传递给子组件。使用 props 还是 state 主要取决于数据和组件的关系,以及组件和其它组件的关系。
构成组件是 React 中的常见模式。在下面的示例代码中定义了三个组件:Color,ColorList, 和 ColorForm。Color 是ColorList 和 ColorForm 的父组件或者说是容器。作为父组件,Color 负责维护 state 并触发子组件的重绘。
Parent Color Component getInitialState: function() { return { colors: new Immutable.List(this.props.colors) }; }, render: function() { return ( <ColorList colors={this.state.colors} /> <ColorForm addColor={this._addColor} /> ); }
为了将 state 的值从父组件传送给子组件,state 值由 props 传入子组件,就像上面父组件的 render 函数那样。
子组件通过组件的 props 属性访问传入的属性值,就像下面的代码。
Child Color List Component render: function() { return ( <ul> {this.props.colors.map(function(color) { return <li key={color}>{color}</li>; })} </ul> ); }
观察数据流向——父组件通过它的 props 接收数据,然后由这些 props 初始化父组件的 state,之后父组件将 state 传递给子组件的 props,最后子组件根据 props 进行渲染。
同一个数据被当作不可变的 props,也被当作可变的 state,到底是什么取决于接收数据的组件用它来干什么。父组件把数据当作可变的 state 来进行处理,是因为它能处理子组件的事件,而从这些事件可以得到新的数据,这就可以导致 state 变化,再将更新的 state 传递给所有子组件。子组件得到新数据之后不会用来更新任何东西,它只是直接把数据传送给父组件来进行更新。这样的结果使得理解和预测数据流变得简单。
Child Color Form Component _onClick: function(e) { // calls the _addColor function on the parent to notify it of a new color this.props.addColor(this.state.newColor); // the input component is a child component of this form component // so this component maintains state for its own form, and also passes // along new data to it’s parent so the parent can maintain state for // the whole component // // because the form is self-contained in this component, the state for // form is maintained here, not the parent component this.setState({ newColor: undefined }); }, render: function() { return ( <form> <label> New Color <input type="text" name="newColor" value={this.state.newColor} onChange={this._onChange} /> <button type="button" onClick={this._onClick}>Add Color</button> </label> </form> ); }
上面的示例中,点击按钮会触发相应的处理函数,这个函数是从父组件 Color 中以 props 方式传递给子组件 ColorForm 的。这个函数触发时会让父组件往 state 里添加一个新的颜色,再由 setState 函数触发重绘。
Parent Color Component _addColor: function(newColor) { this.state.colors.push(newColor); this.setState({ colors: this.state.colors }); },
一个组件发生更新时会通知它的父组件,随后这个父组合会通知所有子组件。state 由某个组件维护,其它组件都只是显示从 state 而来的不可变的 props。
要查看整个 ColorList 演示的偌,就去下面的 CodePen。
props 严格来说是可变的(JavaScriopt 没有阻止组件改变它们),但修改它们会违背 React 的基本原则,因此应该把 props 看作是不可变的。
另一方面,state 却经常变化。然后将不变性原则用于 state 可以提高 React 组件的性能。
对改变 state 来说有一个重要的方面就是检测改变了什么,然后根据这些变化更新虚拟 DOM。其中一些变化很容易确定。state 中的数和字符串值变化很容易通过对新旧值的简单比较来确定。甚至创建一个新的对象,并在 state 上设置的新引用也很容易检测出来。但是,一个数组呢?程序如何确定一个数组已经改变了?当在数组中添加新项时,数据的引用并不会发生变化。检查数据的长度可能会发现更改,但如果添加了一项同时删除了一项又该怎么办?程序如何在不遍历数组的情况下确定数据是否发生变化?与检查意会的更改相比,这是一个难以解决的问题。
解决这个问题的办法就是不可变性。Facebook 创建了一个叫 Immutable 的 JavaScript 库,它提供了一些结构来辅助 JavaScript 创建和管理不可变对象。
var colors = [“red”, “blue”, “green”]; var listOfColors = new Immutable.List(colors); var newListOfColors = listOfColors.push(“orange”); // outputs false console.log(listOfColors === newListOfColors);
在上面的示例代码中,向列表中添加 “orange” 会产生新的列表。push() 不会 返回一个添加了额外的颜色的原列表。相反,它返回一个新对象的新引用。新的对象引用可以很容易地用于检测列表是否已经发生了变化。使用不可变结构让 React 组件避免在列表中一项项地去比较 — 而只是简单的比较数组的引用。
React 对每个组件调用 shouldComponentUpdate 函数以确定是否需要重绘以响应 state 的变化。这个函数的默认实现只是返回了一个 true。这么一来,组件及其子组件就不会关心是否发生变化,每次都会进行重绘。如果要在不需要重绘的情况下避免重绘,组件可以改写默认的函数,对 state 或 props 的数据进行检测。(props 由新的 state 数据填充)。为了利用这种高效的检查,可以在组件的定义中自定义 shouldComponentUpdate 函数(原函数会隐藏在原型中),添加一行简单的代码用于检查引用是否相同。
看看上一节的演示的颜色列表的代码,展示如下。
Parent Color Component getInitialState: function() { return { colors: new Immutable.List(this.props.colors) }; }, _addColor: function(newColor) { this.setState({ colors = this.state.colors.push(newColor) }); }, render: function() { return ( <div> <ColorList colors={this.state.colors} /> <ColorForm addColor={this._addColor} /> </div> ); }
Child Color List Component shouldComponentUpdate: function(nextProps, nextState) { return nextProps.colors !== this.props.colors; }
父组件中的 _addColor 函数作为另一个子组件 (这里没有展示出来,它在 CodePen 中)的结果来执行。事件中将新的颜色传递给 _addColor 函数,添加到颜色列表中。添加颜色的时候,Immutable 库提供的 push 函数会创建一个新的列表返回出来。父组件重绘的时候,子组件 ColorList 的 shouldComponentUpdate 函数被调用,它会比较原来的颜色列表和新颜色列表的引用(想明白新颜色列表是如何从父组件传递给子组件的,就看看上一节的内容)。因为 Immutable 库产生了新的对象,只需要通过引用就能判断列表是否发生变化。因此,列表在有变化时更新,而不是每次都由父组件触发重绘。
下面的 CodePen 中有所有示例。
参阅由 SitePoint ( @SitePoint ) 在 CodePen 上写的 React.js Immutability 演示 。
有一个针对本文的 React Web 演示应用,我已经将它放在 https://github.com/DevelopIntelligenceBoulder/react-flux-app ,它演示了本文中的许多概念。这个应用部署在 Azure 上,可以通过这个 URL 访问: http://react-widgets.azurewebsites.net .
这个 Web 应用演示了创建组件、组合组件、适当使用 props 和 state,以及大量使用事件。JavaScript 符合 ES2015 标准和 JSX,使用 Babel 来翻译成 ES5,也使用了 WebPack 来生成包含所有代码库的 JavaScript 文件,这些库包括 React、ReactDOM、Immutable 等。所有这些通过 NPM 安装。 Gulp 用于自动化各项开发任务。 现在就创建这个项目的分支,来探索 React 中许多新颖又有用的编程方法。
必须安装 Node.js 来运行这个工程。另外, SASS 用来预处理 CSS,为此还需要安装 Ruby 。 这里 有完整的安装说明。
在 React 中使用数据是一种不同的思维方式。数据从某个子组件流向父组件的 state,再通过 props 散发给所有子组件并根据其变化选择性地对 DOM 进行重绘,这不仅有效而且高效。一开始这个方法可能有点吓人,尤其是对服务端开发者或通过 jQuery、AngularJS 等技术构建 Web 应用的开发者来说。然而,一旦正确理解了数据流从 输入到 state 再到 props 直到触发渲染 ,我们可以看到这是一个让 DOM 保持同步的强大模式。
这篇文章是来自微软技术布道者和 DevelopIntelligence 的 Web 开发系列之一。DevelopIntelligence 致力于 JavaScript 实践学习、开源项目和与 Microsoft Edge 浏览器、 EdgeHTML 渲染引擎 相关的互操作性最佳实践。DevelopIntelligence 通过 appendTo 提供 JavaScriopt 和 React 培训课程,它们的前端致力于博客和 课程网站 。
我们鼓励你在多种浏览器和设备上进行测试,包括 Microsoft Edge – Windows 10 的默认浏览器 –在 dev.microsoftedge.com 上有一些免费的工具,如 F12 开发者工具 (七个各不同),文档完整的工具,它们可以帮助你调试、测试和优化网页。你也可以 访问 Edge 博客 了解更新以及来自微软开发人员和专家的最新消息。