使用 Props 和 State 定义组件
如何定义?
在强调组件化的 React 中, 我们需要以高内聚, 低耦合的原则设计高可复用性的组件. 因此渲染组件的数据由两部分组成, 一个是由父组件传入的 Props 参数, 一个是组件的内部状态 State.Props 参数可以是任何的 Javascript 对象, 作为组件本身可以通过使用 propTypes 限制必须输入的参数和输入参数的类型以保证组件的可用性. State 负责维护组件内部的状态, 组件内部必要时可以通过触发父组件传递的回调函数传递信息给父组件或者将 State 以 Props 的形式传递给子组件.
存在的问题
1, 数据重复以及数据不一致的问题
不同的组件之间在数据上如果存在依赖关系, 则在不同的组件中可能都各自维护着相同的数据或者一个组件的数据可以根据另一个组件的数据计算得到, 这就存在了数据重复的问题. 数据出现了重复的情况就有了一致性的问题, 特别在一个组件的数据由其他多个组件的多个数据决定的情况下一致性的问题就需要越来越复杂的函数来保证.
2, 数据传递问题
在一个应用中如果包含三级或者三级以上的组件结构, 顶层的祖父级组件想要传递一个数据给最底层的子组件, 用 prop 的方式就只能通过父组件的中转. 这样, 即使父组件不需要该 prop 也被迫成为一个搬运工的角色这样与我们创建高可用性的组件原则相违背.(虽然可以使用 React 的上下文 Context 解决这个问题, 但是 Context 的使用有可能使组件间的关系变得复杂且代码维护性差, 在官方文档中并不推荐使用)
Flux
以上描述的 React 原生数据流存在的问题会使我们使用 React 开发应用时将视图, 数据和业务逻辑混在一起, 当应用足够庞大的时候代码的可阅读性和可维护性就变得很低. 因此, Facebook 在发布 React 的时候也同时推出了 Flux 框架; Flux 的核心思想是 "单向数据流", 在理解 Flux 的基础上我们可以更容易地理解 Redux.
Flux 的出现
Flux 框架的出现源于 Facebook 对现有的传统 MVC 框架不满, 在 MVC 框架中当 Model 数据层和 View 视图层可以直接相互调用的时候而不是通过控制器 Controller 通讯时就会出现多个 Model 对应多个 View 的多对多混乱的情况, 例如下图:
Flux 框架的工作模式
一个 Flux 应用包含以下的四个部分:
Dispatcher: 将用户或者视图的动作 Action 派发给所有注册到 Dispatcher 上的状态管理 Store 的回调函数
Store: 负责存储数据和处理数据相关逻辑
Action: 驱动 Dispatch 的 Javascript 对象
View: 视图部分, 在这里指的就是纯 React 的部分
使用 Flux 的流程:
1, 创建 Dispatcher
- import {Dispatcher} from 'flux';
- export default new Dispatcher();
安装好 Flux 后导入 Dispatcher 模块, new 一个 Dispatche 对象就可以了. 它的的主要作用就是用来派发 Action 到 Store 注册的回调函数里的.
2, 创建 Action
创建 Action 分为以下两个步骤
步骤一: 在 ActionType.js 中定义动作的类型
export const ActionTypeName= '字符串';
动作的定义是一个常量字符串类型, 仅用于表示当前动作的类型.
步骤二: 在 Action.js 中定义动作的构造函数
- export const actionConstructor= (参数) => {
- AppDispatcher.dispatch({
type: ActionTypes. 动作类型,
参数名: 参数
});
在 Action.js 中定义可以产生并派发 action 对象的函数
3, 创建 Store
Store 存储应用的状态, 同时还要接受 Dispatcher 派发的动作并根据动作来决定如何更新应用的状态
- const Store = Object.assign({}, EventEmitter.prototype, {
- getValue: function() {
- // 这是获取 store 存储的状态的方法
- }
- emitChange: function() {
- this.emit(CHANGE_EVENT);
- },
- addChangeListener: function(callback) {
- this.on(CHANGE_EVENT, callback);
- },
- removeChangeListener: function(callback) {
- this.removeListener(CHANGE_EVENT, callback);
- }
- });
在 Store 中, Store 扩展了 EventEmitter.prototype 成为一个 EventEmitter 对象. 可以将 Store 绑定在特定的 View 上并在状态改变的时候通过 this.emit 触发绑定了该 Store 的 View 上的回调函数.
- Store.dispatchToken = Dispatcher.register((action) => {
- if (action.type === ActionTypes.ActionTypesA) {
- Store.emitChange();
- } else if (action.type === ActionTypes.ActionTypesB) {
- Store.emitChange();
- }
- });
将 Store 注册到 Dispatcher 上, 当 Dispatcher 接收到一个动作的时候会将该动作派发到所有注册到 Dispatcher 上的回调函数, 回调函数去判断对应的动作类型做对应的操作. 我们无法预测 Dispatcher 派发到不同 Store 的不同回调函数的顺序, 所以不同的回调函数之间如果存在依赖关系可以使用 Dispatcher.waitFor(某个回调函数的返回值 dispatchToken)处理回调函数的先后处理顺序.
4, 修改 View
- constructor(props) {
- super(props);
- this.state = {
- stateName: Store.getValue( )
- }
- }
在组件创建时的构造函数中, 将组件的 State 设置为 Store 中存储的状态
- componentDidMount() {
- Store.addChangeListener(this.onChange);
- }
- componentWillUnmount() {
- Store.removeChangeListener(this.onChange);
- }
- onChange() {
- // 当 Store 中状态改变的时候触发组件改变
- }
在组件被挂载时 (生命周期的 componentDidMount 函数中) 为组件添加监听器和在组件被销毁之前 (生命周期的 componentWillunmount 函数中) 移除监听器. 当 Store 中的状态改变的时候, 将会触发添加在监听器上的回调函数 this.onChange(), 一般我们在该回调函数中调用 this.state 方法修改组件的内部状态触发组件的重新渲染. 对于视图 View 来说, 要想修改 Store 的状态则需要调用 Action.js 中的动作构造函数, 动作函数根据参数创建 Action 对象并将其派发.
总结
通过创建 Action,Store,Dispatcher 以及 View 我们就实现这种 Flux"单向数据流" 的状态数据管理方式, 杜绝了像 MVC 框架中 View 和 Model 直接通讯的情况. 在 Flux 框架下, 用户的操作等行为调用由 Action.js 维护的动作构造函数, 构造函数根据 ActionType.js 描叙的动作类型创建对应的 Action 并使用全局唯一的 Dispatcher 将其派发给所有已经在 Dispatcher 上注册的 Store 的回调函数, Store 根据对应的动作类型修改状态值. 而由于组件在初始化的时候已经添加了 Store 的监听函数, 组件的 State 已经成为了 Store 中某个状态的映射; 当 Store 改变的时候将出发组件 State 的修改进而触发组件的重新渲染. 新渲染的组件又能响应各种动作触发不同的动作构造函数完成新一轮的交互, 这样我们就使用了 "单向数据流" 的形式将视图与数据的业务逻辑隔离开了.
存在的问题
1,Flux 中的 Store 之间存在依赖关系
Flux 中允许多 Store, 多个 Store 各自维护着渲染一部分组件的状态数据. 但无法避免的多个 Store 之间可能会存在或多或少的依赖关系, 某一个 Store 的状态数据需要根据另一个 Store 先更新后再计算得到. 虽然 Flux 中提供了 waitFor 函数可以等待另一个 Store 注册在 Dispatcher 上的回调函数执行完成, 但当依赖关系复杂的时候就很容易出错了.
2,Flux 中的 Store 混杂了逻辑和状态
Store 的定义类似于面向对象思想中对象的定义, 包含了状态数据和状态数据改变的业务逻辑. 在上面数据单向流动的过程中, 我们仅仅只是修改了 Store 中的状态数据; 如果修改了 Store 的业务逻辑, 则需要销毁当前的 Store 对象并重新创建新逻辑的 Store, 这种方法将无法保存当前 Store 最新的状态数据, 无法实现像热加载这类型的功能.
Redux
如果把 Flux 看作是 web 应用中状态数据管理的一个框架理念的话, 则 Redux 是 Flux 的一个具体的实现. 其中, Redux 名字的由来就是 Reducer+Flux 的组合.
与 Flux 的区别
在 Redux 中, Redux 用一个单独的 Store 对象保存这一整个应用的状态, 这个对象不能直接被改变. 当一些数据变化了, 通过 Action 和 Reducer 一个新的对象就会被创建.
Redux 基本原则
1, 唯一数据源
在 Redux 应用中只维护了唯一一个数据源 Store, 所有组件的数据源都是这个 Store 上的状态. 避免了在 Flux 应用中多个 Store 之间互相依赖的问题并消除了数据冗余的问题.
2, 保持状态只读
在 Redux 中, 如果想要修改组件状态达到驱动用户界面重新渲染的目的不是通过 this.setState 去修改组件的 State 状态而是创建一个新的状态对象返回给 Redux, 由 Redux 来完成新状态的渲染.
3, 数据改变只能通过纯函数来完成
这里所说的纯函数值得就是 Reducer, 规约函数 Reducer 接收两个参数分别是当前的状态和接收到的动作 action 对象. Reducer 根据当前的 State 和动作类型 Action 产生一个新的 State 对象返回. 需要注意的是在这里, Reducer 是一个纯函数; 也就是说 Reducer 的输出完全是由当前 State 和接收到的动作 action 决定, 并且只是返回一个新的对象而没有产生类似修改 State 的副作用.
Redux 基本使用
在 Redux 中仅仅维护了一个状态管理 Store, 不需要像 Flux 中一样单独有一个 Dispatcher 对象来派发动作 action 给所有 Store 绑定的回调函数; 在 Redux 中 Dispatcher 对象简化成 Store 的一个 dispatch 方法, 用于将动作 action 派发给唯一的 Store. 将 Flux 中 Store 的状态存储和计算状态功能分离开, Store 专门做数据存储而 Reducer 专门做状态计算.
1, 创建 Action
- export const Action= (参数) => {
- return {
type: ActionTypes. 动作类型,
参数名: 参数
};
};
ActionType 的定义和 Flux 没有区别, 都是用字符串表示一个特定动作的类型. 而在 Action.js 中, 定义的不再是构造一个 Action 动作并将其派发出去了而是简单地构造一个动作对象并返回.
2, 创建 Store
- import {createStore} from 'redux';
- import reducer from './Reducer.js';
- const init = {
初始参数名: 参数值
- };
- const store = createStore(reducer, init);
- export default store;
使用 Redux 的 createStore 方法创建全局唯一的 Store 对象, 可以带三个参数按顺序分别用于规约的 Reducer, 初始值和 Store enhancer 增强器.
3, 创建 Reducer
- import * as ActionTypes from './ActionTypes.js';
- export default (state, action) => {
const {参数名} = action;
- switch (action.type) {
- case ActionTypes.ActionTypesA:
return {...state, [参数名]: OPR(state[参数名]) };
case ActionTypes.ActionTypesB:
return {...state, [参数名]: OPR(state[参数名])};
- default:
- return state
- }
- }
Redux 中的 Reducer 类似于 Flux 中的回调函数, 不同的是在 Reducer 中多了一个传入参数 State 表示当前状态, Reducer 返回一个更新后的 State 状态对象. 在 Reducer 中严格杜绝直接修改传入参数 State 的行为, Reducer 应该是一个纯函数不产生任何副作用.
4, 修改 View
- class component extends Component {
- constructor(props){
- super(props);
- ....
- this.state = this.getState( );
- }
- getState( ){
- return {
value: store.getState()[参数名]
}
}
响应用户交互的函数( ){
- store.dispatch(Actions.[Action.js 中动作的构造函数](参数));
- }
- componentDidMount() {
- store.subscribe(回调函数);
- }
- componentWillUnmount() {
- store.unsubscribe(回调函数);
- }
- }
每次 Store 更新时都会触发 View 获取最新的状态值, 因此我们将获取 Store 中最新的状态信息抽出一个单独的函数 getState 处理. 使用 Store 的 subscribe 和 unsubscribe 方法在组件挂载和取消挂载时绑定和解绑回调函数, 回调函数将会重新获取 Store 中最新的状态值并且使用 this.setState 修改组件内部的状态值触发组件渲染. 当 View 需要改变 Store 值的时候, 使用 Store 的 dispatch 方法派发一个特定 ActionType 的动作 Action.
总结
使用 Redux 对应用中的状态进行管理, 首先使用 Redux 中 Store 提供的 subscribe 和 unsubscribe 方法在组件的生命周期内监听 Store 的更新并及时将 Store 中的最新状态通过 this.setState 方法渲染在组件中. View 要修改 Store 中的状态, 则使用 Store 的 Dispatch 方法派发一个 Action.js 中定义的动作构造函数. Store 由 Redux 来维护, Redux 负责存储数据最新的状态并将当前状态和动作传递给 Reducer 进行状态计算, 计算后返回更新后的状态又交由 Store 来存储. Store 的更新将触发 View 的回调函数重新渲染组件. 这样就实现了使用 "单向数据流" 并将存储状态数据和状态计算分离达到提供可预测化状态管理的目的.
来源: https://www.qcloud.com/developer/article/1156976