Ref: 阮一峰的网络日志
安装
安装稳定版:
npm install --save redux
附加包
多数情况下,你还需要使用 React 绑定库和开发者工具。
npm install –save react-redux
npm install –save-dev redux-devtools
设计思想
Redux 的设计思想很简单,就两句话。
(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。
基本概念和API
- Store
Store就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Redux 提供createStore
这个函数,用来生成 Store。
import { createStore } from 'redux';
const store = createStore(fn);
上面代码中,createStore
函数接受另一个函数作为参数,返回新生成的 Store 对象。
- State
Store
对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
当前时刻的 State,可以通过store.getState()
拿到。
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
- Action
State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置,社区有一个规范可以参考。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串Learn Redux。
可以这样理解,Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。
- Action Creator
View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
上面代码中,addTodo函数就是一个 Action Creator。
- store.dispatch()
store.dispatch()是 View 发出 Action 的唯一方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。
结合 Action Creator,这段代码可以改写如下。
store.dispatch(addTodo('Learn Redux'));
reducer 是一个接收 state 和 action,并返回新的 state 的函数。
三大原则
Redux 可以用这三个基本原则来描述:- 单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。 - State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。 - 使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
- 单一数据源
Note:
ES6的...
,该运算符将一个数组,变为参数序列。
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。
// ES5 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
上面代码的 ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。
一个很好的redux入门step by step教程:
https://segmentfault.com/a/1190000011474522
使用Redux工具调试
如果我们的代码出错了,应该如何调试呢?
Redux拥有很多第三方的调试工具,可用于分析代码和修复bug。最受欢迎的是time-travelling tool,即redux-devtools-extension。设置它只需要三个步骤。
首先,在Chrome中安装Redux Devtools扩展。
然后,在运行Redux应用程序的终端里使用Ctrl+C停止服务器。并用npm或yarn安装redux-devtools-extension包。
yarn add redux-devtools-extension
一旦安装完成,我们对store.js稍作修改:
Learning from React.js 小书
React-redux 就是把 Redux 这种架构模式和 React.js 结合起来的一个库,就是 Redux 架构在 React.js 中的体现。
函数dispatch
它专门负责数据的更改。它接受一个参数action
,这个action
是一个普通的JavaScript对象,里面必须包含一个type字段来声明action的种类。dispatch在switch中识别这个type字段,能够被识别出来的action种类才能执行对appState的修改。
function dispatch (action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}
三:纯函数(Pure Function)
纯函数有如下特点:
- 函数的返回结果只依赖于它的参数。
即,函数的返回结果与其他外部值无关。- 函数执行过程里面没有副作用。
不会修改外部传进来的对象。
除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 Ajax 请求,还有调用 window.reload 刷新浏览器,甚至是 console.log
往控制台打印数据也是副作用。
纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。
为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。
四:共享结构的对象提高性能
这里目的是优化多余的更新操作:因为有时候只是某个值被修改了,但是却导致整个页面的整体渲染,这明显是非常耗费时间的。
这里提出的解决方案是,在每个渲染函数执行渲染操作之前先做个判断,判断传入的新数据和旧的数据是不是相同,相同的话就不渲染了。
- 共享结构的对象
希望大家都知道这种 ES6 的语法:
const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }
const obj2 = { ...obj }
其实就是新建一个对象 obj2,然后把 obj 所有的属性都复制到 obj2 里面,相当于对象的浅复制。上面的 obj 里面的内容和 obj2 是完全一样的,但是却是两个不同的对象。除了浅复制对象,还可以覆盖、拓展对象属性:
const obj = { a: 1, b: 2}
const obj2 = { ...obj, b: 3, c: 4} // => { a: 1, b: 3, c: 4 },覆盖了 b,新增了
例如,新建一个 appState
,新建 appState.title
,新建 appState.title.text
:
let newAppState = { // 新建一个 newAppState
...appState, // 复制 appState 里面的内容
title: { // 用一个新的对象覆盖原来的 title 属性
...appState.title, // 复制原来 title 对象里面的内容
text: '《React.js 小书》' // 覆盖 text 属性
}
}
如果我们用一个树状的结构来表示对象结构的话:
appState 和 newAppState 其实是两个不同的对象,因为对象浅复制的缘故,其实它们里面的属性 content 指向的是同一个对象;但是因为 title 被一个新的对象覆盖了,所以它们的 title 属性指向的对象是不同的。
修改数据的时候就把修改路径都复制一遍,但是保持其他内容不变,最后的所有对象具有某些不变共享的结构(例如上面2个对象共享 content 对象)。大多数情况下我们可以保持 50% 以上的内容具有共享结构,这种操作具有非常优良的特性,我们可以用它来优化上面的渲染性能。
- 优化性能
修改数据的时候,并不会直接修改原来的数据 state,而是产生上述的共享结构的对象:
function stateChanger (state, action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
text: action.text
}
}
case 'UPDATE_TITLE_COLOR':
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
color: action.color
}
}
default:
return state // 没有修改,返回原来的对象
}
}
五: reducer
createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action。如果没有传入state或者state===null的话,那么它就会返回一个初始化的数据,如果有传入state的话,它就会根据action来”修改数据”(其实并不是修改,而是把修改路径的对象都赋值一遍,然后产生一个新的对象返回)。如果不能识别action的话,它就会原封不动地将state返回。
reducer 是不允许有副作用的。你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅是 —— 初始化和计算新的 state。
六:redux总结
我们优化了 stateChanger 为 reducer,定义了 reducer 只能是纯函数,功能就是负责初始 state,和根据 state 和 action 计算具有共享结构的新的 state。
createStore 现在可以直接拿来用了,套路就是:
// 定一个 reducer
function reducer (state, action) {
/* 初始化 state 和 switch case */
}
// 生成 store
const store = createStore(reducer)
// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))
// 首次渲染页面
renderApp(store.getState())
//后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)
例子练习: #16 实现 Users Reducer
动手实现 React-redux
connect
和mapStateToProps
策略:既然store有严格规定的的方式进行数据的修改(使用dispatch
),这样就避免了之前直接用context进行数据共享的时候,数据紊乱的问题。那我们现在就把store作为context放在父组件中,子组件通过调用父组件的context得到store,就可以安全地获取数据了。
带来问题:用context解决变量共享问题会导致:
1. 大量重复的逻辑: 取出context
,取出里面的store,然后用store里面的状态设置自己的状态。
2. 对context依赖性过强:这些子组件都要依赖父组件的context来获取数据,使得该子组件本身的复用性基本为零。
这里我们要引用高阶组件。我们称这个高阶组件为connect
,因为它把Dumb组件(可预测性很强,对于参数以外零依赖,复用性强)和context连接起来了:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export connect = (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
// TODO: 如何从 store 取数据?
render () {
return <WrappedComponent />
}
}
return Connect
}
每个传进去的组件需要 store 里面的数据都不一样的,所以除了给高阶组件传入 Dumb 组件以外,还需要告诉高级组件我们需要什么数据,高阶组件才能正确地去取数据。
为了解决这个问题,我们可以给高阶组件传入类似下面这样的函数:
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor,
themeName: state.themeName,
fullName: `${state.firstName} ${state.lastName}`
...
}
}
这个函数会接受store.getState()
的结果作为参数,然后返回一个对象,这个对象时根据state
生成的。mapStateToProps
相当于告知了Connect
应该如何去store里面取数据,然后可以把这个函数的返回结果传给被包装的组件。
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
render () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState())
// {...stateProps} 意思是把这个对象里面的属性全部通过 `props` 方式传递进去
return <WrappedComponent {...stateProps} />
}
}
return Connect
}
这个connect
接受一个参数mapStateToProps
,然后返回一个函数,这个返回的函数才是高阶组件。connect
的用法是:
...
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}
Header = connect(mapStateToProps)(Header)
...
connect 还没有监听数据变化然后重新渲染,所以现在点击按钮只有按钮会变颜色。我们给 connect 的高阶组件增加监听数据变化重新渲染的逻辑,稍微重构一下 connect:
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = { allProps: {} }
}
componentWillMount () {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState(), this.props) // 额外传入 props,让获取数据更加灵活方便
this.setState({
allProps: { // 整合普通的 props 和从 state 生成的 props
...stateProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
我们在Connect
组件的constructor
里面初始化了state.allProps
,它是一个对象,用来保存需要传给被包装组件的所有参数。生命周期componentWillMount
会调用_updateProps
进行初始化,然后通过store.subscribe
监听数据变化重新调用_updateProps
。
为了让connect返回新组件和被包装的组件使用参数保持一致,我们会把所有传给Connect
的props
原封不动地传给WrappedComponent
。因此在_updateProps()里面,我们整合了普通的this.props
和从state生成的props,一起放在this.state.allProps
里面,再通过render
方法,把所有参数都传给WrappedComponent。
mapDispatchToProps
有些时候,我们不仅需要store里面的数据,也需要store来dispatch.
我们可以给connect函数传入另一个参数来告诉它,我们的组件需要如何触发dispatch
,这个参数称为mapDispatchToProps
.
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}
和mapStateToProps
一样,它返回一个对象,这个对象内容会同样被connect
当作是props
参数传给被包装的组件。不同的是,这个函数不是接受state
作为参数,而是dispatch
, 这里用到dispatch
来触发特定的action
。
修改connect
函数内的_updateProps()
:
...
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {} // 防止 mapStateToProps 没有传入
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {} // 防止 mapDispatchToProps 没有传入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
...
在这个函数中,我们把store.dispatch
作为参数传给mapDispatchToProps
,它会返回一个对象dispatchProps
。接着把stateProps
,dispatchProps
, this.props
三者合并到this.state.allProps
里面去,它们都在render的时候传给被包装的组件: return <WrappedComponent {...this.state.allProps} />
我们看ThemeSwitch
的代码,
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from "./Connect";
class ThemeSwitch extends Component {
//定义了两个props
static propTypes = {
themeColor: PropTypes.string,
onSwitchColor : PropTypes.func
}
handleSwitchColor(color) {
if (this.props.onSwitchColor){
//如果onSwitchColor函数存在的话,则调用
this.props.onSwitchColor(color)
}
}
render () {
return (
<div>
<button style={{ color: this.props.themeColor}}
onClick={this.handleSwitchColor.bind(this,'red')}>Red</button>
<button style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this,'blue')}>Blue</button>
</div>
)
}
}
//以下是从store获取参数themeColor和onSwitchColor
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({
type:'CHANGE_COLOR',
themeColor:color
})
}
}
}
const mapStateToProps = (state) => {
return {
themeColor:state.themeColor
}
}
ThemeSwitch = connect(mapStateToProps,mapDispatchToProps)(ThemeSwitch)
export default ThemeSwitch
Provider
为什么需要Provider
?
我们在定义父组件Index的时候,有一段这样的代码⬇️。它主要的功能其实是:创建context,把store存放到里面,好让子组件connect的时候能够取到store。
class Index extends Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
return { store }
}
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
现在我们额外构建一个组件来做这件事,然后让这个组件称为组件树的根节点,那么它的子组件就都可以获取到context了。
我们把这个组件叫做Provider
,因为它提供了store
以下是Provider的定义:
export class Provider extends Component {
static propTypes = {
store: PropTypes.object,
children: PropTypes.any
}
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
return {
store: this.props.store
}
}
render () {
return (
<div>{this.props.children}</div>
)
}
}
Provider就是一个容器组件,会把嵌套的内容原封不动地作为自己的子组件渲染出来,它还会把外界传给他的props.store
放到context,这样子,子组件connect的时候可以获取到。
一般Provider
这样用:
// 头部引入 Provider
import { Provider } from './react-redux'
// 删除 Index 里面所有关于 context 的代码
class Index extends Component {
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
// 把 Provider 作为组件树的根节点
ReactDOM.render(
<Provider store={store}>
<Index />
</Provider>,
document.getElementById('root')
)
- 真正地使用redux和react-redux
在工程目录下使用 npm 安装 Redux 和 React-redux 模块:
npm install redux react-redux --save
把 src/ 目录下 Header.js、ThemeSwitch.js、Content.js 的模块导入中的:
import { connect } from './react-redux'
改成:
import { connect } from 'react-redux'
也就是本来从本地(自己写的connect
) 导入的 connect 改成从第三方 react-redux 模块中导入。
然后,删除自己写的 createStore,改成使用第三方模块 redux 的 createStore;Provider 本来从本地 引入,改成从第三方 react-redux 模块中引入。其余代码保持不变。
{connect}
高阶组件(函数) 和 Provider
组件都由'react-redux'
包提供。
{connect} <- 'react-redux'
Provider <- 'react-redux'