Redux
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html
http://cn.redux.js.org/docs/api/applyMiddleware.html
也参考《深入React技术栈》
Redux是Flux的实现,或者说Redux参考了Flux的设计。Redux是一个包,提供了一些api让我们方便创建store、更新数据等,简化我们的操作。
Flux解决了MVC的局限性,Redux也遵循Flux的这个特点,所以使用Redux的原则就是一个应用的数据都存放在单一的数据源Store中(太大的话可以通过combineReducers来进行拆分),这解决了MVC 多个model,数据流混乱的问题,而且单个数据源的话,方便持久化。
在Flux中,要求store是只读的(只允许内部对store进行修改),也就是没有给外界提供setter方法,在Redux中,更加发扬这种只读的特性,修改状态都是在reducer中返回一个全新的state,而不会对原来的state进行修改
Redux = Reduce + Flux。所以在Redux中对这两者的理解十分重要
重申一下redux和flux在应用上的区别:
redux:调用storeA.dispatch,storeA中的reducer会被触发执行,而storeB中的reducer不会执行。
Flux:调用Dispatcher.dispatch方法,所有store在上面register的方法都会执行
因为store中的state代表整个应用程序的状态,而修改state,必须通过dispatch(Action)来实现,也就是说每次改变对应一个action,只需要跟踪action,即可跟踪整个程序的状态变化
Store
import { createStore } from 'redux';
const store = createStore(reducer);
在View中调用dispatch发送action
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
// 使用Action Creator
store.dispatch(addTodo('Learn Redux'));
查看 这里 ,store代表了整个应用程序的状态,而状态的初始化当然也是在store中进行处理,有两种方式:
- 指定reducer中的state参数默认值,store被创建的时候,这个reducer会被执行一次,reducer内我们把这个默认值返回返回即可
- 指定createStore的第二个参数,这个参数代表默认值,这种方式指定的优先级比以上默认值方式更高,即这个值会在初始化的时候被传递进reducer的state。
以上两种方式的本质都是要求reducer返回一个初始值,只不过这个初始值可能是默认值而已。
reducer
这个函数的职责就是根据当前state以及action,计算出下一个新的状态new_state,如果不需要发生变化,则返回原state即可
创建Store的时候,需要传入一个函数作为Store的Reducer。以上dispatch之后,这个Action就会被传入到这个Store中的reducer进行处理,返回一个新的state
const reducer = function (state, action) {
// ...
return new_state;
};
对于程序的初始状态,可以这么进行设置(在执行createStore的时候,reducer会被首次执行用于获取初始状态,显然这个时候store没有任何状态,所以应该给state设置一个默认值,作为初始状态):
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
规范要求Reducer是一个纯函数,不能对输入数据进行修改,而且相同的输入对应相同的输出(更加方便测试)。所以必须要返回一个全新的对象,手段如下:
function reducer(state, action) {
return Object.assign({}, state, { thingToChange });
return { ...state, ...newState };
return [...state, newItem];
}
以上reduce返回了一个不同的对象(state),就代表界面需要刷新,因为状态发生变化了。这需要在view中对store进行监听:
store.subscribe(listener);
在以上的listener中调用setState即可。
回顾以上的逻辑(调用了Store的subscribe、getState和dispatch方法):
在模块中创建store,view引入这个模块,进行store.subscrible注册监听。
在注册的回调函数中,先调用store.getState获取到最新的状态,然后把数据通过setState更新到界面上(View模板中通过this.state.xx获取数据)
用于与view发生交互,view调用store.dispatch,然后store中的reducer被执行,返回一个新的state,以上的监听就会被触发
Reducer拆分
以上可以发现一个store中对状态的修改全部集中在reduce函数中,有必要对这个函数进行拆分
const chatReducer = (state = defaultState, action = {}) => {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
};
可见以上的state中有3个字段,每个字段的值都通过一个函数的返回值来决定,在这个函数中进行action.type的判断,再进行数据修改,返回最新的值。
这个过程可以使用Redux提供的语法糖combineReducers进行更改(这里使用了对象的简洁写法):
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
Reducer的复用
以上使用combineReducers的时候,可能会遇到一个问题,如chatLog与statusMessage中对数据的处理逻辑很类似,怎么把这两个函数中的逻辑抽取出来呢?
假如这两部分直接使用相同的函数,这会造成一个问题,即这两个属性对于相同的action,返回的数据是一致的(因为调用的是同一个函数,里面对action.type的判断以及数据处理是完全一致的),即会同时相同改变。这不是我们想要的。
这时可以使用一个函数(高阶reducer)来生成这些需要被复用的reducer,然后通过参数来控制这些reducer的差异,实现返回的reducer内部逻辑大部分相同,但存在差异。如为里面判断的action.type添加一个前缀
State
一个view和一个state相互对应
const state = store.getState();
Action
用户与View发生交互,View发出Action,触发State的变化(因为State是从Store中获取的,所以也可以说成Store发生变化)
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
Action Creator简化Action的生成
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
中间件
分析以上过程可以发现,Dispatch到reduce之间的细节被封装到Redux框架内部,假如我希望在执行dispatch的时候先进行一些操作,后执行reduce或者说要增强dispatch。这就需要用到中间件。
使用Flux架构进行开发,会遇到一个问题,纠结在哪里处理异步,哪里发请求,这是因为Flux没有规定在哪里进行异步请求(感觉其实也可以这样,在action creator中调用dispatch之前进行异步请求,有了结果之后再进行dispatch action 告知结果)。而redux在这点上提供了解决方案,使用中间件增强dispatch,使其可以处理异步请求。
手动实现一个日志中间件
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
redux-thunk
thunk返回一个接收 ‘ dispatch’ 的函数Fn,在Fn中进行异步操作,返回promise,在这个promise上注册的回调函数中可以调用dispatch,使用的方式如下:
// thunk
function makeASandwichWithSecretSauce(forPerson) {
return function (dispatch,getState) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
)
}
}
// Thunk middleware 可以让我们像 dispatch 普通 action
// 一样 dispatch 异步的 thunk action。
store.dispatch(
makeASandwichWithSecretSauce('Me')
)
本质就是thunk返回的函数中返回一个promise,在这个promise的异步操作中调用dispatch,可以发送action。
React-redux
redux不一定要配合react来使用,更加通用。而react-redux提供了api帮助react和redux进行绑定
了解容器型组件和展示型组件 。容器组件由React-Redux生成,即状态全部交由框架来管理。
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
基本的用法就是最外层一个provider,接着就是容器组件,这个容器组件是通过react-redux的connect生成的,connect为这个容器组件中的所有子组件提供了访问store的功能,connect帮助store与组件建立关联(state映射到props),即通过组件内的props可以获取到store中state的值,以及当state发生变化,组件会自动重新渲染(connect中mapStateToProps(state)和mapDispatchProps(dispatch)也会重新执行,即重新执行映射),而这些功能都在Container中做好了。
这里记录一些使用要点:
首先定义一个展示型组件(无状态组件),里面UI的回调函数以及要展示的数据都是通过props获取的
通过connect根据展示型组件生成一个容器组件,还需要函数mapStateToProps(state)和mapDispatchProps(dispatch),前者用于把传入的state(第一次执行的话这个state就是来源于store的初始state,有两种方法可以指定),映射成props对象属性返回,这里用到的state是通过store.getState获取,而store后面会传入;后者也是用于定义props对象属性返回,这不过这些属性都是函数,用于被容器中的子组件调用,在这些函数中调用dispatch发送action,或者这些函数中直接返回一个action,也会被自动发送。
以上创建完成容器组件之后,接着通过createStore(reducer)创建store,根据以往的理解,接下来需要通过store.subscrible进行监听,如果reducer返回的state发生变化,则回调函数会被执行,里面执行setState触发重新渲染。但这个监听的步骤在react-redux中已经做好了,在容器组件被实例化的时候,会获取外部的store(后面会说道怎么把store传给容器组件),然后进行监听,已经重新渲染等操作。
最后通过provider的store属性(provider组件从React-redux中获取),把我们创建的store传递进去即可。被provider包着的容器组件内部就可获取到这个store了。回顾context的用法就知道provider内部做了什么(定义getChildContext)以及容器组件是如何获取到store了(通过this.context获取)。
对于树形的组件结构如何用react-redux来实现?展示型组件中有多个子组件,这些子组件可以拥有自己的状态,也可以通过获取上级展示型组件中的props来展示数据。
dispatch除了可以在中mapDispatchProps(dispatch)使用,还可以在被连接的展示型组件中使用:
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux'
import { Provider,connect } from 'react-redux'
let Btn = ({dispatch})=>{
return (
<button onClick={()=>{dispatch({type:"click"})}}>click</button>
)
}
const reducer = (state,action)=>{
console.log(action)
return state;
}
const store = createStore(reducer);
Btn = connect()(Btn);
ReactDOM.render(
<Provider store={store}>
<Btn />
</Provider>,
document.getElementById('root')
);
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux'
import { Provider,connect } from 'react-redux'
class Show extends React.Component{
componentDidMount(){
this.props.loadData();
}
render(){
return <div>{this.props.msg}</div>
}
}
const mapStateToProps = (state)=>{
return {
msg:state.msg
}
};
const mapDispatchProps = (dispatch)=>{
return {
loadData:() => {
setTimeout(()=>{
dispatch({type:"onLoaded",data:"async data"})
},2000);
}
}
}
const Container = connect(mapStateToProps,mapDispatchProps)(Show);
const reducer = (state, action) => {
switch (action.type) {
case "onLoaded":
return {
msg: action.data
}
}
return state
}
const initState = {msg:""}
const store = createStore(reducer,initState);
ReactDOM.render(
<Provider store={store}>
<Container/>
</Provider>,
document.getElementById('root')
);
运行结果:2s 后界面上才会显示数据。