从源代码的入口文件发现,其实 redux 最终就只是导出了一个对象,对象中有几个方法,代码如下:
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
所以重点分析几个方法:
createStore 方法
方法中定义的一些变量:
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
这些变量会被 dispatch
或者别的方法引用,从而形成闭包。这些变量不会被释放。
创建 srore
的方法最终返回的是一个对象。对象中含有比较重要的方法dispatch,subscribe,getState
。
return {
dispatch,
subscribe,
getState,
replaceReducer,
[?observable]: observable
}
其中 createStore
的第三个参数是应用中间件来做一些增强操作的。
if (typeof enhancer !== 'undefined') { // 如果增强方法存在就对 createStore 进行增强
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
subscribe 方法
其中 subscribe
用来注册监听方法,每一次注册后会将监听方法维护到数组currentListeners
中,currentListeners
是 createStore
中的一个变量,由于被 subscribe
引用着所以形成了一个闭包。也就是通过闭包来维护状态。
let currentListeners = []
dispatch 方法
dispatch
方法用来分发 action
, 函数里面会生成新的 currentState
, 会执行所有注册了的函数。
核心代码:
try {
isDispatching = true
currentState = currentReducer(currentState, action) // 生成新的 state
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
} // 遍历执行注册函数
getState
仅仅用来获得当前的 state
:
function getState() {
return currentState
}
combineReducers 函数
函数中定义的一些变量,
const finalReducers = {}
const finalReducerKeys = Object.keys(finalReducers)
这个函数最后返回的是一个函数 combination
, 返回的函数中引用了 finalReducers
和 finalReducerKeys
,形成了闭包。
出于业务场景考虑,不同的模块采用不同的 reducer
进行处理,所以 reducer
函数有很多。这些 reducer
会遍历执行。
每一次 dispatch
一个 action
的时候就会执行
currentState = currentReducer(currentState, action) // 生成新的 state
这里的 currentReducer
就是返回的 combination
函数。combination
函数中的核心代码:
function combination(state = {}, action) {
...
let hasChanged = false
// 每一次 reducer 执行的时候都会生成一个新的对象来作为新的 state
const nextState = {}
// 通过 for 循环遍历 reducer
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
// 获取当前的 state
const previousStateForKey = state[key]
// 执行相应的 reducer 后会生成新的 state
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
// 给新的 state 赋值
nextState[key] = nextStateForKey
// 如果是一个简单类型比如 string,number
// 如果前后值一样就不会触发改变
// 但如果 state 中某个值是一个对象,
// 尽管前后对象中的值一样,但是引用地址变化,还是会触发改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 所以如果简单值没有变化并且没有对象的引用地址改变就会返回原来的 state
return hasChanged ? nextState : state
}
结合 react-redux
中向 redux
订阅的方法发现
subscribe() {
const { store } = this.props // 这里的 store 是 createStore 方法执行后返回的对象
this.unsubscribe = store.subscribe(() => { // 通过订阅方法注册监听事件
const newStoreState = store.getState() // 获取新的 state
if (!this._isMounted) {
return
}
// 通过使用函数替代对象传入 setState 的方式能够得到组件的 state 和 props 属性可靠的值。
this.setState(providerState => {
// 如果值是一样的就不会触发更新
if (providerState.storeState === newStoreState) {
return null
}
return { storeState: newStoreState }
})
})
// Actions might have been dispatched between render and mount - handle those
const postMountStoreState = store.getState()
if (postMountStoreState !== this.state.storeState) {
this.setState({ storeState: postMountStoreState })
}
}
在注册的 listen
方法中会发现如果最 新的state和原来的state一样
就不会触发 setState
方法的执行,从而就不会触发 render
。
applyMiddleware 使用中间件
源码:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => { // 接收 createStore 函数作为参数
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
} // 中间件函数接收的 API 参数,能够获取到当前的 state 和 createStore 函数的参数
// 所以这里就向中间件函数中传递了参数
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 通过函数组合生成一个新的 dispatch 函数
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
} // 这里返回的是最后生成的 store,相比不使用中间件的区别是对 dispatch 进行了增强。
}
}
结合 createStore
中的源码:
return enhancer(createStore)(reducer, preloadedState)
所以上面 applyMiddleware
中返回的函数就是这里的 enhancer
方法,接收 createStore
作为参数。
(reducer, preloadedState)
对应着中间件中的 (...args)
。
react-redux
react-redux
通过提供 Provider
组件将 store
和整个应用中的组件联系起来。确保整个组件都可以获得 store
, 这是通过 Context
来实现的。
Provider
组件最终渲染的组件:
render() {
const Context = this.props.context || ReactReduxContext
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
其中 state
的定义如下:
const { store } = props
this.state = {
storeState: store.getState(),
store
}
所以 Provider
给应用提供 store
的写法如下,属性名必须是 store
。
<Provider store={store}>
<Router />
</Provider>
redux-thunk
redux-thunk
是一个中间件,直接看中间件的源代码是绝对不可能看明白的
。
中间件不是一个完整的个体。它是为了丰富或者扩展某个模块而出现的,其中会调用一些原来的模块的方法,所以如果不看源模块的对应的方法实现,根本无法理解。
所以要想看懂一个中间件,必须结合源模块的代码一起看。
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
如何让 dispatch 分发一个函数,也就是 action creator??
dispatch
的参数只能是一个普通的对象,如果要让参数是一个函数,需要使用中间件 redux-thunk
。
设计思想就是一种面向切面编程AOP,对函数行为的增强,也是装饰模式的使用。
redux-thunk
源码:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
如果只是用了 thunk
,那么最终增强版的 dispatch
就是
action => {
// 当 dispatch 参数是一个函数的时候执行这里
if (typeof action === 'function') {
// 这里的 dispatch 就是最原始的 dispatch
// 所以 action 函数中可以直接使用参数 dispatch 和 getState 函数
return action(dispatch, getState, extraArgument);
}
return next(action); // 这里的 next 是 store.dispatch
}
异步操作带代码
异步操作如果使用 action creator
, 则至少要送出两个 Action:
- 用户触发第一个 Action,这个跟同步操作一样,没有问题;
- 在
action creator
函数中送出第二个Action
代码实例:
handleClick = () => {
const { dispatch } = this.props
dispatch(this.action); // 发出第一个 action(函数)
}
action = (dispatch, getState) => setTimeout(() => {
dispatch({ type: 'REQUESTSTART' })
}, 1000) // 发出第二个 action(普通对象)
思考
异步代码的处理一定要使用 redux-thunk
吗?
非也。在触发含有异步代码的函数执行时,把 dispatch
函数作为一个参数传给函数,然后这个异步函数里面在合适的时机调用 dispatch
发出 action
就行。
上面的异步代码可改写如下:
handleClick = () => {
const { dispatch } = this.props
this.action(dispatch);
}
action = dispatch => setTimeout(() => {
dispatch({ type: 'REQUESTSTART' })
}, 1000)
不过相比 redux-thunk
有个缺陷就是不能获取 getState
这个方法。