redux代码分析
1. 前言
2. redux最核心API
3. applyMiddleware
4. combineReducers
5. bindActionCreators
6. 学习点
redux代码分析
该函数构造了一个增强版的createStore。之所以叫做增强版,因为标准的createStore里,dispatch(action)
时,直接将currentState=currentReucer(currentState , action)
。即直接调用reducer函数,处理更新。但是利用applyMiddleware
之后,他便可以经过一系列中间件middlewares
处理。\
于是乎,怎样通过调用dispatch(action)
进入中间件????答案是:redux同过改造标准的dispatch方法来实现。
applyMiddleware
的代码并不多,但是理解起来,还是有点绕的。首先看他的调用方:createStore
。
return enhancer(createStore)(reducer, preloadedState)
带着这个前提,看看applyMiddleware
的返回部分:
return createStore => (...args) => {}
由此可见:createStore
传递到applyMiddleware
是一个标准的store创建函数。而applyMiddleware
里的 (...args)
部分,对应的就是(reducer, preloadedState)
这一部分了。
因此,我们可以说:enhancer(createStore)
返回的是一个增强版的createStore
。
我们再往里看看:
const store = createStore(...args)
构建了一个标准的store。
applyMiddleware
最关键的部分:
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// POS. A
// middlewares的函数结构:
// function m1 = store => next => action => {}
// 执行完后:
// chain里的middlewares只包含后面两个箭头,即:
// function c_m1 = next => action => {};
chain = middlewares.map(middleware => middleware(middlewareAPI))
// POS. B
// 将middlewares链组合起来生成新的一个dispatch
dispatch = compose(...chain)(store.dispatch)
代码中compose的作用将输入的middlewares链[m1, m2, m3],输出返回(...args) => m1(m2(m3(...args))
。因此,这一块是从最后一个开始调用执行的。
那么这里为什么compose(...chain)(store.dispatch)
之后就返回了一个新的dispatch呢?举个例子:
// 两个中间件。m1和m2
const m1 = store => next => action => {
console.log('i am m1 before');
next(action)
console.log('i am m1 after');
}
const m2 = store => next => action => {
console.log('i am m2 before');
next(action)
console.log('i am m2 after');
}
// 执行完POS. A之后变成
const c_m1 = next => action => {
console.log('i am m1 before');
next(action)
console.log('i am m1 after');
}
const c_m2 = next => action => {
console.log('i am m2 before');
next(action)
console.log('i am m2 after');
}
chain = [c_m1, c_m2];
// 执行完POS. B之后
dispatch = c_m1(c_m2(store.dispatch));
// 我们将等式右侧从里到外拆开。
// const dispatch1 = c_m2(store.dispach) = action => {
// console.log('i am m2 before');
// store.dispatch(action);
// console.log('i am m2 after');
// }
// 上面dispatch1的作用和标准的dispatch基本一样,
// 无非多了一个个性化的console.log('i am m2');
// 因此我们可以说dispatch1是标准dispatch的一个增强版。
// ---------------------------------------------------
// 带着dispatch1,继续执行c_m1(dispatch1)。这个时候如下:
// const dispatch2 = c_m1(dispatch1) = action => {
// console.log('i am m1 before');
// dispatch1(action);
// console.log('i am m1 after');
// }
// 从上述结果来看,dispatch2在dispatch1的基础上又增强了。
// 同时,我们也可以理解到参数next,其实表示的是next dispatch
// 的意思。
// ---------------------------------------------------
// 因此 c_m1(c_m2(store.dispatch))返回的是一个新的dispatch。
接下来,我们再看看用这个增强的dispatch发出一个action会怎么样:
disaptch({type: 'REQUEST_DATA'});
// 当发出一个action时,会先经过c_m1,
// 打出console.log('i am m1 before');之后,
// 再将同样的action传递给c_m2
这就解释了:dispatch是从中间件最右侧开始构建的,但是中间件调用的顺序还是从第一个来的。 (本人之前一直的疑惑)
关于问题:
1、关于这个函数还有一点要说的就是这个middlewareAPI:
// 为什么getState用的是标准的store.getState
// 为什么dispatch用的是新的,而不是store.dispatch
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
其实applyMiddleware的目的就是改造dispatch,那么如果只写使用store.dispatch
会有什么问题呢?
试想一下我们的多个中间件相当与下面这幅图(来自网上随便找的)。
嗯,类似于一个圈中圈娱乐城,最中心的就是我们标准的dispatch。试想当middlewareAPI
中的dispatch直接调用了store.dispatch
。那中间件m1里的store.dispatch
就不会经过这些next
了,而直接到达了中心,那么这一切的改造就没有了意义。
const m1 = store => next => action => {
console.log('i am m1');
store.dispatch({type: 'xxx'});
}
2、middlewareAPI
中的dispatch
为什么匿名包裹。
从middlewareAPI
的dispatch
的实现来看,他其实是一个闭包的匿名包裹。这样就可以实现,当下面dispatch被更新的时候,闭包里的disaptch页得到了相应的更新。如果用的是下面这种方式的话
const middlewareAPI = {
getState: store.getState,
dispatch
}
那么middlewareAPI.dispatch
将一直都dispatch的初始化,即代码中的:
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
为什么会有这个东西的存在呢?在业务中,我们经常会创建不同的reducer,比如权限相关的,路由相关的。而createStore
只接受一个reducer。那怎么办呢?这就有了combineReducers
,他将多个reducer合并撑了一个reducer,并返回和reducer同样参数的函数。
export default function combineReducers(reducers) {
// 将多个reducer按照key值合并
return function combination(state = {}, action) {
// 把action分发给每个reducer。
}
}
从返回上来看,其实combineReducers
返回的是一个大的reducer,也可以说combineReducers(reducers)
他是一个高阶的reducer。
他的主要工作是:
1、根据key值合并reducer。
2、返回一个新的reducer:combination。\
那么combination
到底是做什么的呢?
1、把action分发给所有的reducer。生成新的state。
2、如果有因为action导致状态改变的话,就返回nextState
,否则返回state
。
从上述,我们看到了一个不合理的地方,就是combination
把action分发给了所有的reducer。即使我们知道某个action是不可能发到其他reducer的。这样无疑造成了一定的性能损耗问题。因此,针对这个现象,也提出了解决方案:specialActions.
redux最核心的API:createStore,创建store对象,这个store对象并不是我们在用react-redux中的connect第一个参数mapStoreToState中的store。确切的说mapStoreToState中的store只是createStore中的store.getState返回的对象。
先来看看redux(4.0.0-beta.2)中的src目录
├── applyMiddleware.js // 中间件函数
├── bindActionCreators.js //
├── combineReducers.js // 合并reducer
├── compose.js
├── createStore.js // 创建store对象
├── index.js
└── utils
├── actionTypes.js
├── isPlainObject.js
└── warning.js
目录呈现的即使redux相关的API。
redux帮助我们创建了一个store,但是如何关联到react组件上?这里用到了另外一个库:react-redux
。
那么:我们就会有三个问题:
针对以上三个问题,我们需要了解react-redux
中的Provider
和connect
。当前react-redux
版本:4.4.5
react-redux
利用组件Provider
将store关联到react组件上。实际上利用的是react中的context
属性。将传给props的store属性赋值给了context,这样Provider
下的所有子组件通过context.store
方式都能访问到store。
Provider
的store
字段在4.4.5版本是写死的(5.0.7版本可自定义)。constructor(props, context) {
super(props, context)
this.store = props.store
}
// Right
// <Provider store={store}>
// ......
// </Provider>
// Wrong
// <Provider customStore={store}>
// ......
// </Provider>
Provider.prototype.render
Children.only
这个函数很少用到,作用是判断子节点只有一个,且返回子节点。render() {
return Children.only(this.props.children)
}
在调用react-redux
的connect
时,我们知道,第一个参数就是将store绑定到数组的属性上。那怎么做到的呢?这里我们采用倒推的方式。先看connect
返回render
里的两句话:
if (withRef) {
this.renderedElement = createElement(WrappedComponent, {
...this.mergedProps,
ref: 'wrappedInstance'
})
}
else {
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
}
react-redux
利用React.createElement
将mergedProps
绑定到组件上。回溯mergedProps
来源。
parentProps
stateProps
dispatchProps
。stateProps
第一次store关联来的stateProps
来源于configureFinalMapState
计算,之后都来源于computeStateProps
计算。
首先看看configureFinalMapState
,代码不多,直接贴出来:
configureFinalMapState(store, props) {
// mapState 来源于connect第一个参数mapStateToProps
const mappedState = mapState(store.getState(), props)
const isFactory = typeof mappedState === 'function'
// finalMapStateToProps:最终的mapStateToProp函数。
this.finalMapStateToProps = isFactory ? mappedState : mapState
this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1
if (isFactory) {
return this.computeStateProps(store, props)
}
return mappedState
}
mapStateToProps
函数返回函数的。即如下两种方法都是可行的。因为有了这个的条件,reselect才可以利用闭包的特性,减少一些不必要的计算。// right
const mapStateToProps = (state, props) => {
return {
auth: state.get('auth')
};
}
// right
const mapStateToProps = (state, props) => {
return (state, props) => {
return {
auth: state.get('auth')
}
};
}
因此,我们可以得出finalMapStateToProps
的意思表示最终的finalMapStateToProps
函数。当得到这个最终的函数之后,我们就可以计算他的stateProps
。
configureFinalMapState
有一个this.doStatePropsDependOnOwnProps
的赋值。意思表示的是,stateProps是以来props
(第二个参数)。方法是查看函数的形参个数:Function.length。this.finalMapStateToProps.length !== 1
dispatchProps
dispatchProps
的方式与stateProps
结构可以说是几乎完全一样。这里可以自己查看。
最后mergedPros将stateProps,dispatchProps和this.props合并,赋值给this.mergedProps
。默认采用简单的扩展:
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
上述反推倒过来之后结合render,就变成了如下:
第一次render
的时候会调用updateStatePropsIfNeeded
,这个时候回根据finalMapDispatchToProps
计算stateProps。同时也会调用updateDispatchPropsIfNeeded
,根据finalMapDispatchToProps
计算dispatchProps,最后将stateProps
、dispatchProps
和this.props
合并,生成this.mergedProps。最后利用React.createElement
将mergedProps
班谷底功能到组件上。
还记得redux中是如何调用监听时间的吗?
每次dispatch(action)
的时候,redux都会同过reducer计算出nextState
,然后强制调用nextListeners
里的所有listener。
那么上面的监听事件从何而来?
nextListeners
里的listener
,通过store.subscribe(listener)
,将要注册的事件push到nextListeners
中。
当redux dispatch(action)
之后,redux会调用所有的listener。那么react-redux需要做的事,就是把这个响应事件放到listener中。react-redux做法如下:
// 在组件挂载之后,注册handleChange事件,同时强制调用一次
componentDidMount() {
this.trySubscribe()
}
trySubscribe() {
if (shouldSubscribe && !this.unsubscribe) {
this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
this.handleChange()
}
}
这里解释下为什么在注册完监听事件之后,还要强制调用一下handleChange。
connect
返回的是一个包裹了目标组件的组件:CON_COMA = connect(COMA)
。因此他的声明周期顺序如下:
// constructor CON_COMA ---> 生成this.state.storeState
// render CON_COMA
// constructor COMA
// render COMA
// componentDidMount COMA ----> 阶段A
// componentDidMount CON_COMA ---> 阶段B
假如我们在【阶段A】dispatch(action)
更新了Store,那么这个时候的store会与阶段B中的this.state.storeState
不一致。因此我们需要强制调用handleChange
同步store
。
mapStateToProps
没传入的话,即使store改变,也不会造成render的重新渲染,因为handleChange没有被注册option.pure = false
,表示不需要计算他的stateProps,dispatchProps,this.props的改变,因为shouldComponentUpdate
都返回true。任何改变,都会重新调用render。这个函数的主要目的是方便。。。他将拥有actions的对象与dispatch绑定到了一起。这样调用的时候就不需要
import {requestData, successData} from 'actions.js';
store.dispatch(requestData())
store.dispatch(successData())
而直接是:
import getData from 'actions.js';
const bindedCreator = bindActionCreators(getData, store.dispatch)
bindedCreator.requestData()
bindedCreator.successData()
看起来很方便。但我很少用。。。
PS:作为一个点,虽然还没开始做同构:http://cn.redux.js.org/docs/api/bindActionCreators.html里的【小贴士】
说到redux最核心的API,不得不提createStore。
currentReducer
currentState
currentListeners
nextListeners
isDispatching
从变量上看,我们知道createStore工作:处理reducer、store、和响应事件之间的关系。那么我们接下来看看creatStore是怎么处理的。
变量中的nextListeners
和currentListeners
基本一样。不一样的是我们操作的永远是nextListeners
这个变量。可能有个疑惑:在dispatch函数中有const listeners = (currentListeners = nextListeners)
这么一句话,因为nextListeners
是数组,所以操作nextListeners
的时候,其实currentListeners
也相应发生了变化。其实不然:createStore
中有这么一个函数ensureCanMutateNextListeners
。具体实现方式如下:
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
通过array.prototype.slice
,currentListeners
拷贝给了nextListeners
。因此nextListeners
和currentListeners
只是数值一样,引用不一样的不同数组。
getState // 获取当前状态currentState
subscribe //订阅响应函数
dispatch
replaceReducer // 替换当前reducer
observable // 类似一个Observable类型,观察state的变化
不处在dispatch的时候,返回当前状态currentState
,如果是初始的时候,currentState
是reducer的初始值:preloadedState
。
1、重新设置currentReducer。
2、dispatch
替换通知:ActionTypes.REPLACE
不处在dispatch的时候,currentReducer
处理currentState
,得到新的状态。最后,强制调用nextListeners
里所有的listener
。
函数过程中涉及一些isDispatching
处理。
该函数返回的是一个轻量级的observable
对象。具体可参见:https://github.com/tc39/proposal-observable。他是TC委员会在2017年1月提出的提案。动机是为了解决异步数据流的问题。
注册监听事件,将监听事件入队到nextListeners
中
返回注销监听事件函数:事件从nextListeners
删除。用到函数:array.prototype.splice
。
每次调用createStore
,都会自动分发出一个type: ActionTypes.INIT
通知,从【2.2.3: dispatch】知道,这个动作是调用currentReducer,生成一个新的currentState状态,去初始化状态数:state tree。
utils里的isPlainObject写法:
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.