我们先来看一下redux中暴露的api;
核心方法 ( [ ] 代表参数可选 )
- createStore(reducer, [preloadedState], [enhancer])
- compose(...functions)
- applyMiddleware(...middlewares)
- combineReducers(reducers)
- bindActionCreators(actionCreators, dispatch)
Store API
- getState()
- dispatch(action)
- subscribe(listener)
- getReducer()
- replaceReducer(nextReducer)
一:createStore(reducer, [preloadedState], [enhancer])
redux的设计基于flux思想,数据流为单向,改变state的唯一途径就是通过dipatch派发action
- 基于发布订阅模式,实现下代码
const createStore = (reducer, preloadedState, enhancer)=>{
/* 处理下 createStore(reducer, enhancer) 调用情况*/
if(typeof preloadedState === "function" && !enhancer){
enhancer = preloadedState;
preloadedState = undefined;
}
/* 存在中间件传参 */
if(enhancer && typeof enhancer === "function"){
/* 执行中间件逻辑,增强dispatch,返回新的store */
return enhancer(createStore)(reducer, preloadedState)
}
let state = preloadedState;
let listeners = [];
const getState = ()=> state;
const dispatch=(action)=>{
state = reducer(state,action);
/* 事件派发 */
listeners.forEach((fn)=>{
typeof fn === "function" && fn();
});
return action
}
const subscribe = (listener)=>{
/* 事件订阅 */
listeners.push(listener);
/* 返回取消订阅函数 */
return ()=>{
listeners = listeners.filter(fn=> fn !== listener);
}
}
const replaceReducer = (nextReducer)=>{
if(typeof nextReducer === "function"){
reducer = nextReducer;
dispatch({})
}
}
/* 手动初始state */
dispatch({});
return {
getState,
dispatch,
subscribe,
replaceReducer
}
}
上面的代码写完了,但需要再抠一些细节, 考虑下下面两个问题:
- dispatch方法使用时:代码state = reducer(state,action);如果reducer调用中又存在dispatch派发怎么办?
- dispatch中遍历执行fn(),如果fn执行时增加了新的订阅subscribe,或者取消了某个订阅又怎么办?
下面看下真实的redux源码中是怎么处理的
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function.'
)
}
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer
let currentState = preloadedState as S
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false
/**
* This makes a shallow copy of currentListeners so we can use
* nextListeners as a temporary list while dispatching.
*
* This prevents any bugs around consumers calling
* subscribe/unsubscribe in the middle of a dispatch.
*/
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
/**
* Reads the state tree managed by the store.
*
* @returns The current state tree of your application.
*/
function getState(): S {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState as S
}
/**
* Adds a change listener. It will be called any time an action is dispatched,
* and some part of the state tree may potentially have changed. You may then
* call `getState()` to read the current state tree inside the callback.
*
* You may call `dispatch()` from a change listener, with the following
* caveats:
*
* 1. The subscriptions are snapshotted just before every `dispatch()` call.
* If you subscribe or unsubscribe while the listeners are being invoked, this
* will not have any effect on the `dispatch()` that is currently in progress.
* However, the next `dispatch()` call, whether nested or not, will use a more
* recent snapshot of the subscription list.
*
* 2. The listener should not expect to see all state changes, as the state
* might have been updated multiple times during a nested `dispatch()` before
* the listener is called. It is, however, guaranteed that all subscribers
* registered before the `dispatch()` started will be called with the latest
* state by the time it exits.
*
* @param listener A callback to be invoked on every dispatch.
* @returns A function to remove this change listener.
*/
function subscribe(listener: () => void) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
/**
* Dispatches an action. It is the only way to trigger a state change.
*
* The `reducer` function, used to create the store, will be called with the
* current state tree and the given `action`. Its return value will
* be considered the **next** state of the tree, and the change listeners
* will be notified.
*
* The base implementation only supports plain object actions. If you want to
* dispatch a Promise, an Observable, a thunk, or something else, you need to
* wrap your store creating function into the corresponding middleware. For
* example, see the documentation for the `redux-thunk` package. Even the
* middleware will eventually dispatch plain object actions using this method.
*
* @param action A plain object representing “what changed”. It is
* a good idea to keep actions serializable so you can record and replay user
* sessions, or use the time travelling `redux-devtools`. An action must have
* a `type` property which may not be `undefined`. It is a good idea to use
* string constants for action types.
*
* @returns For convenience, the same action object you dispatched.
*
* Note that, if you use a custom middleware, it may wrap `dispatch()` to
* return something else (for example, a Promise you can await).
*/
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
/**
* Replaces the reducer currently used by the store to calculate the state.
*
* You might need this if your app implements code splitting and you want to
* load some of the reducers dynamically. You might also need this if you
* implement a hot reloading mechanism for Redux.
*
* @param nextReducer The reducer for the store to use instead.
* @returns The same store instance with a new reducer in place.
*/
function replaceReducer<NewState, NewActions extends A>(
nextReducer: Reducer<NewState, NewActions>
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
// TODO: do this more elegantly
;((currentReducer as unknown) as Reducer<
NewState,
NewActions
>) = nextReducer
// This action has a similar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE } as A)
// change the type of the store by casting it to the new store
return (store as unknown) as Store<
ExtendState<NewState, StateExt>,
NewActions,
StateExt,
Ext
> &
Ext
}
/**
* Interoperability point for observable/reactive libraries.
* @returns A minimal observable of state changes.
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
function observable() {
const outerSubscribe = subscribe
return {
/**
* The minimal observable subscription method.
* @param observer Any object that can be used as an observer.
* The observer object should have a `next` method.
* @returns An object with an `unsubscribe` method that can
* be used to unsubscribe the observable from the store, and prevent further
* emission of values from the observable.
*/
subscribe(observer: unknown) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
const observerAsObserver = observer as Observer<S>
if (observerAsObserver.next) {
observerAsObserver.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)
const store = ({
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
return store
}
上面源码中可以看到:
- 对于问题1:增加isDispatching字段控制,简称:加锁
- 代码片段(只截取核心部分,其他地方也有用到isDispatching)
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
- 对于问题2:增加了nextListeners字段
- 代码片段
/* 每次改变nextListeners前先进行拷贝 */
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
/* subscribe */
ensureCanMutateNextListeners()
nextListeners.push(listener)
/* unsubscribe */
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
/* 真正调用时 */
const listeners = (currentListeners = nextListeners);
// 就算listeners中存在方法,执行时进行订阅与取消订阅,我都用原来的listeners
额外: 看看下面的代码分别输出一样嘛
- 例1
var list = [0,1,2];
list.forEach((item)=>{
list.push(3);
console.log(item)
});
- 例2
var list = [0,1,2];
for(let i=0;i<list.length;i++){
if(i===0){
list.push(3)
}
console.log(list[i])
}
可以看到,有意思的现象,forEach中其实存在一个闭包function环境,
所以使用listeners.forEach(fn=> {fn()})
一定程度上避免了问题2的发生
二:compose(...functions)
compose是什么函数,compose可以理解为一个柯里化过程,将函数层层执行并最终返回
- 从右至左调用,比如: compose(f, g, h) 将会返回一个新函数: (...args) => f(g(h(...args))).
- 基于数组reduce方法实现下代码
const compose =(...funcs)=>{
if(funcs.length === 0){
return (arg)=> arg
}
if(funcs.length === 1){
return funcs[0]
}
return funcs.reduce((a,b)=> (...args)=> a(b(...args))); // 这句比较难理解,多读几遍
}
三:applyMiddleware(...middlewares)
最核心的中间件代码,看之前,我们先来看一端redux-chunk中间件源码:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => {
return next=>{
return action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
}
}
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
上面的redux-thunk 源码可以看到需要顶层提供dispatch, getState参数
- applyMiddleware实现
const applyMiddleware=(...middlewares)=>{
/* 下面是createStore中enhancer */
return (createStore)=>{
return (reducer, preloadedState)=>{
const store = createStore(reducer, preloadedState);
let dispatch = ()=>{}
const middlewareApi = {
getState: store.getState,
dispatch: (...args)=> { dispatch(...args) }
}
const chain = middlewares.map(middleware => middleware(middlewareApi));
dispatch = compose(...chain)(store.dispatch); // dispatch增强
return{
...store,
dispatch
}
}
}
}
- 思考:上面定义时dipatch只是()=> {},作为形参传入到redux-thunk中可以吗?
- 提示
四:combineReducers(reducers)
将{} 对象形式合成最终的大reducer函数形式
const combineReducers(reducers)=>{
return (state, action)=>{
return Object.keys(reducers).reduce((now,item)=>{
now[item] = reducers[item](state[item], action);
return now;
}, {})
}
}
五:bindActionCreators(actionCreators, dispatch)
一般用不到,在react-redux源码中mapDispatchToProps可以为对象时,使用bindActionCreators进行加工处理
const bindActionCreator = (actionCreator, dispatch)=>{
return (...args)=>{
return dispatch(actionCreator(...args))
}
}
const bindActionCreators=(actionCreators,dispatch)=>{
return Object.keys(actions).reduce((now,item)=>{
now[item]= bindActionCreator(actionCreators[item], dispatch)
// 等同 now[item]= (...args)=> dispatch(actionCreators[item](...args))
return now
},{})
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!