- 《深入浅出React & Redux》
- Redux 中文文档
《深入浅出React & Redux》第3章 从Flux到Redux
作者认为要了解Redux,首先要从Flux开始了解,这样我们可以知道Flux一族框架贯彻的最重要的观点--单线数据流
,并且通过发现Flux的缺点,从而更加深刻的认识Redux相对于Flux的改进之处。
Flux
Flux简短历史介绍
Flux是和React同时面世的,React和Flux相辅相成,Facebook认为两者结合才能构建大型JavaScript应用。
我们可以这样理解,React是用来替换jQuery的,那么Flux就是替换Backbone.js、Ember.js等MVC一族架构的。但是Flux并不是一个MVC框架,事实上,Flux认为MVC框架存在很大的问题,所以Flux推翻了MVC框架,并用一个全新的思维来管理数据流转
。
MVC 框架的缺陷
Facebook描述的MVC框架
Model和View之间缠绕着太多的复杂依赖关系,并且相互调用,非常乱。
MVC框架提出的数据流很理想,用户请求先到达controller,由controller调用Model获取数据,然后把数据交给View进行渲染。但是在实际框架实现中,总是允许View 和 Model可以直接通信,就出现了Facebook描述的MVC框架的情况发生。
Flux单项数据流
Facebook推出Flux时,有很多质疑。很多人认为Flux只不过是一个对数据流管理更加严格的MVC框架而已。这种说法不完全正确,但是一定意义上也说明了Flux的一个特点:更严格的数据流控制。
Redux的原则
Flux的基本原则是“单向数据流”,Redux在此基础上强调了三个基本原则:
- 单一数据源 (Single Source of Truth)
- State是只读的 (State is read-only)
- 使⽤纯函数来执⾏修改数据(Changes are made with pure functions)
这里说的纯函数就是Reducer,Redux 这个名字的前三个字母Red代表的就是Reducer。按照Redux创作者Dan Abramov的说法,Redux = Reducer + Flux
Reducer 不是一个Redux特定的术语,而是计算机科学中的一个通用概念,很多语言和匡佳都有对Reducer函数的支持。如JavaScript,数组类型就有reduce函数,接受的参数就是一个reducer,reduce做的事情就是把数组所有元素依次做“规约”,对每个元素都调用一次参数reducer,通过reducer函数完成规约所有元素的功能。
示例讲解
在书中,作者按照如下顺序改写demo的code,讲解每一步的改进之处。
Flux -->
纯React -->
纯React + 拆分容器组件+展示组件 -->
纯React + 拆分容器组件+展示组价+自己写的Provider(context概念) -->
react-redux库的使用
纯React + 拆分容器组件+展示组件
这里拆分组件主要是用了容器组件和展示组件的概念,在容器组件中获取store,将store中拿到的data通过props传递给展示组件,类似于react-redux中的connect函数的作用。
因为react-redux并不需要将组件拆分为容器组件和展示组件。
组件Context
在使用react-redux的时候,只是照本宣科,不理解。在本书中,通过作者描述使用纯React+手写provider了解了provider的作用,并为什么会被作为一个线程的组件放到react-redux库中。
Redux的三大原则中第一条:单一数据源,也就是说Redux应用全局就一个store,如果我们在每个文件使用store的时候都直接使用import导入,依然会有问题。
因为当开发一个独立的组件的时候,都不知道这个组件会存在哪个应用中,当然也不可能预先知道定义唯一Redux Store的文件位置进行导入,所以在组件中直接导入store是非常不利于组件复用。
一个应用最好只有一个地方需要直接导入store,这个位置当然应该是在调用最顶层React组件的位置。在项目中,一般来说就是index.js入口文件。其余组件避免直接导入store。
面对这种问题,两种解决方法:
解决方法1. 让组件的上层组件把store传递下来。也就是用props。这个缺陷就是,从上到下,所有的组件都需要帮忙传递store这个props,即使有的组件根本使用不到。
解决方法2.React中提供了一个叫做Context的功能,使用这个功能能够完美的解决这个问题。
Context,“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象,为了完成这个任务,需要上级组件和下级组件配合。
首先,上级组件要宣传自己支持context,并且提供一个函数来返回代表context的对象。然后,这个上级组件之下的所有子孙组件,只要宣称自己需要这个context,就可以通过this.context 访问到这个共同的环境对象。
provider的由来
因为React只有一个store,因此所有组件需要使用store的话,只能访问这个唯一store,我们就希望顶层的组件来扮演这个context提供者的角色,只要顶层组件提供包含store的context,那么就覆盖了整个应用的组件,简单而且够用。
但是每个应用的顶层组件不同,所以我们需要创建一个特殊的React组件,它将是一个通用的context提供者,可以应用于任何一个应用中,当做React应用所有组件的最外层的父组件,我们把这个组件叫做Provider
。
Provider的原理
Provider 也是一个React组件,不过它的render函数就是简单的把子组件渲染出来(return this.props.children;)
,在渲染上,Provider不做任何附加的事情。
每个React组件的props中都有一个特殊属性:children
,代表的是子组件,所以this.props.children代表是在Provider之间的子组件。
除了把渲染工作全交给子组件,Provider还需要提供一个函数getChildContext,这个函数返回的就是代表context对象。下列代码中context中只有一个字段scope,而且我们也希望Provider足够通用,所以并不在这个文件中导入store,而是要求Provider的使用者通过props传递进来store。
为了让Provider能够被React认可为一个Context提供者,还需要制定Provider的childContextTypes属性,必须和getChildContext函数返回值对应,只有这两者都齐了,Provider的子组件才有可能访问到Context
import {PropTypes, Component} from 'react';
class Provider extends Component {
//函数getChildContext,这个函数返回的就是代表context对象,这个context只包含store这个字段
getChildContext() {
return {
store: this.props.store
};
}
//简单的把子组件渲染出来,在渲染上不做任何处理
render() {
return this.props.children;
}
}
//将store做为Provider的props,由使用者提供该props的值
Provider.propTypes = {
store: PropTypes.object.isRequired
}
//为了让Provider能够被React认可为一个Context提供者,还需要制定Provider的childContextTypes属性
Provider.childContextTypes = {
store: PropTypes.object
};
export default Provider;
使用手写的Provider如何使用store。
//因为我们自己定义了构造函数,所以要带上context参数
constructor(props, context) {
super(props, context);
}
//在函数中使用就是
getOwnState() {
return {
value: this.context.store.getState()[this.props.caption]
};
}
ComponentName.contextTypes = {
store: PropTypes.object
}
Context功能相当于提供了一个全局可以访问的对象,但是全局对象或者说全局变量是我们应该避免的用法。所以,单纯看React的context功能,必须强调这个功能要谨慎使用,只有对那些每个组件都可能使用,但是中间组件由可能不适用的对象才有必要使用Context,千万不要滥用。(
这点有疑问
)
对于Redux,因为Redux的Store封装很好,没有提供直接修改状态的功能,就是说一个组件虽然能够访问全局唯一的Store,却不可能直接修改Store中的状态,这样部分克服了作为全局对象的缺点,并不算滥用,所以,使用Context来传递Store是一个不错的选择。
React-Redux部分
React-Redux库已经实现了Provider,我们可以直接导入使用。
import { Provider } from 'react-redux';
react-redux两个最主要的功能:
- connect:链接容器组件和展示组件
- Provider: 提供包含store的context
connect
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
connect是react-redux提供的一个方法,接受两个参数:
- mapStateToProps
- mapDispatchToProps
执行的结果依然是一个函数,所以才可以在后面又加一个圆括号,把connect函数执行的结果立刻执行,这一次参数是Counter这个组件。
这里有两次函数执行:
- 第一次是connect函数的执行
- 第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件,功能相当于
纯React + 拆分容器组件+展示组件
这个示例中拆分出来的容器组件。
当然我们也可以把connect的结果赋值给一个变量,然后在export这个变量,只是connect已经大大简化了代码,习惯上可以直接导出函数的执行结果,也不用纠结如何命名这个变量。
connect 函数如何工作的?
作为容器组件,要做的工作无外乎两件事情:
- 把store上的状态转换为内层组件的props
- 把内层傻瓜组件中的用户动作转话为派送给store的动作
这两个工作,一个是内层傻瓜组件的输入,一个是内层傻瓜组件的输出。
这两个工作的套路也很明显,把store上的状态转化为内层组件的props,其实就是一个映射的关系,去掉框架,最后就是一个mapStateToProps函数应该做的事情。这个函数命名是业界习惯,因为它只是一个模块内的函数。
把内层傻瓜组件中用户动作转化为派送给Store的动作,也就是把内层傻瓜组件暴露出来的函数类型的prop关联上dispatch函数的调用,每个prop代表的回调函数的主要区别就是dispatch函数的参数不同,这就是mapDispatchToProps函数做的事情,和mapStateToProps一样,函数名只是习惯问题。
mapStateToProps和mapDispatchToProps 函数都可以包含第二个参数,代表ownProps,也就是直接传递给外层容器组件的props????还没理解
react-redux的Provider
react-redux的Provider更加严谨,要求store不光是一个object,而且是必须包含三个函数的object,这三个函数是:
- subscribe
- dispatch
- getState
拥有上述三个函数的对象,才能称之为一个Redux的store。
react-redux定义了Provider的componentWillReceiveProps函数,在react组件的生命周期中,componentWillReceiveProps函数在每次重新渲染时都会调用到,react-redux在componentWillReceiveProps函数中会检查这次渲染是代表store的props和上一次的是否一样。如果不一样,就会给出警告,这样做是为了避免多次渲染用了不同的redux store。每个Redux应用只能有一个store,在整个redux生命周期中,都应该保持store的唯一性。
WHY
为什么要引入Redux?
Redux解决的是什么样的问题?
React-Redux
Redux还可以和其他前端框架结合吗?
Redux的核心概念
- state
- action
- reducer
//普通对象描述应用的state
const state =
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
// 这个对象就像“Model”,区别是该对象没有setter(修改容器方法)。
//因此其他的代码就不能随意修复它,从而造成难以复现的bug。
//要想更新state中的数据,需要发起action。
//action就是一个个普通的JavaScript对象,
//用来描述发生了什么。
// action 示例
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
// 为了把action和state串起来,需要开发一些函数,
//这些函数就是reducer。
// reducer仅仅只是一个接收 state 和 action,并且
//返回新的state的函数。
// 对于大的应用来说,可能很难开发这样的函数,
//所以,我们编写很多小的reducer函数来分别管理state的一部分。
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter;
} else {
return state;
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }]);
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index ?
{ text: todo.text, completed: !todo.completed } :
todo
)
default:
return state;
}
}
//再开发⼀个 reducer 调⽤这两个 reducer,
//进⽽来管理整个应⽤的 state:
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}
永远不要在Reducer里做这些操作:
- 修改传入的参数
- 执行有副作用的操作,如API请求和路由跳转
- 调用非纯函数, 如:
Date.now()
或Math.random()
Redux的三大原则
-
单一数据源 (Single Source of Truth)
整个应用的state被存储在一颗object tree中,并且这个object tree只存在于唯一一个store中。 (如何设计store是Redux应用的核心问题)
-
State是只读的 (State is read-only)
惟⼀改变 state 的⽅法就是触发 action,action 是⼀个⽤于描述已发⽣事件的普通对象。
-
使⽤纯函数来执⾏修改数据(Changes are made with pure functions)
为了描述 action 如何改变 state tree ,你需要编写 reducers。
Store
Store就是把 state,action 和 reducer 联系到一起的对象。
Store有以下的职责:
- 维持应用的state;
- 提供
getState()
方法获取state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe (listener)
方法注册监听器; - 通过
subscribe (listener)
方法返回的函数注销监听器。
** Redux 应用只有一个单一的store**, 当需要拆分数据处理逻辑时,你应该使用reducer组合而不是创建多个store。
根据已有的reducer创建store:
- 使用
combineReducers ()
方法将多个 reducer 合并成为一个 - 将合并的 reducers 导入,并传递给
createStore()
方法。
createStore()
方法的第二个参数是可选的,用于设置 state 的初始状态。
发起 Actions
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters }
// 打印初始状态
console.log(store.getState())
// 每次 state 更新时,打印⽇志
// 注意 subscribe() 返回⼀个函数⽤来注销监听器
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// 发起⼀系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// 停⽌监听 state 更新
unsubscribe();
数据流
严格的单向数据流是Redux架构的设计核心。
Redux 应用中数据的生命周期遵循下面4个步骤:
- 调用
store.dispatch (action)
方法; - Redux store 调用传入的 reducer 函数;
Store 会把两个参数传入 reducer:当前的state树 和 action。 - 根 reducer 应该把多个子 reducer 输出合并成一个单一的state 树;
根 reducer 的结构完全由你决定。 Redux 原生提供combineReducers ()
辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。 - Redux store 保存了根 reducer 返回的完整的 state 树。
流程总结
- 根据业务想好初始化的state和操作state的动作
- 编写actions
- 编写 reducers 去调用 action 和 state,返回新的state
- 根据已有的reducer 创建 store
- 发起action,检查 store中的 state 是否按期望的action 变化
Redux 搭配 React
安装 react-redux 包
容器组件 和 展示组件
Redux 的 React 绑定库是基于容器组件和展示组件相分离的开发思想。
使用 react-redux 包中的connect 方法
容器组件是把展示组件和Redux关联起来。技术上讲,容器组件就是使用 store.subscribe ()
从Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件,
可以通过手工来开发容器组件,但是建议使用 react-redux 库的 connect ()
方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。
使用 connect ()
前,需要先定义 mapStateToProps ()
这个函数来指定如何把当前 Redux Store State 映射到展示组件的 props中。
除了读取state,容器组件还能分发 action。类似的方式,可以定义 mapDispatchToProps ()
方法接收 dispatch ()
方法并返回期望注入到展示组件的props中的回调方法。