在react生态中redux和react-router扮演着极为重要的角色,那么今天我们就来聊聊react的得力好帮手redux,从“是什么”“怎么用“为什么”三个方面来揭开它的神秘面纱(非小白可以直接跳至第三部分“为什么”)。
我们来看一下官方定义:
Redux is a predictable state container for JavaScript apps.
我们来根据这句话划一下重点。首先,redux是为JavaScript服务的,换句话说redux不仅仅能用在react中,目前流行的主流框架vue、angular等,甚至原生的js中都可以使用,不得不说redux的作者野心真大。
其次,redux是一个可预测的状态容器,这里我们来说说什么叫状态容器,简单来说就是用来存放状态数据的公共仓库。就react而言,我们可以利用redux来存放需要共用的state数据,由此实现父子组件、兄弟组件之间的相互通信,解决react因为单向通信的限定,而不得不采用state状态提升等复杂的方法来实现多组件通信的问题。
知道了redux是什么,也大致了解了它在react中的用途,那么下面我们来通过简单的例子来看看redux在react中的基本使用。
首先是纯天然无污染的redux的使用,这里需要掌握三个最基本的概念:action、reducer和store。
俗话说,抛开代码给程序员讲知识就是耍流氓,所以我们通过下面的代码来说明它们怎么用。
const action = {
type: "ADD_TODO",
payload: 'Learn Redux'
};
上面这段代码就是一个action。可以看到action其实就是一个普通对象,其中必须包含type
属性(变化的标签),用来区分到底是哪个变化。其他属性就随你的心情自由设置(有社区里有个规范可以参考)。
但为了方便管理,通常我们会使用action creator去构造action,代码如下:
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: text
}
}
function onAddTodo(state = '', action) {
switch (action.type) {
case 'ADD_TODO':
state = action.paybload;
break;
}
return state;
}
reducer做了什么?首先它是一个函数,接受state和action两个参数,在此可对state设置默认值。然后在reducer函数体中,它通过switch-case来区分变化的标签,从而使得不同的变化发生有不同的行为。比如在这个reducer中,当ADD_TODO
变化发生时,就将state
变为action.paybload
的值。最后,返回更新之后的state
。
也是为了方便管理,通常我们会把多个reducer放在一个文件里面,然后用combineReducers
去整合成一个。
import { combineReducers } from 'redux';
//...省去多个reducer
export default combineReducers({
onAddTodo,
//...省去多个reducer
})
import { createStore } from 'redux';
let store = createStore(reducers);
上面这两句话中createStore
就把reducers安排进存储数据的变化调度中心,这样一切就准备就绪了,静候变化的到来。下面我们就通过subscribe
、getState
和dispatch
三个函数来分别监听变化、接收更新后state和发起变化。
//subscribe用来监听state的变化,并返回一个取消监听的函数
const unsubscribe = store.subscribe(()=>{
//getStore用来接收更新后的state
console.log(store.getState());
})
//dispatch用来发起变化
store.dispatch(addTodo('发起一个变化'));
//停止监听变化
unsubscribe();
到此,单纯的使用redux就介绍完了。下面我们要来介绍一位熟悉而陌生的朋友react-redux,顾名思义这个插件在react和redux之间构建起了一座坚固的桥梁,让它们的友谊长存。
react-redux有两个重要的部分,Provider和connect。
import { Provider } from 'react-redux';
import { render } from 'react-dom';
import { Component } from 'react';
class Main extends Component {
render() {
//省去创建store的代码
return (
{/*根组件*/}
)
}
}
export default render(
,
document.getElementById('root')
);
Provider就做了一件事情,将根组件包裹起来,并传入store以实现所有组件的数据共享。
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
function mapStateToProps(state, ownProps) {
return {
...state.need //仅绑定需要的state
}
}
function mapDispatchToProps(dispatch, ownProps) {
return bindActionCreators({
addTodo: action.addTodo,
//...
})
}
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
options
)(ChildrenApp)
connect是一个柯里化的函数,第一级接收四个参数,一般会使用前两个。mapStateToProps
是一个将state绑定到当前组件props上的函数,可接收两个参数state
、ownProps
,我们可以根据 state
中的数据,动态地输出组件需要的(最小)属性;mapDispatchToProps
是一个将action绑定到当前组件props上的函数,也可接收两个参数dispacth
、ownProps
。connect的第二级参数是传入当前组件,用来绑定state、action到当前组件上。
到此为止,我们就简要的介绍了redux及react-redux的用法(异步情况的处理,请自行阅读redux官网中间件内容),我们通过下面的数据流图来回顾一个整个过程。
)
通过上文介绍,想必大家对redux和react-redux有了初步的印象,那么下面我们从源码的入手来看看他们底层到底都干了些什么(以下源码都做了适当的简化)。
在redux的源码中,我们着重分析createStore.js
和combineReducers.js
这两个文件。
export default function createStore(reducer, preloadedState, enhancer) {
//......
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
createStore用来创建整个应用唯一的store以及初始化整个store状态树的数据,接收了三个参数,返回了一些执行方法。
function dispatch(action) {
try {
isDispatching = true
currentState = currentReducer(currentState, action) // 执行reducer
} finally {
isDispatching = false
}
//执行完reducer后通知监听器队列, 在执行监听器前执行同步监听器队列副本到当前监听器队列
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
dispatch函数主要是将action发送到reducer中,并执行reducer,将生成的状态树更新到currentState,最后依次通知监听器队列。
function subscribe(listener) {
//使用闭包变量表示当前监听器是否被订阅,在unsubscribe函数机取消订阅时使用,防止重复执行取消订阅
let isSubscribed = true
// 更改监听器队列前,创建当前监听器副本
ensureCanMutateNextListeners()
// 新增监听器到当前监听器副本
nextListeners.push(listener)
// 返回取消订阅函数
return function unsubscribe() {
// 如果已经取消订阅,立即退出
if (!isSubscribed) {
return
}
//...
//取消订阅后,将标志位置为false
isSubscribed = false
// 同步当前标志位副本,删除当前监听器
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
subscribe方法增加监听器函数到监听器队列中,并返回取消订阅函数。
function getState() {
return currentState
}
getState方法返回当前应用store存储的state。
export default function combineReducers(reducers) {
// 获取reducers的每一个属性的key
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 将处理state每一个部分的reducer加入finalReducers对象中
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
// 返回合成后的 reducer
return function combination(state = {}, action) {
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key] // 获取当前子 state
const nextStateForKey = reducer(previousStateForKey, action) // 执行各子 reducer 获取子 nextState
// 如果reducer返回未定义的值抛出错误
nextState[key] = nextStateForKey // 将子 nextState 挂载到对应的键名
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
将传入的reducer打包存储finalReducers
中,并返回合成之后的reducercombination
函数。在combination
函数执行时,依次遍历finalReducers
,获取更新之后的state
,最后返回state状态树。
react-redux有两个重要的方法,provider和connect。首先我们来看一下react-redux源码的目录结构:
柿子还得挑软的捏。我们从先从最简单的入手——Provider。
export function createProvider(storeKey = 'store') {
class Provider extends Component {
getChildContext() {
return { [storeKey]: this[storeKey] }
}
constructor(props, context) {
super(props, context)
this[storeKey] = props.store;
}
render() {
return Children.only(this.props.children)
}
}
return Provider
}
export default createProvider()
Provider本质上是一个react组件,它实际上就干了一件事,通过context
属性,将store
直接传递给子孙组件,相当于将store
定义成了全局变量。
对于connect我们不急着看源码(太复杂了,会疯掉),我们先来看一张图:
上图就将connect柯里化的流程一步一步拆解出来,最后我们发现当我们调用connect函数时,实际上调用了hoistStatics
这个函数。那么什么是hoistStatics
?简单来说,是将WrappedComponent
中的非React特定的静态属性(例如propTypes
就是React的特定静态属性)赋值到Connect
。作用有点类似于Object.assign
,但是仅复制非React特定的静态属性。
知道了最终返回的结果,我们下面带着几个问题来分析connect最核心的部分connectAdvanced.js。
connect是如何获取store并将state、action传递给props的?
store的subscribe
方法是在哪里监听的?
store的getState
方法又是在哪里执行的?
根据上面三个问题,我们模拟出connectAdvanced.js最简化的理想代码,如下:
export default function connectAdvanced(){
const contextTypes = {
store: storeShape
}
return function wrapWithConnect(WrappedComponent){
class Connect extends Component {
constructor(props, context) {
super(props, context);
//从祖先Component处获得store
this.store = props.store || context.store;
this.stateProps = computeStateProps(this.store, props)
this.dispatchProps = computeDispatchProps(this.store, props)
this.state = { storeState: null }
//对stateProps、dispatchProps、parentProps进行合并
this.updateState()
}
shouldComponentUpdate(nextProps, nextState) {
//进行判断,当数据发生改变时,Component重新渲染
if(propsChanged || mapStateProducedChange || dispatchPropsChanged) {
this.updateState(nextProps)
return true
}
}
componentDidMount() {
//改变Component的state
this.store.subscribe(()=>{
this.setState({
storeState: this.store.getState()
})
})
}
render() {
//生成包裹组件Connect
return (
this.nextState} />
)
}
}
Connect.contextTypes = contextTypes;
return Connect;
}
}
其实connect的主要逻辑就是这样,通过context获取store
,并将store
中的state
和action
传递到子组件的props
,然后在生命周期中去监听store中的state
有没有发生变化,如过有,就重新渲染组件,函数的最后封装出一个高阶组件。而源码中那些复杂的部分不过是为了保证功能的稳定和对性能的优化,如果有兴趣想深入研究react-redux,可以阅读庖丁解牛React-Redux(一): connectAdvanced这篇文章,里面对connect中的重点函数都做了比较详细的讲解。
按照惯例,我们还是通过一张图来回顾一下redux、react-redux是如何工作的。