文章目录
本文参考文档地址:http://cn.redux.js.org/docs/introduction/Motivation.html
介绍
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。
如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。
这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 我称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 试图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux 就是为了帮你解决这个问题。
跟随 Flux、CQRS 和 Event Sourcing 的脚步,通过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测。这些限制条件反映在 Redux 的三大原则中。
Redux 可以用这三个基本原则来描述:
-
单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree ,调试也变得非常容易。在开发中,你可以把应用的 state 保存在本地,从而加快开发速度。此外,受益于单一的 state tree ,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。
-
State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心竞态条件(race condition)的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。
-
使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。
基础学习 Redux
一、安装
npm install --save redux
下面在React
框架下使用
二、Action
首先,让我们来给 action 下个定义。
Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store。
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
在 Redux 中的 action 创建函数只是简单的返回一个 action:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
这样做将使 action 创建函数更容易被移植和测试。
const sendAction = () => {
return {
type: "send_type",
value: "this is an action"
}
}
module.exports = {
sendAction
}
三、Reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
Reducer接收两个参数
- state
- action
注意:
- 不要修改
state
。 使用Object.assign()
新建了一个副本。不能这样使用Object.assign(state, { visibilityFilter: action.filter })
,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对 ES7 提案对象展开运算符的支持, 从而使用{ ...state, ...newState }
达到相同的目的。 - **在
default
情况下返回旧的state
。**遇到未知的 action 时,一定要返回旧的state
。
Object.assign
须知
Object.assign()
是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,Babel 插件,或者使用其它库如_.assign()
提供的帮助方法。
switch
和样板代码须知
switch
语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。很不幸到现在为止,还有很多人存在一个误区:根据文档中是否使用
switch
来决定是否使用它。如果你不喜欢switch
,完全可以自定义一个createReducer
函数来接收一个事件处理函数列表,参照"减少样板代码"。
const initState = {
value: "default value"
}
const rootReducer = (state = initState, action) => {
console.log("Reducer", state, action);
switch (action.type) {
case "send_type":
return Object.assign({}, state, action);
default:
return state;
}
}
module.exports = {
rootReducer
}
四、Store
在前面的章节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
createStore()
的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
案例:
import {createStore} from 'redux'
import {rootReducer} from '../reducer'
const store = createStore(rootReducer);
export default store;
五、案例演示
下面创建一个控件,点击后修改下面的值:
import React from 'react'
import store from './store'
import { sendAction } from './action'
class ReduxDemo extends React.Component {
handleClick = () => {
const action = sendAction();
store.dispatch(action);
};
componentDidMount() {
store.subscribe(() => {
console.log('subscribe', store.getState());
this.setState({});
})
}
render() {
return (
<div>
<button onClick={this.handleClick}>这是ReduxDemo组件</button>
<h2>{store.getState().value}</h2>
</div>
)
}
}
export default ReduxDemo;
这里介绍一个Chrome
工具Redux DevTools
插件,在谷歌商城下载,使用地址:https://github.com/zalmoxisus/redux-devtools-extension#usage
这里修改一下
import {createStore} from 'redux'
import {rootReducer} from '../reducer'
const store = createStore(rootReducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
运行一下,看它的变化
基础学习 React Redux
这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
尽管如此,Redux 还是和 React 和 Deku 这类库搭配起来用最好,因为这类库允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。
一、安装
Redux 默认并不包含 React 绑定库,需要单独安装。
npm install --save react-redux
如果你不使用 npm,你也可以从 unpkg 获取最新的 UMD 包(包括开发环境包和生产环境包)。如果你用 <script>
标签的方式引入 UMD 包,那么它会在全局抛出window.ReactRedux
对象。
二、Provider 标签
React-Redux 提供<Provider/>
组件,能够使你的整个app访问到Redux store
中的数据:
import React from 'react';
import store from './pages/store'
import ComA from './pages/ComA'
import ComB from './pages/ComB'
import ComC from './pages/ComC'
import {Provider} from 'react-redux'
function App() {
return (
<Provider store={store}>
<div className="App">
<h1>Redux学习</h1>
<ComA/>
<ComB/>
<ComC/>
</div>
</Provider>
);
}
export default App;
在整个APP里,组件ComA
、ComB
、ComC
都能共享到这个store
三、connect函数
1、mapStateToProps(state, ownProps) : stateProps
这个函数允许我们将 store 中的数据作为 props 绑定到组件上。
const mapStateToProps = (state) => {return { count: state.count } }
(1)这个函数的第一个参数就是 Redux 的 store,我们从中摘取了 count 属性。你不必将 state 中的数据原封不动地传入组件,可以根据 state 中的数据,动态地输出组件需要的(最小)属性。
(2)函数的第二个参数 ownProps,是组件自己的 props。有的时候,ownProps 也会对其产生影响。
当 state 变化,或者 ownProps 变化的时候,mapStateToProps 都会被调用,计算出一个新的 stateProps,(在与 ownProps merge 后)更新给组件。
2、mapDispatchToProps(dispatch, ownProps): dispatchProps
connect 的第二个参数是 mapDispatchToProps,它的功能是,将 action 作为 props 绑定到组件上,也会成为 组件的 props。
3、[mergeProps],[options]
不管是 stateProps 还是 dispatchProps,都需要和 ownProps merge 之后才会被赋给组件。connect 的第三个参数就是用来做这件事。通常情况下,你可以不传这个参数,connect 就会使用 Object.assign 替代该方法。
4、[options] (Object)
如果指定这个参数,可以定制 connector 的行为。一般不用。
四、案例
下面用一个案例来说明:
问题:通过两个按钮来加减显示数字,范围是0~100,需要三个控件,控件A是加号键,点击一次,B控件加1,C控件点击一次,B控件减一。
首先我们需要三个控件
ComA
import React from "react";
import {connect} from 'react-redux'
class ComA extends React.Component {
handleClick = () => {
console.log('ComA',this.props);
// 4、当点击时发送action,也就是说发送函数会合并到props上,这样通过props就可以获取到发送函数了
this.props.sendAction();
}
render() {
return (<button onClick={this.handleClick}>+</button>)
}
}
// 2、这个函数接受一个dispatch函数,它是用来发送action的
const mapDispatchToProps = (dispatch) => {
return {
sendAction: () => {
// 3、这个函数的参数是一个action
dispatch({
type: 'add_action'
})
}
}
}
// 1、这里是要发送actoin,所以使用第二个参数,增强控件
export default connect(null, mapDispatchToProps)(ComA)
ComB
import React from "react";
import {connect} from 'react-redux'
class ComB extends React.Component{
render() {
console.log('ComB',this.props);
// 3、这里通过props可以获取到state的属性,也就是说state会合并到props上
return (<div>{this.props.count}</div>)
}
}
// 2、这个函数第一个参数是接受的state,返回值是state,当state不变的时候是不操作的
const mapStateToProps = (state)=>{
console.log('ComB',state)
return state;
}
// 1、这个控件用来接受state的,使用第一个参数
export default connect(mapStateToProps)(ComB)
ComC
控件和ComA
基本一致
import React from "react";
import {connect} from 'react-redux'
class ComC extends React.Component {
handleClick = () => {
console.log('ComC',this.props);
this.props.sendAction();
}
render() {
return (<button onClick={this.handleClick}>-</button>)
}
}
const mapDispatchToProps = (dispatch) => {
return {
sendAction: () => {
dispatch({
type: 'sub_action'
})
}
}
}
export default connect(null, mapDispatchToProps)(ComC)
然后创建store
import {createStore} from 'redux'
import {reducer} from '../reducer'
const store = createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
接着创建reducer
const initstate = {count: 0};
exports.reducer = (state = initstate, action) => {
console.log('reducer', action)
//这里是业务逻辑
switch (action.type) {
case 'add_action':
if (state.count===100){
state.count = -1;
}
return {
count: state.count + 1
};
case 'sub_action':
if (state.count===0){
state.count = 101;
}
return {
count: state.count - 1
};
default:
return state;
}
}
最后将控件整合到APP
上
import React from 'react';
import store from './pages/store'
import ComA from './pages/ComA'
import ComB from './pages/ComB'
import ComC from './pages/ComC'
import {Provider} from 'react-redux'
function App() {
return (
//这里要把store绑定到Provider上
<Provider store={store}>
<div className="App">
<h1>Redux学习</h1>
<ComA/>
<ComB/>
<ComC/>
</div>
</Provider>
);
}
export default App;
然后运行:
这样就看到效果了,控件之间产生了互动;
下面说一下流程情况:
这里的发送和接收都是通过connect
函数增强实现的。