├── .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 |
31 | 32 | 33 |
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 | 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 | ![flux overview](image/flux-overview.png) 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`,对应 `` 和 `