React 概览

React 的核心思想是:封装组件。

各个组件维护自己的状态和 UI,当状态变更,自动重新渲染整个组件。

基于这种方式的一个直观感受就是我们不再需要不厌其烦地来回查找某个 DOM 元素,然后操作 DOM 去更改 UI。

React 大体包含下面这些概念:

  • 组件
  • JSX
  • Virtual DOM
  • Data Flow

这里通过一个简单的组件来快速了解这些概念,以及建立起对 React 的一个总体认识。

import React, { Component } from 'react';
import { render } from 'react-dom';

class HelloMessage extends Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}

// 加载组件到 DOM 元素 mountNode
render(<HelloMessage name="John" />, mountNode);

组件

React 应用都是构建在组件之上。

上面的 HelloMessage 就是一个 React 构建的组件,最后一句 render 会把这个组件显示到页面上的某个元素 mountNode 里面,显示的内容就是 <div>Hello John</div>

props 是组件包含的两个核心概念之一,另一个是 state(这个组件没用到)。可以把 props 看作是组件的配置属性,在组件内部是不变的,只是在调用这个组件的时候传入不同的属性(比如这里的 name)来定制显示这个组件。

JSX

从上面的代码可以看到将 HTML 直接嵌入了 JS 代码里面,这个就是 React 提出的一种叫 JSX 的语法,这应该是最开始接触 React 最不能接受的设定之一,因为前端被“表现和逻辑层分离”这种思想“洗脑”太久了。但实际上组件的 HTML 是组成一个组件不可分割的一部分,能够将 HTML 封装起来才是组件的完全体,React 发明了 JSX 让 JS 支持嵌入 HTML 不得不说是一种非常聪明的做法,让前端实现真正意义上的组件化成为了可能。

好消息是你可以不一定使用这种语法,后面会进一步介绍 JSX,到时候你可能就会喜欢上了。现在要知道的是,要使用包含 JSX 的组件,是需要“编译”输出 JS 代码才能使用的,之后就会讲到开发环境。

Virtual DOM

当组件状态 state 有更改的时候,React 会自动调用组件的 render 方法重新渲染整个组件的 UI。

当然如果真的这样大面积的操作 DOM,性能会是一个很大的问题,所以 React 实现了一个Virtual DOM,组件 DOM 结构就是映射到这个 Virtual DOM 上,React 在这个 Virtual DOM 上实现了一个 diff 算法,当要重新渲染组件的时候,会通过 diff 寻找到要变更的 DOM 节点,再把这个修改更新到浏览器实际的 DOM 节点上,所以实际上不是真的渲染整个 DOM 树。这个 Virtual DOM 是一个纯粹的 JS 数据结构,所以性能会比原生 DOM 快很多。

Data Flow

“单向数据绑定”是 React 推崇的一种应用架构的方式。当应用足够复杂时才能体会到它的好处,虽然在一般应用场景下你可能不会意识到它的存在,也不会影响你开始使用 React,你只要先知道有这么个概念。

开发环境配置

要搭建一个现代的前端开发环境配套的工具有很多,比如 Grunt / Gulp / Webpack / Broccoli,都是要解决前端工程化问题,这个主题很大,这里为了使用 React 我们只关注其中的两个点:

  • JSX 支持
  • ES6 支持

好消息是业界领先的 ES6 编译工具 Babel 随着作者被 Facebook 招入麾下,已经内置了对 JSX 的支持,我们只需要配置 Babel 一个编译工具就可以了,配合 webpack 非常方便。

Webpack 配置 React 开发环境

Webpack 是一个前端资源加载/打包工具,只需要相对简单的配置就可以提供前端工程化需要的各种功能,并且如果有需要它还可以被整合到其他比如 Grunt / Gulp 的工作流。

安装 Webpack:npm install -g webpack

Webpack 使用一个名为 webpack.config.js 的配置文件,要编译 JSX,先安装对应的 loader: npm install babel-loader --save-dev

假设我们在当前工程目录有一个入口文件 entry.js,React 组件放置在一个 components/ 目录下,组件被 entry.js 引用,要使用 entry.js,我们把这个文件指定输出到 dist/bundle.js,Webpack 配置如下:

var path = require('path');

module.exports = {
    entry: './entry.js',
    output: {
        path: path.join(__dirname, '/dist'),
        filename: 'bundle.js'
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [
            { test: /\.js|jsx$/, loaders: ['babel'] }
        ]
    }
}

resolve 指定可以被 import 的文件后缀。比如 Hello.jsx 这样的文件就可以直接用 import Hello from 'Hello' 引用。

loaders 指定 babel-loader 编译后缀名为 .js 或者 .jsx 的文件,这样你就可以在这两种类型的文件中自由使用 JSX 和 ES6 了。

监听编译: webpack -d --watch

更多关于 Webpack 的介绍

JSX

为什么要引入 JSX 这种语法

传统的 MVC 是将模板放在其他地方,比如 <script> 标签或者模板文件,再在 JS 中通过某种手段引用模板。按照这种思路,想想多少次我们面对四处分散的模板片段不知所措?纠结模板引擎,纠结模板存放位置,纠结如何引用模板……下面是一段 React 官方的看法:

We strongly believe that components are the right way to separate concerns rather than “templates” and “display logic.” We think that markup and the code that generates it are intimately tied together. Additionally, display logic is often very complex and using template languages to express it becomes cumbersome.

简单来说,React 认为组件才是王道,而组件是和模板紧密关联的,组件模板和组件逻辑分离让问题复杂化了。显而易见的道理,关键是怎么做?

所以就有了 JSX 这种语法,就是为了把 HTML 模板直接嵌入到 JS 代码里面,这样就做到了模板和组件关联,但是 JS 不支持这种包含 HTML 的语法,所以需要通过工具将 JSX 编译输出成 JS 代码才能使用。

JSX 是可选的

因为 JSX 最终是输出成 JS 代码来表达的,所以我们可以直接用 React 提供的这些 DOM 构建方法来写模板,比如一个 JSX 写的一个链接:

<a href="http://facebook.github.io/react/">Hello!</a>

用 JS 代码来写就成这样了:

React.createElement('a', {href: 'http://facebook.github.io/react/'}, 'Hello!')

你可以通过 React.createElement 来构造组件的 DOM 树。第一个参数是标签名,第二个参数是属性对象,第三个参数是子元素。

一个包含子元素的例子:

var child = React.createElement('li', null, 'Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child);
React.render(root, document.body);

对于常见的 HTML 标签,React 已经内置了工厂方法:

var root = React.DOM.ul({ className: 'my-list' },
             React.DOM.li(null, 'Text Content')
           );

所以 JSX 和 JS 之间的转换也很简单直观,用 JSX 的好处就是它基本上就是 HTML(后面会讲到有一些小差异),对于构造 DOM 来说我们更熟悉,更具可读性。

关于 JSX 映射成 JS 对象,也就是 Virtual DOM 的内部描述,参见Virtual DOM Terminology,如果你不想使用 JSX,直接使用 JS 就是用这里面提到的接口方法。

使用 JSX

利用 JSX 编写 DOM 结构,可以用原生的 HTML 标签,也可以直接像普通标签一样引用 React 组件。这两者约定通过大小写来区分,小写的字符串是 HTML 标签,大写开头的变量是 React 组件。

使用 HTML 标签:

import React from 'react';
import { render } from 'react-dom';

var myDivElement = <div className="foo" />;
render(myDivElement, document.getElementById('mountNode'));

HTML 里的 class 在 JSX 里要写成 className,因为 class 在 JS 里是保留关键字。同理某些属性比如 for 要写成 htmlFor

使用组件:

import React from 'react';
import { render } from 'react-dom';
import MyComponent from './MyComponet';

var myElement = <MyComponent someProperty={true} />;
render(myElement, document.body);

使用 JavaScript 表达式

属性值使用表达式,只要用 {} 替换 "":

// Input (JSX):
var person = <Person name={window.isLoggedIn ? window.name : ''} />;
// Output (JS):
var person = React.createElement(
  Person,
  {name: window.isLoggedIn ? window.name : ''}
);

子组件也可以作为表达式使用:

// Input (JSX):
var content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>;
// Output (JS):
var content = React.createElement(
  Container,
  null,
  window.isLoggedIn ? React.createElement(Nav) : React.createElement(Login)
);

注释

在 JSX 里使用注释也很简单,就是沿用 JavaScript,唯一要注意的是在一个组件的子元素位置使用注释要用 {} 包起来。

var content = (
  <Nav>
      {/* child comment, put {} around */}
      <Person
        /* multi
           line
           comment */
        name={window.isLoggedIn ? window.name : ''} // end of line comment
      />
  </Nav>
);

HTML 转义

React 会将所有要显示到 DOM 的字符串转义,防止 XSS。所以如果 JSX 中含有转义后的实体字符比如 &copy; (©) 最后显示到 DOM 中不会正确显示,因为 React 自动把 &copy; 中的特殊字符转义了。有几种解决办法:

  • 直接使用 UTF-8 字符 ©
  • 使用对应字符的 Unicode 编码,查询编码
  • 使用数组组装 <div>{['cc ', <span>&copy;</span>, ' 2015']}</div>
  • 直接插入原始的 HTML
<div dangerouslySetInnerHTML={{__html: 'cc © 2015'}} />

自定义 HTML 属性

如果在 JSX 中使用的属性不存在于 HTML 的规范中,这个属性会被忽略。如果要使用自定义属性,可以用 data- 前缀。

可访问性属性的前缀 aria- 也是支持的。

支持的标签和属性

如果你要使用的某些标签或属性不在这些支持列表里面就可能被 React 忽略,必须要使用的话可以提 issue,或者用前面提到的 dangerouslySetInnerHTML

属性扩散

有时候你需要给组件设置多个属性,你不想一个个写下这些属性,或者有时候你甚至不知道这些属性的名称,这时候 spread attributes 的功能就很有用了。

比如:

var props = {};
props.foo = x;
props.bar = y;
var component = <Component {...props} />;

props 对象的属性会被设置成 Component 的属性。

属性也可以被覆盖:

var props = { foo: 'default' };
var component = <Component {...props} foo={'override'} />;
console.log(component.props.foo); // 'override'

写在后面的属性值会覆盖前面的属性。

关于 ... 操作符

The ... operator (or spread operator) is already supported for arrays in ES6. There is also an ES7 proposal for Object Rest and Spread Properties.

React 组件

可以这么说,一个 React 应用就是构建在 React 组件之上的。

组件有两个核心概念:

  • props
  • state

一个组件就是通过这两个属性的值在 render 方法里面生成这个组件对应的 HTML 结构。

注意:组件生成的 HTML 结构只能有一个单一的根节点。

props

前面也提到很多次了,props 就是组件的属性,由外部通过 JSX 属性传入设置,一旦初始设置完成,就可以认为 this.props 是不可更改的,所以不要轻易更改设置 this.props 里面的值(虽然对于一个 JS 对象你可以做任何事)。

state

state 是组件的当前状态,可以把组件简单看成一个“状态机”,根据状态 state 呈现不同的 UI 展示。

一旦状态(数据)更改,组件就会自动调用 render 重新渲染 UI,这个更改的动作会通过 this.setState方法来触发。

划分状态数据

一条原则:让组件尽可能地少状态。

这样组件逻辑就越容易维护。

什么样的数据属性可以当作状态?

当更改这个状态(数据)需要更新组件 UI 的就可以认为是 state,下面这些可以认为不是状态:

  • 可计算的数据:比如一个数组的长度
  • 和 props 重复的数据:除非这个数据是要做变更的

最后回过头来反复看几遍 Thinking in React,相信会对组件有更深刻的认识。

无状态组件

你也可以用纯粹的函数来定义无状态的组件(stateless function),这种组件没有状态,没有生命周期,只是简单的接受 props 渲染生成 DOM 结构。无状态组件非常简单,开销很低,如果可能的话尽量使用无状态组件。比如使用箭头函数定义:

const HelloMessage = (props) => <div> Hello {props.name}</div>;
render(<HelloMessage name="John" />, mountNode);

因为无状态组件只是函数,所以它没有实例返回,这点在想用 refs 获取无状态组件的时候要注意,参见DOM 操作

组件生命周期

一般来说,一个组件类由 extends Component 创建,并且提供一个 render 方法以及其他可选的生命周期函数、组件相关的事件或方法来定义。

一个简单的例子:

import React, { Component } from 'react';
import { render } from 'react-dom';

class LikeButton extends Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  handleClick(e) {
    this.setState({ liked: !this.state.liked });
  }

  render() {
    const text = this.state.liked ? 'like' : 'haven\'t liked';
    return (
      <p onClick={this.handleClick.bind(this)}>
          You {text} this. Click to toggle.
      </p>
    );
  }
}

render(
    <LikeButton />,
    document.getElementById('example')
);

getInitialState

初始化 this.state 的值,只在组件装载之前调用一次。

如果是使用 ES6 的语法,你也可以在构造函数中初始化状态,比如:

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: props.initialCount };
  }

  render() {
    // ...
  }
}

getDefaultProps

只在组件创建时调用一次并缓存返回的对象(即在 React.createClass 之后就会调用)。

因为这个方法在实例初始化之前调用,所以在这个方法里面不能依赖 this 获取到这个组件的实例。

在组件装载之后,这个方法缓存的结果会用来保证访问 this.props 的属性时,当这个属性没有在父组件中传入(在这个组件的 JSX 属性里设置),也总是有值的。

如果是使用 ES6 语法,可以直接定义 defaultProps 这个类属性来替代,这样能更直观的知道 default props 是预先定义好的对象值:

Counter.defaultProps = { initialCount: 0 };

render

必须

组装生成这个组件的 HTML 结构(使用原生 HTML 标签或者子组件),也可以返回 null 或者 false,这时候 ReactDOM.findDOMNode(this) 会返回 null

生命周期函数

装载组件触发

componentWillMount

只会在装载之前调用一次,在 render 之前调用,你可以在这个方法里面调用 setState 改变状态,并且不会导致额外调用一次 render

componentDidMount

只会在装载完成之后调用一次,在 render 之后调用,从这里开始可以通过 ReactDOM.findDOMNode(this)获取到组件的 DOM 节点。

更新组件触发

这些方法不会在首次 render 组件的周期调用

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate

卸载组件触发

  • componentWillUnmount

更多关于组件相关的方法说明,参见:

事件处理

一个简单的例子:

import React, { Component } from 'react';
import { render } from 'react-dom';

class LikeButton extends Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  handleClick(e) {
    this.setState({ liked: !this.state.liked });
  }

  render() {
    const text = this.state.liked ? 'like' : 'haven\'t liked';
    return (
      <p onClick={this.handleClick.bind(this)}>
          You {text} this. Click to toggle.
      </p>
    );
  }
}

render(
    <LikeButton />,
    document.getElementById('example')
);

可以看到 React 里面绑定事件的方式和在 HTML 中绑定事件类似,使用驼峰式命名指定要绑定的 onClick属性为组件定义的一个方法 {this.handleClick.bind(this)}

注意要显式调用 bind(this) 将事件函数上下文绑定要组件实例上,这也是 React 推崇的原则:没有黑科技,尽量使用显式的容易理解的 JavaScript 代码。

“合成事件”和“原生事件”

React 实现了一个“合成事件”层(synthetic event system),这个事件模型保证了和 W3C 标准保持一致,所以不用担心有什么诡异的用法,并且这个事件层消除了 IE 与 W3C 标准实现之间的兼容问题。

“合成事件”还提供了额外的好处:

事件委托

“合成事件”会以事件委托(event delegation)的方式绑定到组件最上层,并且在组件卸载(unmount)的时候自动销毁绑定的事件。

什么是“原生事件”?

比如你在 componentDidMount 方法里面通过 addEventListener 绑定的事件就是浏览器原生事件。

使用原生事件的时候注意在 componentWillUnmount 解除绑定 removeEventListener

所有通过 JSX 这种方式绑定的事件都是绑定到“合成事件”,除非你有特别的理由,建议总是用 React 的方式处理事件。

Tips

关于这两种事件绑定的使用,这里有必要分享一些额外的人生经验

如果混用“合成事件”和“原生事件”,比如一种常见的场景是用原生事件在 document 上绑定,然后在组件里面绑定的合成事件想要通过 e.stopPropagation() 来阻止事件冒泡到 document,这时候是行不通的,参见 Event delegation,因为 e.stopPropagation 是内部“合成事件” 层面的,解决方法是要用 e.nativeEvent.stopImmediatePropagation()

”合成事件“ 的 event 对象只在当前 event loop 有效,比如你想在事件里面调用一个 promise,在 resolve 之后去拿 event 对象会拿不到(并且没有错误抛出):

handleClick(e) {
  promise.then(() => doSomethingWith(e));
}

详情见 Event pooling 说明。

参数传递

给事件处理函数传递额外参数的方式:bind(this, arg1, arg2, ...)

render: function() {
    return <p onClick={this.handleClick.bind(this, 'extra param')}>;
},
handleClick: function(param, event) {
    // handle click
}

React 支持的事件列表