企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 第 1 章 ## 1.JSX 的 onClick 事件处理方式和 HTML 的 onclick 有很大的不同: 在 HTML 中直接使用 onclick 存在的问题: * onclick 添加的事件处理函数是在全局环境下执行的,这污染了全局环境,很容易产生意料不到的后果 * 给很多 DOM 元素添加 onclick 事件,可能会影响网页的性能,毕竟,网页的事件处理函数越多,性能就会越低 * 对于使用 onclick 的 DOM 元素,如果要动态地从 DOM 树中删掉的话,需要把对应的事件处理器注销,如果忘了注销,就可能造成内存泄漏,这样的 bug 很难被发现 上述问题在 JSX 中都不存在 * onClick 挂载的每个函数,都可以控制在组件范围内,不会污染全局空间 * 我们在 JSX 中看到一个组件使用了 onClick,但并没有产生直接使用 onclick 的HTML,而是使用了事件委托(event delegation)的方式处理点击事件,无论有多少个 onClick 出现,其实最后都只在 DOM 树上添加了一个事件处理函数,挂在最顶层的 DOM 节点上。所有的点击事件都被这个事件处理函数捕获,然后根据具体组件分配给特定函数,使用事件委托的性能当然要比为每个 onClick 都挂载一个事件处理函数要高 * 因为 React 控制了组件的生命周期,在 unmount 的时候自然能够清除相关的所有事件处理函数,内存泄漏也不再是一个问题 ## 2.script 脚本中的 eject(弹射) 命令 执行`npm run eject`就是把潜藏在 react-scripts 中的一系列技术栈配置都 “弹射” 到应用的顶层,然后我们就可以研究这些配置细节,而且可以更灵活地定制应用的配置。 如 config 目录下的 webpack.config.dev.js 文件,定制 npm start 所做的构造过程 ## 3.*UI=render(data)* React 的理念可归结为这一公式。用户看到的界面(UI),应该是一个函数(在这里叫 render)的执行结果,只接受数据(data)作为参数。这个函数是一个纯函数,即没有任何副作用,输出完全依赖于输入的函数,两次函数调用如果输入相同,得到的结果也绝对相同。如此一来,最终的用户界面,在 render 函数确定的情况下完全取决于输入数据  对于开发者来说,重要的是区分开哪些属于 data,哪些属于 render,想要更新用户界面,要做的就是更新 data,用户界面自然会作出响应。所以 React 实践的也是“响应式编程”(Reactive Programming)的思想,React 的名字由此而来。 ## 4.Virutal DOM 的简单描述 DOM 树是对 HTML 的抽象,Virtual DOM 是对 DOM 树的抽象。Virtual DOM 不会触及浏览器的部分,只是存在于 JavaScript 空间的树形结构,每次自上而下渲染 React 组件时,会对比这一次产生的 Virtual DOM 和上一次渲染的 Virtual DOM,对比就会发现差别,然后修改真正的 DOM 树时就只需要触及差别中的部分就行 # 第 2 章 设计高质量的 React 组件 ## 1.组件的划分通则 组件的划分要满足高内聚(High Cohesion)和低耦合(Low Coupling)的原则 **高内聚** 指的是把逻辑紧密相关的内容放在一个组件中:传统上,内容由 HTML 表示,交互行为放在 JavaScript 代码文件中, 样式放在 CSS 文件中定义,这虽然满足一个工程模块的需要,却要放在三个不同的文件中。React 中,展示内容的 JSX、定义行为的 JavaScript、甚至定义样式的 CSS,都可以放在一个 JavaScript 文件中,所以 React 天生具有高内聚的特点 **低耦合** 指不同组件之间的依赖关系要尽量弱化,也就是每个组件要尽量独立。保持整个系统的低耦合度,需要对系统中的功能由充分的认识,然后根据功能点划分模块,让不同的组件去实现不同的功能 ## 2.React 中的数据 React 组件的数据分为两种,porp 和 state,prop 或者 state 改变都可能引发组件的重新渲染,那么设计一个组件的时候,什么时候用 porp,什么时候用 state 呢? prop 是组件的对外接口,state 是组件的内部状态,对外用 prop,内部用 state prop 的类型不限于纯数据,也可以是函数,函数类型的 prop 等于让父组件给了子组件一个回调函数 可以通过回答以下问题来看看你是否对 React 较为熟悉 * 父组件用 prop 传递信息给子组件的形式? * 子组件如何读取 prop 值? * 通过类的 propTypes 属性定义 prop 规格的格式? 关于 propTypes 的使用需要注意一些问题:定义类的 propTypes 属性,无疑是要占用一些代码空间,而且类型检查也是要消耗 CPU 计算资源的。其次,在线上环境做 propTypes 类型检查没有什么帮助,即在开发过程中为了避免犯错我们才使用 propTypes,发布产品代码时,可以用一种自动的方式将 propTypes 去掉。`babel-react-optimize`具有这个功能,可以通过过 npm 安装,但是确保只在发布产品代码的时候使用它。 <br /> state 需要在构造函数中初始化(通过对 state 的赋值),组件的 state 必须是一个 JavaScript 对象。通过`this.state`读取当前组件的 state,通过`this.setState()`更新 state **prop 和 state 的对比** * prop 用于定义外部接口,state 用于记录内部的状态 * prop 的赋值在外部世界使用组件时,state 的赋值在组件内部 * 组件不应该改变 prop 的值,而 state 的存在目的就是让组件来改变的。虽然 React 并没有办法阻止你去修改传入的 props 对象,但是你仍然不该跨越这条红线,否则最后可能出现不可预料的 bug # 第 3 章:从 Flux 到 Redux ## Flux 到 Redux ![](https://box.kancloud.cn/721176e45da9eb9aaf535d3d801db11f_825x473.png) 对于 MVC 框架,为了让数据流可控,Controller 应该是中心,当 View 要传递消息给 Model 时,应该调用 Controller 的方法,同样,当 Model 要更新 View 时,也应该通过 Controller 引发新的渲染 ![](https://box.kancloud.cn/96083da782507dc86984dea459d334b0_828x252.png) 一个 Flux 应用包含四个部分: * Dispatcher:处理动作分发,维持 Store 之间的依赖关系 * Store:负责存储数据和处理数据相关逻辑 * Action:驱动 Dispatcher 的 JavaScript 对象 * View:视图部分,负责显示用户界面 MVC 与 Flux 的对比:在 MVC 框架中,系统能够提供什么样的服务,通过 Controller 暴露函数来实现。每增加一个功能,Controller 往往就需要增加一个函数;在 Flux 的世界里,新增加功能并不需要 Dispatcher 增加新的函数,要做的就是增加一种新的 Action 类型, Dispatcher 的对外接口并不用改变。 当需要扩充应用所能处理的”请求“时,MVC 方法就需要增加新的 Controller,而对于 Flux 则只是增加新的 Action Flux 的优点就在于其 “单向数据流” 的管理方式,这种 “限制” 禁绝了数据流混乱的可能 其不足之处在于: * Store 之间的依赖关系 * 难以进行服务器端渲染 * Store 混杂了逻辑和状态 > 感兴趣的可以搜下使用 Flux 的状态管理方案 ## Redux ![](https://box.kancloud.cn/23200e084e7147c03fd5718d9426a7e2_392x242.png) Flux 的基本原则是“单向数据流”,Redux 在此基础上强调三个基本原则: * 唯一数据源(Single Source of Truth) * 保持状态只读(State is read-only) * 数据改变只能通过纯函数完成(Changes are made with pure functions) 1.唯一数据源:应用的状态数据应该只存储在唯一的一个 Store 上,避免了状态数据分散在多个 Store 中造成的数据冗余 2.保持状态只读:要修改 Store 的状态,必须要通过派发一个 action 对象来完成 3.数据改变只能通过纯函数完成:这里所说的纯函数就是 Reducer,在 Redux 中,每个 reducer 的函数签名如下:`reducer(state, action)` 第一个参数 state 是当前的状态,第二个参数 action 是接收到的 action 对象,而 reducer 函数要做的事情,就是根据 state 和 action 的值产生一个新的对象返回。注意 reducer 必须是纯函数,即函数的返回结果完全由参数 state 和 action 决定,而且不产生任何副作用,也不能修改参数 state 和 action 对象 ## 容器组件与傻瓜组件 在 Redex 框架下,一个 React 组件基本上就是要完成以下两个功能: * 和 Redux Store 打交道,读取 Store 的状态,用于初始化组件的状态,同时还要监听 Store 的状态改变;当 Store 状态发生变化时,需要更新组件状态,从而驱动组件重新渲染;当需要更新 Store 状态时,就要派发 action 对象 * 根据当前 prop 和 state,渲染出用户界面 >[warning] 疑问:现在有一种说法是所有组件的状态全部丢到 Redux 的 Store 中进行管理,那么组件自身的“状态”哪去了?Store 中状态的更新是否必定触发组件的重新渲染?个人的理解是:以 react-redux 的实现为例,我们在组件中是通过 this.props 来访问 Store 中的数据和调用一些 dispatch 的,所以如果 Store 中的状态改变了,以这些状态为 props 的组件就会重新渲染(state 和 prop 的改变都可能触发重新渲染) 如果 React 组件都是要包办上面所说的两个任务,似乎做的事情稍微多了点。所以我们可以考虑拆分为两个组件,分别承担一个任务,然后把两个组件嵌套起来,完成原本一个组件完成的所有任务。 这样的关系里,两个组件是父子组件的关系。负责与 Redux Store 打交道的组件,处于外层,称为容器组件(Container Component);只负责渲染界面的组件,处于内层,叫做展示组件(Presentational Component);外层的容器组件也叫作聪明组件(Smart Componnet),内层的展示组件又叫做傻瓜组件(Dumb Component) ![](https://box.kancloud.cn/96c3306bc62169cd87cb1838bc403e76_685x376.png =350x200) 状态全部交由容器组件打理,展示组件只需要根据 props 来渲染结果,不需要 state;这只是设计 React组件的一种模式,和 Redux 没有直接关系,另外,没有 state 只有一个 render 方法,所有数据都来自于 props 的组件称为 **无状态组件**。 无状态组件可以写成一个函数,不再需要用对象表示 ```js // 解构赋值 function Counter ({caption, onIncrement, onDecrement, value}) { return ( <div> <button style={buttonStyle} onClick={onIncrement}>+</button> <button style={buttonStyle} onClick={onDecrement}>-</button> <span>{caption} count: {value}</span> </div> ); } // 不使用解构赋值 function Counter (props) { const {caption, onIcrement, onDecrement, value} = props // ... } ``` ## 组件 Context 不使用 react-redux 之前,每个组件都需要直接导入 Redux Store `import store from '../Store.js'` 虽然 Redux 应用全局就一个 Store,但是这样的直接导入依然有问题(很麻烦) 为了解决这个问题,那就只能让上层组件把 Store 传递下来,首先想到的是用 props,但是层层传递的方法显然是不可行的。设想在一个嵌套多层的组件结构中,只有最里层的组件才需要使用 store,但是为了把 store 从最外层传递到最里层,就要求中间所有的组件都需要增加对这个 store prop 的支持,即使根本不使用它。 React 提供了一个叫 Context 的功能,能完美地解决这个问题。 所谓 Context,就是“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象。 ![](https://box.kancloud.cn/661c43a8cee1ad4795d06668172ab5ba_575x281.png) > 可以查阅 React 文档了解如何使用 React 提供的 Context API:[https://react.docschina.org/docs/context.html](https://react.docschina.org/docs/context.html) 其实 react-redux 这个库就是基于 Context 实现的。 这里分析下它使用的这条语句: ```js export default connect(mapStateToProps, mapDispatchToProps)(Counter) ``` connect 是 react-redux 提供的一个方法,这个方法接收两个参数,执行结果依然是一个函数,所以才可以在后面又加一个圆括号(柯里化?),把 connect 函数执行的结果立即执行。 这里有两次函数的执行,第一次是 connect 函数的执行,第二次是把 connect 函数返回的函数再次执行,最后产生的就是容器组件。 connect 函数具体做了哪些事呢? * 把 Store 上的状态转化为内层傻瓜组件的 prop * 把内层傻瓜组件中的用户动作转化为派送给 Store 的动作 这两个工作一个是内层傻瓜对象的输入,一个是内层傻瓜对象的输出 mapStateToProps(命名是业界习惯)就是把 Store 上的状态转化为内层组件的 props,建立映射关系 mapDispatchToProps 把内层傻瓜组件暴露出来的函数类型的 prop 关联上 dispatch 函数的调用 ``` // 第二个参数是直接传递给外层容器组件的 props function mapDispatchToProps(dispatch, ownProps) { return { onIncrement: () => { dispatch(Actions.increment(ownProps.caption)); }, onDecrement: () => { dispatch(Actions.decrement(ownProps.caption)); } } } ``` # 第四章 模块化 React 和 Redux 应用 ## 代码文件的组织方式 1.按角色组织 ```js reducers/ todoReducer.js filterReducer.js actions/ todoActions.js filterAction.js components/ todoList.js todoItem.js filter.js containers/ todoListContainer.js todoItemContainer.js filterContainer.js ``` - reducer 目录包含所有 Redux 的 reducer - actions 目录包含所有 action 构造函数 - components 目录包含所有的傻瓜组件 - containers 目录包含所有的容器组件 2.按功能组织 ```js todoList/ action.js actionType.js index.js reducer.js views/ component.js container.js filter/ action.js actionType.js index.js reducer.js views/ component.js container.js ``` - actionType.js 定义 action 类型 - action.js 定义 action 构造函数,决定了这个功能模块可以接受的动作 - reducer.js 定义这个功能模块如何响应 action.js 中定义的动作 - views 目录包含这个功能模块中所有的 React 组件,包括傻瓜组件和容器组件 - index.js 把所有的角色导入,然后统一导出 >[warning]个人感觉不太能够接受这两种组织文件的方式...... # 第 5 章 React 组件的性能优化 ## 引入性能检测工具 React Pref `npm install react-addons-perf -D`:开发环境下性能检测(哪些组件造成了无意义的渲染) `npm install redux-immutable-state-invariant -D`:开发环境下检测 reducer 是否为纯函数,不是则报错 Store.js 文件 ```js import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; import {reducer as todoReducer} from './todos'; import {reducer as filterReducer} from './filter'; // 下面三行代码! import Perf from 'react-addons-perf' const win = window; win.Perf = Perf const reducer = combineReducers({ todos: todoReducer, filter: filterReducer }); const middlewares = []; // 考虑到将来的扩展,使用数组变量 middlewares 来存储所有的中间件,之后的中间件直接 push if (process.env.NODE_ENV !== 'production') { // 使用 require 是因为 import 语句不能存在于条件语句中 middlewares.push(require('redux-immutable-state-invariant')()); // react-immutable-state-invariant 中间件只在开发环境下有意义,用于检查 reducer 是否为纯函数 } // Redux 提供 compose 函数把多个 Store Enhancer 组合在一起 const storeEnhancers = compose( applyMiddleware(...middlewares), (win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f, // Redux Devtools 开发者工具 ); export default createStore(reducer, {}, storeEnhancers); // Store Enhancers 能够让 createStore 函数产生的 Store 对象具有更多的功能 ``` ## 单个组件的性能优化 更改 shouldComponentUpdate 函数的默认实现,根据每个 React 组件的内在逻辑定制其行为 ``` shouldComponentUpdate(nextProps, nextState) { // 假设影响渲染内容的 prop 只有 completed 和 text,只需要确保 // 这两个 prop 没有变化,函数就可以返回 false return (nextProps.completed !== this.props.completed) || (nextProps.text !== this.props.text) } ``` 使用 immutable.js 解决复杂数据 diff、clone 等问题。 immutable.js 实现原理:持久化数据结构,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。 ## 多个组件的性能优化(这部分主要介绍了虚拟 DOM 的原理) 用户操作引发界面的更新并不会让 React 生成的虚拟 DOM 推倒重来,React 在更新阶段巧妙地对比原有的 Virtual DOM 和新生成的 Virtual DOM,找出两者的不同之处,根据不同来修改 DOM 树,这样就只需要做最小的必要改动。这个“找不同”的过程,就叫做 Reconciliation(调和)。 React 对比两个 Virtual DOM 的树形结构时,从根节点开始递归往下比对,在树形结构上,每个节点都可以看作这个节点以下部分子树的根节点。所以这个比对算法可以从 Virtual DOM上任何一个节点开始执行。 React 首先检查两个树形结构的根节点的类型是否相同,根据相同或者不同有不同处理方式 **1.节点类型不同的情况** 直接扔掉原来的,构建新的 DOM 树,原有的树形结构上的 React 组件会经历“卸载”的生命周期,而取而代之的组件会经历“装载”的生命周期 ```html <div> <Todos /> </div> // 我们想要更新成这样 <span> <Todos /> </span> ``` 比如上面的代码在比较时,根节点类型不一样,一切推倒重来,重新构建一个 span 节点及其子节点 作为开发者,需要避免这种浪费的情景出现(div 和 span 的子节点是一样的) **2.节点类型相同的情况** 如果两个树形结构的根节点类型相同,React 就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载 这里有必要区分一下节点的类型:一类是 DOM 元素类型,一类是 React 组件;对于 DOM 元素类型, React 会保留节点对应的 DOM 元素,只对树形结构根节点上的属性和内容做一下比对,然后只更新修改的部分 ```html <div style={{color: 'red', fontSize: 15}} className="welcome"> Hello World </div> // 改变之后的 JSX,React 可以对比发现内容和属性的变化,只修改这些变化的部分 <div style={{color: 'green', fontSize: 15}} className="farewell"> Hello World </div> ``` 如果属性结构的根节点是 React 组件类型,React 能做的就是根据新节点的 props 取更新原来根节点的组件实例,即按顺序触发下列函数(旧生命周期) - shouldComponentUpdate - componentWillReceiveProps - componentWillUpdate - render - componentDidUpdate 处理完根节点的对比之后,会对根节点的每个子节点重复一样的动作。 **3.多个子组件的情况** 当一个组件包含多个子组件的情况 ```html <ul> <TodoItem text="First" completed={false} /> <TodoItem text="Second" completed={false} /> </ul> // 更新为 <ul> <TodoItem text="Zero" completed={false} /> <TodoItem text="First" completed={false} /> <TodoItem text="Second" completed={false} /> </ul> ``` 直观上看,只需要创建一个新组件,更新之前的两个组件;但是实际情况并不是这样的,React 并没有找出两个序列的精确差别,而是直接挨个比较每个子组件。 在上面的新的 TodoItem 实例插入在第一位的例子中,React 会首先认为把 text 为 First 的 TodoItem 组件实例的 text 改成了 Zero,text 为 Second 的 TodoItem 组件实例的 text 改成了 First,在最后面多出了一个 TodoItem 组件实例。这样的操作的后果就是,现存的两个实例的 text 属性被改变了,强迫它们完成了一个更新过程,创造出来的新的 TodoItem 实例用来显示 Second。 我们可以看到,理想情况下只需要增加一个 TodoItem 组件,但实际上其还强制引发了其他组件实例的更新。 假设有 100 个组件实例,那么就会引发 100 次更新,这明显是一个浪费;所以就需要开发人员在写代码的时候提供一点小小的帮助,这就是接下来要讲的 key 的作用 > 如果 React 采用的是先找出两个序列的差异的算法,时间是 O(N^2),这不适合一个对性能要求很高的场景 ## key 的作用 ```html <ul> <TodoItem key={1} text="First" completed={false} /> <TodoItem key={2} text="Second" completed={false} /> </ul> // 新增一个 TodoItem 实例 <ul> <TodoItem key={0} text="Zero" completed={false} /> <TodoItem key={1} text="First" completed={false} /> <TodoItem key={2} text="Second" completed={false} /> </ul> ``` React 根据 key 值,就可以知道现在的第二个和第三个 TodoItem 实例其实就是之前的第一个和第二个实例,所以 React 就会把新创建的 TodoItem 实例插在第一位,对于原有的两个 TodoItem 实例只用原有的 props 来启动更新过程,这样 shouldComponentUpdate 就会发生作用,避免无谓的更新操作; 了解了这些之后,我们就知道 key 值应该是 **唯一** 且 **稳定不变的** 比如用数组下标值作为 key 就是一个典型的错误,看起来 key 值是唯一的,但是却不是稳定不变的 比如:[a, b, c] 值与下标的对应关系:a: 0 b:1 c:2 删除a -> [b, c] 值与下标的对应关系 b:0 c:1 无法用 key 值来确定比对关系(新的 b 应该与旧的 b 比,如果按 key 值则是与 a 比) ![](https://box.kancloud.cn/843f670bb548fca2492bf347a2d0c3f6_501x135.png) > 需要注意,虽然 key 是一个 prop,但是接受 key 的组件并不能读取到 key 的值,因为 key 和 ref 是 React 保留的两个特殊 prop,并没有预期让组件直接访问 ## 利用 reselect 提高数据选取的性能 跟 vuex 的 getter 原理类似,缓存数据? # 第 6 章 React 高级组件 ## 高阶组件的概念及应用 高阶组件(Higher Order Component,HOC)并不是 React 提供的某种 API,而是使用 React 的一种模式,用于增强现有组件的功能。 简单来说,一个高阶组件就是一个函数,这个函数接受一个组件作为输入,然后返回一个新的组件作为结果,而且,返回的新组件拥有了输入组件所不具有的功能。这里提到的组件指的并不是组件实例,而是一个组件类,也可以是一个无状态组件的函数。 ```js import React from 'react' function removeUserProp(WrappedComponent) { return class WrappingComponent extends React.Component { render() { const {user, ...otherProps} = this.props return <WrappedComponent {...otherProps} /> } } } export default removeUserProp ``` 这样一个高阶组件做的工作非常简单,它接受一个名为 WrappedComponent 的参数,表示一个组件类,这个函数返回一个新的组件,所做的事情和 WrappedComponent 一模一样,只是忽略名为 user 的 prop。假如我们不希望某个组件接收到 user 的 prop,那么我们就不要直接使用这个组件,而是把这个组件作为参数传递给 removeUserProp 函数,然后把这个函数的返回结果当作组件来使用 ```js const NewComponent = removeUserProp(SampleComponent) ``` 定义高阶组件的意义何在? - 重用代码 - 修改现有 React 组件的行为,假设我们不想修改原有组件的内部逻辑,那么可以考虑使用高阶组件 ## 以函数为子组件的模式 # Redxu 和服务器通信 ## 利用代理服务快速解决跨域请求问题 代理服务器的作用大概如下图 ![](https://box.kancloud.cn/2c3151c5fe2669ff242614c04de86ef0_672x532.png) 使用 create-react-app 创造的应用已经具备了代理功能,只需要在 package.json 中添加如下一行 `"proxy": "http://www.weather.com.cn/"` 这一行配置告诉我们的应用,当接收到不是要求本地资源(localhost)的 HTTP 请求时,这个 HTTP 请求的协议和域名部分"替换"为 `http://www.weather.com.cn` 转发出去(代理服服务器会根据配置分析 入 和 出 的关系,重新构建新的请求),并将收到的结果返还给浏览器,但是注意在线上环境应该开发自己的代理服务器(如 Nginx 的代理配置) ## 如何关联异步的网络请求和同步的 React 组件渲染 可行的方法是这样的 - 在装载过程中,如果组件没有获得服务器结果,就不显示结果或者显示一个“正在加载”之类的提示信息(在 componentDidMount 函数中发送请求) - 获取了请求结果后,要引发组件的一次更新过程,让该组件重新绘制自己的内容 就像下面这样,即我们把请求返回的数据直接保存在 React 组件的 state 中 ```js import React from 'react'; //TODO: change to your city code according to http://www.weather.com.cn/ const cityCode = 101010100; class Weather extends React.Component { constructor() { super(...arguments); this.state = {weather: null}; } componentDidMount() { const apiUrl = `/data/cityinfo/${cityCode}.html`; fetch(apiUrl).then((response) => { if (response.status !== 200) { // 需要检查状态码,因为 fetch 认为只要服务器返回一个合法的 HTTP 响应就算成功 throw new Error('Fail to get response with status ' + response.status); } // response.json 检查返回的数据是否为 JSON 格式同时帮我们执行 JSON.parse(response.text) ? response.json().then((responseJson) => { this.setState({weather: responseJson.weatherinfo}); }).catch((error) => { this.setState({weather: null}); }); }).catch((error) => { this.setState({weather: null}); }); } render() { if (!this.state.weather) { return <div>暂无数据</div>; } const {city, weather, temp1, temp2} = this.state.weather; return ( <div> {city} {weather} 最低气温 {temp1} 最高气温 {temp2} </div> ) } } export default Weather; ``` ## 使用 redux-thunk 中间件 把状态存放在组件中并不是一个很好的选择, Redux 本身就是用来帮助管理应用状态的,应该尽量把状态存放在 Redux Store 中。 Redux 本身的设计理念是不允许异步操作的,所以就需要中间件如 redux-thunk、redux-saga 在 Redux 架构下,一个 action 对象在通过 store.dispatch 派发,在调用 reducer 函数之前,就会先经过一个中间件的环节,这就是产生异步操作的机会 ![](https://box.kancloud.cn/74a868650bc5c1dbd21501a90f9f371c_959x402.png) redux-thunk 的工作是检查 action 对象是不是函数,如果不是函数就放行,完成普通 action 对象的生命周期,而如果发现 action 对象是函数,那就执行这个函数,并把 Store 的 dispatch 函数和 getState 函数作为参数传递到函数中去,不会让这个异步 action 对象继续往前派发到 reducer 函数 举一个简单的例子来介绍异步 action: ```js const increment = () => ({ type: ActionTypes.INCREMENT }) const incrementAsync = () => { return dispatch => { setTimeout(() => { dispatch(increment()) }, 1000) } } ``` 这个函数被 dispatch 函数派发之后,会被 redux-thunk 中间件执行,于是 setTimeout 函数就会发生作用,在 1s 后利用参数 dispatch 函数派发出同步 action 构造函数 increment 的结果。应用到发送 AJAX 请求中就是:action 对象函数可以通过 fetch 发起一个对服务器的异步请求,当得到服务器结果之后,通过参数 dispatch 把成功或者失败的结果当作 action 对象再派发到 reducer 上(这次发送的是普通的 action 对象),最终驱动 Store 上状态的改变。 虽然大致了解了原理,但使用时还要注意设计异步操作的模式,如设计 action: 一个访问服务器的 action,至少要设计到三个 action 类型: - 表示异步操作已经开始的 action 类型 - 表示异步操作成功的 action 类型 - 表示异步操作失败的 action 类型 当这三种类型的 action 对象被派发时,会让 React 组件进入各自不同的三种状态:(组件根据状态来渲染不同的视图) - 异步操作正在进行中 - 异步操作已经成功完成 - 异步操作已经失败 ```js import {FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE} from './actionTypes.js'; // 返回 type 字段以驱动 reducer 函数去改变 Redux Store 上的某个字段的状态,从而驱动对应的 React 组件重新渲染 export const fetchWeatherStarted = () => ({ type: FETCH_STARTED }); export const fetchWeatherSuccess = (result) => ({ type: FETCH_SUCCESS, result }) export const fetchWeatherFailure = (error) => ({ type: FETCH_FAILURE, error }) export const fetchWeather = (cityCode) => { return (dispatch) => { const apiUrl = `/data/cityinfo/${cityCode}.html`; dispatch(fetchWeatherStarted()) // 派发一个普通 action 对象,将视图置于"有异步 action 还未结束"的状态 return fetch(apiUrl).then((response) => { if (response.status !== 200) { throw new Error('Fail to get response with status ' + response.status); } response.json().then((responseJson) => { dispatch(fetchWeatherSuccess(responseJson.weatherinfo)); // 派发一个表示请求成功的普通 action 对象 }).catch((error) => { dispatch(fetchWeatherFailure(error)); // 派发一个表示请求失败的普通 action 对象 }); }).catch((error) => { dispatch(fetchWeatherFailure(error)); }) }; } ``` 再来看下 reducer 函数如何处理 ``` import {FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE} from './actionTypes.js'; import * as Status from './status.js'; /* ./status.js export const LOADING = 'loading'; export const SUCCESS = 'success'; export const FAILURE = 'failure'; */ export default (state = {status: Status.LOADING}, action) => { switch(action.type) { case FETCH_STARTED: { return {status: Status.LOADING}; } case FETCH_SUCCESS: { return {...state, status: Status.SUCCESS, ...action.result}; } case FETCH_FAILURE: { return {status: Status.FAILURE}; } default: { return state; } } } ``` 异步 action 构造函数的模板: ``` export const sampleAsyncAction = () => { return (dispatch, getState) => { // 在这个函数里可以调用异步函数,自行决定在合适的时机通过 // 参数派发出新的 action 对象 } } ``` # Tips 1.在使用 JSX 的代码文件中,即使代码中并没有直接使用 React,也一定要导入 React,因为 JSX 最终会被转译成依赖于 React 的表达式`React.createElement()` ***** 2.尽量避免使用 ref,ref 可以取得对元素的引用来访问 DOM 元素,React 的产生就是为了避免直接操作 DOM 元素,因为直接访问 DOM 元素很容易产生失控的情况。可以通过 **状态绑定** 的方式来实现相应的功能,简单来说,就是利用组件的状态来存储我们需要的内容。 ```js <input onChange={this.onInputChange} /> onInputChange(event) { this.setState({ value: event.target.value // 把内容存在组件状态的 value 字段上 }) } ```