├── .editorconfig
├── .gitignore
├── README.md
├── SUMMARY.md
├── component-communicate.md
├── component-compose.md
├── component-lifecycle.md
├── component.md
├── config.json
├── cover
├── background.jpg
└── logo.png
├── data-flow.md
├── dom.md
├── environment.md
├── events.md
├── flux-evolution.md
├── flux.md
├── forms.md
├── image
└── flux-overview.png
├── introduction.md
├── jsx-gotchas.md
├── jsx-in-depth.md
├── jsx-spread-attributes.md
├── jsx.md
├── mixin.md
├── redux-basic.md
├── redux.md
├── server-rendering.md
├── share
└── simple-component.md
├── usage-with-react.md
└── webpack.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
13 | [Makefile]
14 | indent_stype = tab
15 |
16 | ; Match multi exact files ( should no spaces between , )
17 | ; [{package.json,.travis.yml}]
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /_book/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React 入门教程
2 |
3 | 按照惯例,在介绍一个新技术之前总是要为它背书的,作为 React
4 | 受众在开始接触之前肯定会有一些喜闻乐见的疑问:
5 |
6 | - 为什么不用 Backbone?
7 | - 为什么不用 Angular?
8 | - ...
9 |
10 | 在没有真正使用之前,其实没法评价哪一个好,没有最好的,只有最合适的,如 [Why
11 | React](http://facebook.github.io/react/docs/why-react.html) 所说,[Give it five
12 | minutes](http://37signals.com/svn/posts/3124-give-it-five-minutes),希望你能克服初次遇到
13 | JSX 这种存在的偏见去尝试一下。
14 |
15 | 因为官方文档组织得比较散乱,希望本教程能成为一个不错的入门参考。
16 |
17 | 有任何问题 → [Github](https://github.com/hulufei/react-tutorial)
18 |
19 | 本文档对应 React v0.14.x,使用 ES6。
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | React 入门指南
4 |
5 | - [React 概览](introduction.md)
6 | - [开发环境配置](environment.md)
7 | - [Webpack](webpack.md)
8 | - [JSX](jsx.md)
9 | - [使用 JSX](jsx-in-depth.md)
10 | - [属性扩散](jsx-spread-attributes.md)
11 | - [和 HTML 的差异](jsx-gotchas.md)
12 | - [组件](component.md)
13 | - [组件生命周期](component-lifecycle.md)
14 | - [事件处理](events.md)
15 | - [DOM 操作](dom.md)
16 | - [组合组件](component-compose.md)
17 | - [组件间通信](component-communicate.md)
18 | - [Mixins](mixin.md)
19 | - [Data Flow](data-flow.md)
20 | - [Flux](flux.md)
21 | - [Redux](redux.md)
22 | - [进化 Flux](flux-evolution.md)
23 | - [Redux 基础](redux-basic.md)
24 | - [和 React 配合使用](usage-with-react.md)
25 | - [Redux 进阶]
26 | - [表单](forms.md)
27 | - [动画]
28 | - [测试]
29 | - [性能调优]
30 | - [服务端渲染](server-rendering.md)
31 |
--------------------------------------------------------------------------------
/component-communicate.md:
--------------------------------------------------------------------------------
1 | # 组件间通信
2 |
3 | ## 父子组件间通信
4 |
5 | 这种情况下很简单,就是通过 `props` 属性传递,在父组件给子组件设置 `props`,然后子组件就可以通过 `props` 访问到父组件的数据/方法,这样就搭建起了父子组件间通信的桥梁。
6 |
7 | ```javascript
8 | import React, { Component } from 'react';
9 | import { render } from 'react-dom';
10 |
11 | class GroceryList extends Component {
12 | handleClick(i) {
13 | console.log('You clicked: ' + this.props.items[i]);
14 | }
15 |
16 | render() {
17 | return (
18 |
19 | {this.props.items.map((item, i) => {
20 | return (
21 |
{item}
22 | );
23 | })}
24 |
25 | );
26 | }
27 | }
28 |
29 | render(
30 | , mountNode
31 | );
32 | ```
33 |
34 | `div` 可以看作一个子组件,指定它的 `onClick` 事件调用父组件的方法。
35 |
36 | 父组件访问子组件?用 `refs`
37 |
38 | ## 非父子组件间的通信
39 |
40 | 使用全局事件 Pub/Sub 模式,在 `componentDidMount` 里面订阅事件,在
41 | `componentWillUnmount` 里面取消订阅,当收到事件触发的时候调用 `setState` 更新
42 | UI。
43 |
44 | 这种模式在复杂的系统里面可能会变得难以维护,所以看个人权衡是否将组件封装到大的组件,甚至整个页面或者应用就封装到一个组件。
45 |
46 | 一般来说,对于比较复杂的应用,推荐使用类似 Flux 这种单项数据流架构,参见[Data
47 | Flow](data-flow.md)。
48 |
--------------------------------------------------------------------------------
/component-compose.md:
--------------------------------------------------------------------------------
1 | # 组合组件
2 |
3 | 使用组件的目的就是通过构建模块化的组件,相互组合组件最后组装成一个复杂的应用。
4 |
5 | 在 React 组件中要包含其他组件作为子组件,只需要把组件当作一个 DOM
6 | 元素引入就可以了。
7 |
8 | 一个例子:一个显示用户头像的组件 `Avatar` 包含两个子组件 `ProfilePic` 显示用户头像和 `ProfileLink` 显示用户链接:
9 |
10 | ```javascript
11 | import React from 'react';
12 | import { render } from 'react-dom';
13 |
14 | const ProfilePic = (props) => {
15 | return (
16 |
17 | );
18 | }
19 |
20 | const ProfileLink = (props) => {
21 | return (
22 |
23 | {props.username}
24 |
25 | );
26 | }
27 |
28 | const Avatar = (props) => {
29 | return (
30 |
34 | );
35 | }
36 |
37 | render(
38 | ,
39 | document.getElementById('example')
40 | );
41 | ```
42 |
43 | 通过 `props` 传递值。
44 |
45 | ## 循环插入子元素
46 |
47 | 如果组件中包含通过循环插入的子元素,为了保证重新渲染 UI
48 | 的时候能够正确显示这些子元素,每个元素都需要通过一个特殊的 `key`
49 | 属性指定一个唯一值。具体原因见[这里](http://facebook.github.io/react/docs/reconciliation.html),为了内部 diff 的效率。
50 |
51 | `key` 必须直接在循环中设置:
52 |
53 | ```javascript
54 | const ListItemWrapper = (props) => {props.data.text} ;
55 |
56 | const MyComponent = (props) => {
57 | return (
58 |
59 | {props.results.map((result) => {
60 | return ;
61 | })}
62 |
63 | );
64 | }
65 | ```
66 |
67 | 你也可以用一个 `key` 值作为属性,子元素作为属性值的对象字面量来显示子元素列表,虽然这种用法的场景有限,参见[Keyed Fragments](http://facebook.github.io/react/docs/create-fragment.html),但是在这种情况下要注意生成的子元素重新渲染后在 DOM 中显示的顺序问题。
68 |
69 | 实际上浏览器在遍历一个字面量对象的时候会保持顺序一致,除非存在属性值可以被转换成整数值,这种属性值会排序并放在其他属性之前被遍历到,所以为了防止这种情况发生,可以在构建这个字面量的时候在
70 | `key` 值前面加字符串前缀,比如:
71 |
72 | ```javascript
73 | render() {
74 | var items = {};
75 |
76 | this.props.results.forEach((result) => {
77 | // If result.id can look like a number (consider short hashes), then
78 | // object iteration order is not guaranteed. In this case, we add a prefix
79 | // to ensure the keys are strings.
80 | items['result-' + result.id] = {result.text} ;
81 | });
82 |
83 | return (
84 |
85 | {items}
86 |
87 | );
88 | }
89 | ```
90 |
91 | ## `this.props.children`
92 |
93 | 组件标签里面包含的子元素会通过 `props.children` 传递进来。
94 |
95 | 比如:
96 |
97 | ```javascript
98 | React.render( , document.body);
99 |
100 | React.render(hello {'world'} , document.body);
101 | ```
102 |
103 | HTML 元素会作为 React 组件对象、JS 表达式结果是一个文字节点,都会存入 `Parent` 组件的 `props.children`。
104 |
105 | 一般来说,可以直接将这个属性作为父组件的子元素 render:
106 |
107 | ```javascript
108 | const Parent = (props) => {props.children}
;
109 | ```
110 |
111 | `props.children`
112 | 通常是一个组件对象的数组,但是当只有一个子元素的时候,`props.children`
113 | 将是这个唯一的子元素,而不是数组了。
114 |
115 |
116 | [`React.Children`](http://facebook.github.io/react/docs/top-level-api.html#react.children) 提供了额外的方法方便操作这个属性。
117 |
--------------------------------------------------------------------------------
/component-lifecycle.md:
--------------------------------------------------------------------------------
1 | # 组件生命周期
2 |
3 | 一般来说,一个组件类由 `extends Component` 创建,并且提供一个 `render`
4 | 方法以及其他可选的生命周期函数、组件相关的事件或方法来定义。
5 |
6 | {% include './share/simple-component.md' %}
7 |
8 | ## `getInitialState`
9 |
10 | 初始化 `this.state` 的值,只在组件装载之前调用一次。
11 |
12 | 如果是使用 ES6 的语法,你也可以在构造函数中初始化状态,比如:
13 |
14 | ```javascript
15 | class Counter extends Component {
16 | constructor(props) {
17 | super(props);
18 | this.state = { count: props.initialCount };
19 | }
20 |
21 | render() {
22 | // ...
23 | }
24 | }
25 | ```
26 |
27 | ## `getDefaultProps`
28 |
29 | 只在组件创建时调用一次并缓存返回的对象(即在 `React.createClass` 之后就会调用)。
30 |
31 | 因为这个方法在实例初始化之前调用,所以在这个方法里面不能依赖
32 | `this` 获取到这个组件的实例。
33 |
34 | 在组件装载之后,这个方法缓存的结果会用来保证访问 `this.props` 的属性时,当这个属性没有在父组件中传入(在这个组件的 JSX
35 | 属性里设置),也总是有值的。
36 |
37 | 如果是使用 ES6 语法,可以直接定义 `defaultProps`
38 | 这个类属性来替代,这样能更直观的知道 default props 是预先定义好的对象值:
39 |
40 | ```javascript
41 | Counter.defaultProps = { initialCount: 0 };
42 | ```
43 |
44 | ## `render`
45 |
46 | **必须**
47 |
48 | 组装生成这个组件的 HTML 结构(使用原生 HTML 标签或者子组件),也可以返回 `null` 或者 `false`,这时候 `ReactDOM.findDOMNode(this)` 会返回 `null`。
49 |
50 | ## 生命周期函数
51 |
52 | ### 装载组件触发
53 |
54 | `componentWillMount`
55 |
56 | 只会在装载之前调用一次,在 `render` 之前调用,你可以在这个方法里面调用 `setState`
57 | 改变状态,并且不会导致额外调用一次 `render`
58 |
59 | `componentDidMount`
60 |
61 | 只会在装载完成之后调用一次,在 `render` 之后调用,从这里开始可以通过
62 | `ReactDOM.findDOMNode(this)` 获取到组件的 DOM 节点。
63 |
64 | ### 更新组件触发
65 |
66 | 这些方法不会在首次 `render` 组件的周期调用
67 |
68 | - `componentWillReceiveProps`
69 | - `shouldComponentUpdate`
70 | - `componentWillUpdate`
71 | - `componentDidUpdate`
72 |
73 | ### 卸载组件触发
74 |
75 | - `componentWillUnmount`
76 |
77 | 更多关于组件相关的方法说明,参见:
78 |
79 | - [Component Specs](http://facebook.github.io/react/docs/component-specs.html)
80 | - [Component
81 | Lifecycle](http://facebook.github.io/react/docs/working-with-the-browser.html#component-lifecycle)
82 | - [Component API](http://facebook.github.io/react/docs/component-api.html)
83 |
--------------------------------------------------------------------------------
/component.md:
--------------------------------------------------------------------------------
1 | # React 组件
2 |
3 | 可以这么说,一个 React 应用就是构建在 React 组件之上的。
4 |
5 | 组件有两个核心概念:
6 |
7 | - props
8 | - state
9 |
10 | 一个组件就是通过这两个属性的值在 `render` 方法里面生成这个组件对应的 HTML 结构。
11 |
12 | _注意:组件生成的 HTML 结构只能有一个单一的根节点。_
13 |
14 | ## props
15 |
16 | 前面也提到很多次了,`props` 就是组件的属性,由外部通过 JSX
17 | 属性传入设置,一旦初始设置完成,就可以认为 `this.props` 是不可更改的,所以**不要**轻易更改设置 `this.props` 里面的值(虽然对于一个 JS 对象你可以做任何事)。
18 |
19 | ## state
20 |
21 | `state` 是组件的当前状态,可以把组件简单看成一个“状态机”,根据状态 `state`
22 | 呈现不同的 UI 展示。
23 |
24 | 一旦状态(数据)更改,组件就会自动调用 `render` 重新渲染 UI,这个更改的动作会通过
25 | `this.setState` 方法来触发。
26 |
27 | ## 划分状态数据
28 |
29 | 一条原则:让组件尽可能地少状态。
30 |
31 | 这样组件逻辑就越容易维护。
32 |
33 | 什么样的数据属性可以当作状态?
34 |
35 | 当更改这个状态(数据)需要更新组件 UI 的就可以认为是 `state`,下面这些可以认为**不是**状态:
36 |
37 | - 可计算的数据:比如一个数组的长度
38 | - 和 props 重复的数据:除非这个数据是要做变更的
39 |
40 | 最后回过头来反复看几遍 [Thinking in
41 | React](http://facebook.github.io/react/docs/thinking-in-react.html),相信会对组件有更深刻的认识。
42 |
43 | ## 无状态组件
44 |
45 | 你也可以用纯粹的函数来定义无状态的组件(stateless function),这种组件没有状态,没有生命周期,只是简单的接受 props 渲染生成 DOM 结构。无状态组件非常简单,开销很低,如果可能的话尽量使用无状态组件。比如使用箭头函数定义:
46 |
47 | ```javascript
48 | const HelloMessage = (props) => Hello {props.name}
;
49 | render( , mountNode);
50 | ```
51 |
52 | 因为无状态组件只是函数,所以它没有实例返回,这点在想用 refs
53 | 获取无状态组件的时候要注意,参见[DOM 操作](dom.md)。
54 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "React 入门教程",
3 | "introduction": "因为官方文档组织得比较散乱,希望本教程能成为一个不错的入门参考。",
4 | "path" : {
5 | "toc" : "SUMMARY.md",
6 | "images" : "image"
7 | }
8 | }
--------------------------------------------------------------------------------
/cover/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulufei/react-tutorial/c3c335cb6bd06bec8d3313a58faba9226b957c0d/cover/background.jpg
--------------------------------------------------------------------------------
/cover/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulufei/react-tutorial/c3c335cb6bd06bec8d3313a58faba9226b957c0d/cover/logo.png
--------------------------------------------------------------------------------
/data-flow.md:
--------------------------------------------------------------------------------
1 | # Data Flow
2 |
3 | Data Flow
4 | 只是一种应用架构的方式,比如数据如何存放,如何更改数据,如何通知数据更改等等,所以它不是 React
5 | 提供的额外的什么新功能,可以看成是使用 React
6 | 构建大型应用的一种最佳实践。
7 |
8 | 正因为它是这样一种概念,所以涌现了[许多实现](https://github.com/enaqx/awesome-react#flux),这里主要关注两种实现:
9 |
10 | - 官方的 [Flux](http://facebook.github.io/flux/docs/overview.html)
11 | - 更优雅的 [Redux](https://github.com/rackt/redux)
12 |
--------------------------------------------------------------------------------
/dom.md:
--------------------------------------------------------------------------------
1 | # DOM 操作
2 |
3 | 大部分情况下你不需要通过查询 DOM 元素去更新组件的
4 | UI,你只要关注设置组件的状态(`setState`)。但是可能在某些情况下你确实需要直接操作 DOM。
5 |
6 | 首先我们要了解 `ReactDOM.render` 组件返回的是什么?
7 |
8 | 它会返回对组件的引用也就是组件实例(对于无状态状态组件来说返回 null),注意 JSX
9 | 返回的不是组件实例,它只是一个 `ReactElement` 对象(还记得我们用纯 JS 来构建 JSX
10 | 的方式吗),比如这种:
11 |
12 | ```javascript
13 | // A ReactElement
14 | const myComponent =
15 |
16 | // render
17 | const myComponentInstance = ReactDOM.render(myComponent, mountNode);
18 | myComponentInstance.doSomething();
19 | ```
20 |
21 | ## findDOMNode()
22 |
23 | 当组件加载到页面上之后(mounted),你都可以通过 `react-dom` 提供的 `findDOMNode()` 方法拿到组件对应的
24 | DOM 元素。
25 |
26 | ```javascript
27 | import { findDOMNode } from 'react-dom';
28 |
29 | // Inside Component class
30 | componentDidMound() {
31 | const el = findDOMNode(this);
32 | }
33 | ```
34 |
35 | `findDOMNode()` 不能用在无状态组件上。
36 |
37 | ## Refs
38 |
39 | 另外一种方式就是通过在要引用的 DOM 元素上面设置一个 `ref`
40 | 属性指定一个名称,然后通过 `this.refs.name` 来访问对应的 DOM 元素。
41 |
42 | 比如有一种情况是必须直接操作 DOM 来实现的,你希望一个 ` `
43 | 元素在你清空它的值时 focus,你没法仅仅靠 `state` 来实现这个功能。
44 |
45 | ```javascript
46 | class App extends Component {
47 | constructor() {
48 | return { userInput: '' };
49 | }
50 |
51 | handleChange(e) {
52 | this.setState({ userInput: e.target.value });
53 | }
54 |
55 | clearAndFocusInput() {
56 | this.setState({ userInput: '' }, () => {
57 | this.refs.theInput.focus();
58 | });
59 | }
60 |
61 | render() {
62 | return (
63 |
64 |
65 | Click to Focus and Reset
66 |
67 |
72 |
73 | );
74 | }
75 | }
76 | ```
77 |
78 | 如果 `ref` 是设置在原生 HTML 元素上,它拿到的就是 DOM
79 | 元素,如果设置在自定义组件上,它拿到的就是组件实例,这时候就需要通过
80 | `findDOMNode` 来拿到组件的 DOM 元素。
81 |
82 | 因为无状态组件没有实例,所以 ref
83 | 不能设置在无状态组件上,一般来说这没什么问题,因为无状态组件没有实例方法,不需要
84 | ref 去拿实例调用相关的方法,但是如果想要拿无状态组件的 DOM
85 | 元素的时候,就需要用一个状态组件封装一层,然后通过 `ref` 和 `findDOMNode`
86 | 去获取。
87 |
88 | ## 总结
89 |
90 | - 你可以使用 ref 到的组件定义的任何公共方法,比如 `this.refs.myTypeahead.reset()`
91 | - Refs 是访问到组件内部 DOM 节点唯一**可靠**的方法
92 | - Refs 会自动销毁对子组件的引用(当子组件删除时)
93 |
94 | ### 注意事项
95 |
96 | - 不要在 `render` 或者 `render` 之前访问 `refs`
97 | - 不要滥用 `refs`,比如只是用它来按照传统的方式操作界面 UI:找到 DOM -> 更新 DOM
98 |
--------------------------------------------------------------------------------
/environment.md:
--------------------------------------------------------------------------------
1 | # 开发环境配置
2 |
3 | 要搭建一个现代的前端开发环境配套的工具有很多,比如 Grunt / Gulp / Webpack
4 | / Broccoli,都是要解决前端工程化问题,这个主题很大,这里为了使用 React 我们只关注其中的两个点:
5 |
6 | - JSX 支持
7 | - ES6 支持
8 |
9 | 好消息是业界领先的 ES6 编译工具 [Babel](http://babeljs.io/) 随着作者被 Facebook [招入麾下](https://twitter.com/sebmck/status/620736636830285824),已经内置了对 JSX 的支持,我们只需要配置 Babel 一个编译工具就可以了,配合 webpack 非常方便。
10 |
--------------------------------------------------------------------------------
/events.md:
--------------------------------------------------------------------------------
1 | # 事件处理
2 |
3 | {% include './share/simple-component.md' %}
4 |
5 | 可以看到 React 里面绑定事件的方式和在 HTML
6 | 中绑定事件类似,使用驼峰式命名指定要绑定的 `onClick` 属性为组件定义的一个方法 `{this.handleClick.bind(this)}`。
7 |
8 | 注意要显式调用 `bind(this)` 将事件函数上下文绑定要组件实例上,这也是 React
9 | 推崇的原则:没有黑科技,尽量使用显式的容易理解的 JavaScript 代码。
10 |
11 | ## “合成事件”和“原生事件”
12 |
13 | React 实现了一个“合成事件”层(synthetic event system),这个事件模型保证了和
14 | W3C
15 | 标准保持一致,所以不用担心有什么诡异的用法,并且这个事件层消除了 IE 与 W3C 标准实现之间的兼容问题。
16 |
17 | “合成事件”还提供了额外的好处:
18 |
19 | **事件委托**
20 |
21 | “合成事件”会以事件委托(event
22 | delegation)的方式绑定到组件最上层,并且在组件卸载(unmount)的时候自动销毁绑定的事件。
23 |
24 | **什么是“原生事件”?**
25 |
26 | 比如你在 `componentDidMount` 方法里面通过 `addEventListener`
27 | 绑定的事件就是浏览器原生事件。
28 |
29 | 使用原生事件的时候注意在 `componentWillUnmount` 解除绑定 `removeEventListener`。
30 |
31 | 所有通过 JSX
32 | 这种方式绑定的事件都是绑定到“合成事件”,除非你有特别的理由,建议总是用 React
33 | 的方式处理事件。
34 |
35 | **Tips**
36 |
37 | 关于这两种事件绑定的使用,这里有必要分享一些额外的人生经验
38 |
39 | 如果混用“合成事件”和“原生事件”,比如一种常见的场景是用原生事件在 document
40 | 上绑定,然后在组件里面绑定的合成事件想要通过 `e.stopPropagation()`
41 | 来阻止事件冒泡到 document,这时候是行不通的,参见 [Event
42 | delegation](http://stackoverflow.com/a/24421834/581094),因为 `e.stopPropagation` 是内部“合成事件” 层面的,解决方法是要用 `e.nativeEvent.stopImmediatePropagation()`
43 |
44 | ”合成事件“ 的 `event` 对象只在当前 event loop
45 | 有效,比如你想在事件里面调用一个 promise,在 resolve 之后去拿 `event`
46 | 对象会拿不到(并且没有错误抛出):
47 |
48 | ```javascript
49 | handleClick(e) {
50 | promise.then(() => doSomethingWith(e));
51 | }
52 | ```
53 |
54 | 详情见 [Event
55 | pooling](https://facebook.github.io/react/docs/events.html#event-pooling)
56 | 说明。
57 |
58 | ## 参数传递
59 |
60 | 给事件处理函数传递额外参数的方式:`bind(this, arg1, arg2, ...)`
61 |
62 | ```javascript
63 | render: function() {
64 | return ;
65 | },
66 | handleClick: function(param, event) {
67 | // handle click
68 | }
69 | ```
70 |
71 | [React 支持的事件列表](http://facebook.github.io/react/docs/events.html)
72 |
--------------------------------------------------------------------------------
/flux-evolution.md:
--------------------------------------------------------------------------------
1 | # 进化 Flux
2 |
3 | 我们可以先通过对比 Redux 和 Flux 的实现来感受一下 Redux 带来的惊艳。
4 |
5 | 首先是 _action creators_,Flux 是直接在 action 里面调用 dispatch:
6 |
7 | ```javascript
8 | export function addTodo(text) {
9 | AppDispatcher.dispatch({
10 | type: ActionTypes.ADD_TODO,
11 | text: text
12 | });
13 | }
14 | ```
15 |
16 | Redux 把它简化成了这样:
17 |
18 | ```javascript
19 | export function addTodo(text) {
20 | return {
21 | type: ActionTypes.ADD_TODO,
22 | text: text
23 | };
24 | }
25 | ```
26 |
27 | 这一步把 dispatcher 和 action 解藕了,很快我们就能看到它带来的好处。
28 |
29 | 接下来是 Store,这是 Flux 里面的 Store:
30 |
31 | ```javascript
32 | let _todos = [];
33 | const TodoStore = Object.assign(new EventEmitter(), {
34 | getTodos() {
35 | return _todos;
36 | }
37 | });
38 | AppDispatcher.register(function (action) {
39 | switch (action.type) {
40 | case ActionTypes.ADD_TODO:
41 | _todos = _todos.concat([action.text]);
42 | TodoStore.emitChange();
43 | break;
44 | }
45 | });
46 | export default TodoStore;
47 | ```
48 |
49 | Redux 把它简化成了这样:
50 |
51 | ```javascript
52 | const initialState = { todos: [] };
53 | export default function TodoStore(state = initialState, action) {
54 | switch (action.type) {
55 | case ActionTypes.ADD_TODO:
56 | return { todos: state.todos.concat([action.text]) };
57 | default:
58 | return state;
59 | }
60 | ```
61 |
62 | 同样把 dispatch 从 Store 里面剥离了,Store 变成了一个 **pure function**:`(state,
63 | action) => state`
64 |
65 | ### 什么是 pure function
66 |
67 | 如果一个函数没有任何副作用(side-effects),不会影响任何外部状态,对于任何一个相同的输入(参数),无论何时调用这个函数总是返回同样的结果,这个函数就是一个
68 | pure function。所谓 side-effects 就是会改变外部状态的因素
69 | ,比如 Ajax 请求就有
70 | side-effects,因为它带来了不确定性。
71 |
72 | 所以现在 Store
73 | 不再**拥有**状态,而只是**管理**状态,所以首先要明确一个概念,Store 和 State
74 | 是有区别的,Store 并不是一个简单的数据结构,State 才是,Store
75 | 会包含一些方法来**管理** State,比如获取/修改 State。
76 |
77 | 基于这样的 Store,可以做很多扩展,这也是
78 | Redux 强大之处。
79 |
80 | 来源:[The Evolution of Flux Frameworks](https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31)
81 |
--------------------------------------------------------------------------------
/flux.md:
--------------------------------------------------------------------------------
1 | # Flux
2 |
3 | React 标榜自己是 MVC 里面 V 的部分,那么 Flux 就相当于添加 M 和 C 的部分。
4 |
5 | Flux 是 Facebook 使用的一套前端应用的架构模式。
6 |
7 | 一个 Flux 应用主要包含四个部分:
8 |
9 | - the dispatcher
10 |
11 | _处理动作分发,维护 Store 之间的依赖关系_
12 | - the stores
13 |
14 | _数据和逻辑部分_
15 | - the views
16 |
17 | _React 组件,这一层可以看作 controller-views,作为视图同时响应用户交互_
18 | - the actions
19 |
20 | _提供给 dispatcher 传递数据给 store_
21 |
22 | 针对上面提到的 Flux
23 | 这些概念,需要写一个简单的类库来实现衔接这些功能,市面上有很多种实现,这里讨论 Facebook
24 | 官方的一个实现 [Dispatcher.js](https://github.com/facebook/flux/blob/master/src/Dispatcher.js)
25 |
26 | ## 单向数据流
27 |
28 | 先来了解一下 Flux 的核心“单向数据流“怎么运作的:
29 |
30 | ```
31 | Action -> Dispatcher -> Store -> View
32 | ```
33 |
34 | 更多时候 View 会通过用户交互触发 Action,所以一个简单完整的数据流类似这样:
35 |
36 | 
37 |
38 | 整个流程如下:
39 |
40 | - 首先要有 action,通过定义一些 _action creator_ 方法根据需要创建 Action 提供给 dispatcher
41 | - View 层通过用户交互(比如 onClick)会触发 Action
42 | - Dispatcher 会分发触发的 Action 给所有注册的 Store 的回调函数
43 | - Store 回调函数根据接收的 Action 更新自身数据之后会触发一个 _change_ 事件通知 View 数据更改了
44 | - View 会监听这个 _change_ 事件,拿到对应的新数据并调用 `setState` 更新组件 UI
45 |
46 | 所有的状态都由 Store
47 | 来维护,通过 Action 传递数据,构成了如上所述的单向数据流循环,所以应用中的各部分分工就相当明确,高度解耦了。
48 |
49 | 这种单向数据流使得整个系统都是透明可预测的。
50 |
51 | ## Dispatcher
52 |
53 | 一个应用只需要一个 dispatcher 作为分发中心,管理所有数据流向,分发动作给
54 | Store,没有太多其他的逻辑(一些 _action creator_ 方法也可以放到这里)。
55 |
56 | Dispatcher 分发动作给 Store
57 | 注册的回调函数,这和一般的订阅/发布模式不同的地方在于:
58 |
59 | - 回调函数不是订阅到某一个特定的事件/频道,每个动作会分发给所有注册的回调函数
60 | - 回调函数可以指定在其他回调之后调用
61 |
62 | 基于 Flux 的架构思路,[Dispatcher.js](https://github.com/facebook/flux/blob/master/src/Dispatcher.js) 提供的 API 很简单:
63 |
64 | - **register(function callback): string** 注册回调函数,返回一个 token
65 | 供在 `waitFor()` 使用
66 | - **unregister(string id): void** 通过 token 移除回调
67 | - **waitFor(array ids): void**
68 | 在指定的回调函数执行之后才执行当前回调。这个方法只能在分发动作的回调函数中使用
69 | - **dispatch(object payload): void** 分发动作 payload 给所有注册回调
70 | - **isDispatching(): boolean** 返回 Dispatcher 当前是否处在分发的状态
71 |
72 | dispatcher 只是一个粘合剂,剩余的 Store、View、Action 就需要按具体需求去实现了。
73 |
74 | 接下来结合
75 | [flux-todomvc](https://github.com/facebook/flux/tree/master/examples/flux-todomvc/js)
76 | 这个简单的例子,提取其中的关键部分,看一下实际应用中如何衔接 Flux 整个流程,希望能对 Flux
77 | 各个部分有更直观深入的理解。
78 |
79 | ## Action
80 |
81 | 首先要创建动作,通过定义一些 _action creator_ 方法来创建,这些方法用来暴露给外部调用,通过 `dispatch` 分发对应的动作,所以 _action creator_ 也称作 _dispatcher helper methods_ 辅助 dipatcher 分发。
82 | 参见
83 | [actions/TodoActions.js](https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/actions/TodoActions.js)
84 |
85 | ```javascript
86 | var AppDispatcher = require('../dispatcher/AppDispatcher');
87 | var TodoConstants = require('../constants/TodoConstants');
88 |
89 | var TodoActions = {
90 | create: function(text) {
91 | AppDispatcher.dispatch({
92 | actionType: TodoConstants.TODO_CREATE,
93 | text: text
94 | });
95 | },
96 |
97 | updateText: function(id, text) {
98 | AppDispatcher.dispatch({
99 | actionType: TodoConstants.TODO_UPDATE_TEXT,
100 | id: id,
101 | text: text
102 | });
103 | },
104 |
105 | // 不带 payload 数据的动作
106 | toggleCompleteAll: function() {
107 | AppDispatcher.dispatch({
108 | actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
109 | });
110 | }
111 | };
112 | ```
113 |
114 | `AppDispatcher` 直接继承自
115 | Dispatcher.js,在这个简单的例子中没有提供什么额外的功能。`TodoConstants` 定义了动作的类型名称常量。
116 |
117 | 类似 `create`、`updateText` 就是 _action creator_,这两个动作会通过 View 上的用户交互触发(比如输入框)。 除了用户交互会创建动作,服务端接口调用也可以用来创建动作,比如通过 Ajax 请求的一些初始数据也可以创建动作提供给 dispatcher,再分发给 store 使用这些初始数据。
118 |
119 | > action creators are nothing more than a call into the dispatcher.
120 |
121 | 可以看到所谓动作就是用来封装传递数据的,动作只是一个简单的对象,包含两部分:payload(数据)和 type(类型),type 是一个字符串常量,用来标识动作。
122 |
123 | ## Store
124 |
125 | Stores 包含应用的状态和逻辑,不同的 Store 管理应用中不同部分的状态。如
126 | [stores/TodoStore.js](https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/stores/TodoStore.js)
127 |
128 | ```javascript
129 | var AppDispatcher = require('../dispatcher/AppDispatcher');
130 | var EventEmitter = require('events').EventEmitter;
131 | var TodoConstants = require('../constants/TodoConstants');
132 | var assign = require('object-assign');
133 |
134 | var CHANGE_EVENT = 'change';
135 |
136 | var _todos = {};
137 |
138 | // 先定义一些数据处理方法
139 | function create(text) {
140 | var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
141 | _todos[id] = {
142 | id: id,
143 | complete: false,
144 | text: text
145 | };
146 | }
147 | function update(id, updates) {
148 | _todos[id] = assign({}, _todos[id], updates);
149 | }
150 | // ...
151 |
152 | var TodoStore = assign({}, EventEmitter.prototype, {
153 | // Getter 方法暴露给外部获取 Store 数据
154 | getAll: function() {
155 | return _todos;
156 | },
157 | // 触发 change 事件
158 | emitChange: function() {
159 | this.emit(CHANGE_EVENT);
160 | },
161 | // 提供给外部 View 绑定 change 事件
162 | addChangeListener: function(callback) {
163 | this.on(CHANGE_EVENT, callback);
164 | }
165 | });
166 |
167 | // 注册到 dispatcher,通过动作类型过滤处理当前 Store 关心的动作
168 | AppDispatcher.register(function(action) {
169 | var text;
170 |
171 | switch(action.actionType) {
172 | case TodoConstants.TODO_CREATE:
173 | text = action.text.trim();
174 | if (text !== '') {
175 | create(text);
176 | }
177 | TodoStore.emitChange();
178 | break;
179 |
180 | case TodoConstants.TODO_UPDATE_TEXT:
181 | text = action.text.trim();
182 | if (text !== '') {
183 | update(action.id, {text: text});
184 | }
185 | TodoStore.emitChange();
186 | break;
187 | }
188 | });
189 | ```
190 |
191 | 在 Store 注册给 dispatcher 的回调函数中会接受到分发的 action,因为每个 action 都会分发给所有注册的回调,所以回调函数里面要判断这个 action 的 _type_ 并调用相关的内部方法处理更新 action 带过来的数据(payload),再通知 view 数据变更。
192 |
193 | Store 里面不会暴露直接操作数据的方法给外部,暴露给外部调用的方法都是 Getter
194 | 方法,没有 Setter 方法,唯一更新数据的手段就是通过在 dispatcher 注册的回调函数。
195 |
196 | ## View
197 |
198 | View 就是 React 组件,从 Store 获取状态(数据),绑定 change
199 | 事件处理。如
200 | [components/TodoApp.react.js](https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoApp.react.js)
201 |
202 | ```javascript
203 | var React = require('react');
204 | var TodoStore = require('../stores/TodoStore');
205 |
206 | function getTodoState() {
207 | return {
208 | allTodos: TodoStore.getAll(),
209 | areAllComplete: TodoStore.areAllComplete()
210 | };
211 | }
212 |
213 | var TodoApp = React.createClass({
214 |
215 | getInitialState: function() {
216 | return getTodoState();
217 | },
218 |
219 | componentDidMount: function() {
220 | TodoStore.addChangeListener(this._onChange);
221 | },
222 |
223 | componentWillUnmount: function() {
224 | TodoStore.removeChangeListener(this._onChange);
225 | },
226 |
227 | render: function() {
228 | return /*...*/
229 | },
230 |
231 | _onChange: function() {
232 | this.setState(getTodoState());
233 | }
234 | });
235 | ```
236 |
237 | 一个 View 可能关联多个 Store 来管理不同部分的状态,得益于 React 更新 View 如此简单(`setState`),复杂的逻辑都被 Store 隔离了。
238 |
239 | ## 更多资料
240 |
241 | - [Flux chat](https://speakerdeck.com/fisherwebdev/fluxchat) 很简洁明了的一个
242 | Slide
243 | - [flux-chat source
244 | code](https://github.com/facebook/flux/tree/master/examples/flux-chat/js)
245 | 一个更复杂一点的例子
246 |
--------------------------------------------------------------------------------
/forms.md:
--------------------------------------------------------------------------------
1 | # 表单
2 |
3 | 表单不同于其他 HTML 元素,因为它要响应用户的交互,显示不同的状态,所以在 React
4 | 里面会有点特殊。
5 |
6 | ## 状态属性
7 |
8 | 表单元素有这么几种属于状态的属性:
9 |
10 | - `value`,对应 ` ` 和 `