一、前言
redux是什么?
由Flux演变而来,作为状态容器,提供可预测的状态管理。
为什么要用redux?
-
复杂应用中无法获取操作记录,而状态的改变往往意味着对应视图的变化,这一过程是不可控的。
提供一种机制,统一对状态的
查询、改变、应用
进行管理,同时对每一次的状态变更可进行回溯追踪
。 -
多个组件件间可能存在数据的共享以及通信。
这里可以简单将其理解为充当了eventBus的角色。
-
视图的改变以及数据的获取等操作杂糅在一起,不利于维护
从
具体操作
->状态变更
->触发视图更新
,这一单向数据流
的控制,不仅使得对状态变化的过程变得可控,同时解耦了数据M和视图V
。见下图,针对相同的action,其数据流转以及state管理是独立于前端模板的,因此可以实现跨框架的复用。
redux适用所有项目?
- 需不需要使用redux,还是要结合项目实际以及业务需求,它只是web架构的一种解决方案。比如完全没有必要因为第二种组件通信而使用,目前的主流框架如vue有vuex、依赖注入等实现。
- react使用flux,主要是因为react只提供了组件化UI,是视图模板渲染的一种解决方案,并没有提供诸如双向绑定之类的对数据流的有效管理。
二、Redux概述
Redux主要有如下几个概念:
-
action:一般写法:
<type:操作意图(一般定义为字符串常量), payload:{text:"要改变的新数据"}>
,是store数据的唯一来源。 -
store:Redux应用只有一个单一的store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合而不是创建多个store。(常用方法:
dispatch
、getState
、subscribe
等) -
state:数据对象,由store对象管理,通过
getStore()只读
。改变的唯一途径:由store的dispatch方法对action进行分发,reducer进行处理 范式化。
-
reducer:对action做出相应响应,返回新生成的state(
这也是保证可回溯的原因
)。
【扩展】生成新的state有如下几种方式:Object.assign({}, oldState, newState)
{...oldSate, 新值}
Immutable
这里扩展下:javascript中的基本类型:Boolean、Number、String、Null、undefined、Symbol
等都是不可变的(Immutable
),只有Object
是可变的(Mutable
).
-
subscribe
用于订阅事件,每次state变更时,都会触发其订阅的事件。在这里可以处理
state->view
的绑定相关逻辑,Redux没有对其做约束。
react-redux提供了使用provide和connect绑定,不必关心隐含在内的订阅方法。 -
中间件
这里强调下Redux的中间件使用,在每次action触发时,都会先通过层层中间件再真正执行。这个过程赋予了我们诸如日志记录等能力。洋葱圈模型,和KOA的中间件原理类似。
运作原理图如下:
三、常见问题
1. 怎么处理异步事件?
解答这个问题前,先看下同步action的处理:
//该方法用于生成action
let actionCreator = (name) => ({
type: 'ADD_ITEM',
value: { name }
});
使用:
dispatch(actionCreator('M2'));
现在处理异步的事件要面临一个问题:在何处发请求?无外乎三处选择:actionCreator
、middleware
、reducer
,而reducer是纯函数,middleware一般作用于所有action,因此不建议用于异步事件的处理。由于actionCreator用于生成action,其value往往是直接改变state的值,所以,在actionCreator中处理异步事件生成action,合情合理:
改造1:直接在actionCreator中使用异步
let asyncActionCreator = (name) => {
setTimeout(() => {
return {
type: 'ADD_ITEM',
name: { name }
};
}, 3000)
}
//问题:会直接返回undefined,不符合预期
改造2:异步情况下返回function,而不是返回action对象
let asyncActionCreator = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
# 这里需要使用middleware支持异步,例如redux-thunk
# var thunkMiddleware = function ({ dispatch, getState }) {
# return function(next) {
# return function (action) {
# //如果是function,则传递dispatch、getState,并执行
# return typeof action === 'function' ?
# //原始的dispatch
# action(dispatch, getState) :
# next(action)
# }
# }
# }
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二:返回的Promise,可以在dispatch异步数据,reducer处理后,做一些处理
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
【扩展】thunk的概念,可以简单理解为:将多参数函数替换成一个只接受回调函数作为参数的单参数函数(以闭包的形式实现延迟执行的一段代码)。 举例:
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
【扩展】异步解决方案中一般将异步操作包装成Thunk函数
或者Promise对象
,即Generator的next()
方法返回值的value
属性是一个Thunk函数或者Promise对象。换句话说,Thunk和Promise都是Generator自动执行的解决方案。
2. 多个reducer合并
不可能把所有的action处理放到一个reducer中,可以按业务或者组件对reducer进行拆分,方便维护。
Redux提供了combineReducers
,将子 Reducer 合并成一个大的函数:
import { createStore, combineReducers } from 'redux'
var reducer = combineReducers({
user: userReducer,
items: itemsReducer
})
其对应的state结构,其中user
、items
等同于命名空间,可以进行数据隔离:
{
user: {},
items: {}
}
combineReducer的简单实现:
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
//根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象
nextState[key] = reducers[key](state[key], action);
return nextState;
},
{}
);
};
};
3. appyMiddleware(middlewares)(createStore)(reducers)在干什么?
和createStore(reducers)
的区别是增强了store的dispatch,在dispatch前后根据提供的middleware,提供一些能力。
【扩展】在看源码的时候会发现柯里化
(什么是柯里化?)的使用很常见:combinReducer、applyMiddleware等。好处:提前绑定参数,延迟计算。
thunk和柯里化的对比:
1)两者的返回值都是函数
2)柯里化:一次使用一个参数;配置部分参数,后续重复使用
3)thunk:同样是延迟执行,不过没有要求一次必须一个参数,只要最后转换为只接受回调函数作为参数的单参数函数。
个人认为,没必要纠结名词,知道有这种解决问题的思路即可(尤其在当前流行造各种花里胡哨名词的氛围下)。
四、redux实例
以下是仿Redux写的demo,实现了Redux的基本能力,缓存了操作记录,进一步扩展可以实现诸如回退等功能。
以下列出了几个核心函数:
-
store
/** * store提供的方法: * 1. getState:获取当前state * 2. dispatch:触发行为 * 3. subscribe:订阅事件 */ function createStore(reducers, initState) { //historyState可用作时光机 let state = initState, historyState = [{action:'init', state: initState}], listeners = []; let getState = () => state; let subscribe = (event) => { listeners.push(event); /** * 注册事件的同时,获取事件句柄 * let fnHandle = fnsubscribe(fn) * 删除对应注册事件 * fnHandle(); * */ return () => { listeners = listeners.filter(eventItem => eventItem != event); } } let dispatch = (action) => { //reducer根据action对state做改变,返回全新的state state = reducers(state, action); historyState.push({action, state}) //state改变的情况下,触发事件 listeners.forEach(listener => listener()); } return { dispatch, getState, subscribe, getHistoryState: () => historyState } }
-
绑定中间件applyMiddleware
//将中间件绑定到 let applyMiddleware = middlewares => (createStore) => (...args) => { let store = createStore(...args); let dispatch = store.dispatch; let params = { dispatch, getState: store.getState }; middlewares = middlewares.map(mw => mw(params)); //组合中间件,增强dispatch //装饰者模式 & 洋葱圈,compose对中间件的组合 dispatch = compose(middlewares)(dispatch); //返回增强后的dispatch return Object.assign(store, {dispatch}); }
-
使用
- reducers
/** * 对action做处理,返回全新的state */ function reducers(state, action){ switch(action){ case 'add': return state += 1; case 'minus': return state -=1; default: return 0; } }
- middleware
/** * 中间件(注意中间件的约定格式) */ function middleware1({dispatch, getState}) { return function(next) { return function(action) { console.log(`【日志】当前state值:${getState()},执行${action}`); next(action); console.log(`【日志】操作完成后,state值:${getState()}`); } } } function middleware2({dispatch, getState}) { return function(next) { return function(action) { console.log(`>>>>>>>>>>>>>>>>>>`); next(action); console.log(`<<<<<<<<<<<<<<<<<<`); } } }
- 初始化store
/** * 对外暴露: * 1. createStore: 产生store * 2. applyMiddleware:中间件处理 */ let initState = 1; let store = applyMiddleware([middleware2, middleware1])(createStore)(reducers, initState);
- reducers
-
展示
demo展示效果如下:这里只是简单说明redux的能力,具体功能可自行扩展。
完整代码见:github.com/269117220/w…
五、React-Redux
1. 组件的拆分
基于容器组件和展示组件相分离的开发思想,将组件分为:UI组件
和容器组件
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展示(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用Redux | 否 | 是 |
数据来源 | props | 监听Redux state |
数据修改 | 从props调用回调函数 | 向Redux派发actions |
调用方式 | 手动 | 通常由React Redux生成 |
2. 容器组件
用于关联展示组件和Redux,利用store.subscribe()
从Redux state中读取需要的数据,并通过props来把这些数据提供给要渲染的组件。
import { connect } from 'react-redux'
//第一次是设置参数,第二次是组件与 Redux store 连接
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
connect()
: 不会改变原来的组件类,返回一个新的与 Redux store 连接的组件类。生成的容器组件,做了性能优化避免不必要的重复渲染。
容器组件做的两件事:
提前声明:容器组件的
state
和ActionCreator
作为子组件(展示组件)的props
传入。
-
mapStateToProps:
state -> view
订阅了全局状态state
的变化,每次state
更新时,对state进行过滤,只返回该容器型组件关注的局部状态,继而触发渲染。
将state映射到展示组件的props,即下面例子中的todos为props的属性,其值的来源为state。
每一次全局状态变化都会调用所有容器型组件的mapStateToProps方法
,该方法返回一个纯对象,并将其合并到容器型组件的props上。const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) case 'SHOW_ALL': default: return todos } } // 函数接收整个 Redux store 的 state 作为 props,然后返回一个传入到组件 props 的对象。 const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }
返回的对象会与组件的 props 合并。
-
Immutable的使用
问题: 由于state变动,会通知所有订阅事件,所有容器组件的
mapStateToProps
都会执行一遍,如果不做特殊处理,会导致全部组件重新渲染。
解决: 因此为了避免无意义的重新渲染,在遵从Redux的immutable状态规范的情况下,当一个容器型组件的默认shouldComponentUpdate函数返回true时,则表明其对应的局部状态发生变化,需要将状态传播到各个子组件,相应的所有子组件也都会进行虚拟DOM比较,以确定是否需要重新渲染。
-
-
mapDispatchToProps:
view -> action
建立 UI 组件的参数到store.dispatch方法的映射,返回期望注入到展示组件的 props 中的回调方法,即ActionCreator
。const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } } }
这些属性会被合并到组件的 props 中。
3. <Provider>
组件
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。一种方式是把它以 props 的形式传入到所有容器组件中,存在层层繁琐的传递,而且往往中间组件并不需要的问题。建议的方式是使用指定的 React Redux 组件 <Provider>
:
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。原理是React组件的利用context
属性。
React-Redux的connect代码类似下面所示:
export const connect = (mapStateToProps, mapDispatchToProps) => (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 ? mapStateToProps(store.getState(), this.props): {}
let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
3.1 context的使用
3.1.1 写法一:
生产者:
// 声明Context对象属性
static childContextTypes = {
propA: PropTypes.string,
methodA: PropTypes.func
}
// 返回Context对象,方法名是约定好的
getChildContext () {
return {
propA: 'propA',
methodA: () => 'methodA'
}
}
消费者:
// 通过静态属性contextTypes声明需要使用的Context属性
static contextTypes = {
propA: PropTypes.string
}
//使用,为声明无法获取
this.context.propA
******************************
//example:无状态子组件访问父组件的context
import React from 'react'
import PropTypes from 'prop-types'
//函数式组件等同于类组件的render【第二个参数】
const ChildComponent = (props, context) => {
const {
propA
} = context
return ...
}
ChildComponent.contextProps = {
propA: PropTypes.string
}
3.1.2 写法二:
class Header extends React.Component {
render () {
return (
<Title>Hello React Context API</Title>
);
}
}
//创建Context对象(<Provider /> 和 <Consumer />)
const ThemeContext = React.createContext({
background: 'red',
color: 'white'
});
class App extends React.Component {
render () {
return (
//生产者:value等价于getChildContext
<ThemeContext.Provider value={{background: 'green', color: 'white'}}>
<Header />
</ThemeContext.Provider>
);
}
}
class Title extends React.Component {
render () {
return (
//消费者:其children为一个函数(函数式组件)
<ThemeContext.Consumer>
{context => (
<h1 style={{background: context.background, color: context.color}}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
);
}
}
4. 总结
provide组件将store注入子组件,容器组件接管store,包括:view -> action, state -> view
。
参考文献

欢迎关注公众号,不定时更新哦~