随着一个多月来对React框架越来越多的使用,对React也有了一些自己的理解,今天就来梳理一下react当中的redux部分
react:
对于一些小型项目而言,React就完全能够满足需求,数据的传递和管理通过state、props就能够完成,但是随着业务的增加,我们数据的传递过程可能是这样的A --> B --> C --> D, 组件D需要A中的某个数据,A需要D相对应的回调,但是这些数据对于B,C而言是无用,如果这个时候单纯的使用React就需要一层层传递,逻辑更加复杂的同时也降低了代码的可读性,增加项目后期的维护成本。或者是兄弟组件之间共用父组件的一些数据,传递时也需要重复传递数据给每个子组件,写起来很是麻烦,可能除了自己以外的同事接过项目以后都想拿着菜刀去找的,所以这个时候就引入redux就很有必要了。
redux:
我只是想找一个地方存一下我的数据,大家有需要就拿,也都可以修改了就行了,那应该放在哪呢?
· 将状态统一放在一个state中,由store来管理这个state。
· 这个store按照reducer的“shape”(形状)创建。
· reducer的作用是接收到action后,输出一个新的状态,对应地更新store上的状态。
· 根据redux的原则指导,外部改变state的最佳方式是通过调用store的dispatch方法,触发一个action,这个action被对应的reducer处理,完成state更新。
· 可以通过subscribe在store上添加一个监听函数。每当调用dispatch方法时,会执行所有 的监听函数。
createStore:
export default function createStore(reducer, preloadedState, enhancer) {
// 如果第二个参数没有传入初始的state,而是传入了enhancer(applyMiddleware), 那就将第二个参数preloadedState赋值给enhancer
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 如果传入了enhancer,但enhancer不是一个函数,报错
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 此处意味着,如果createStore的时候传入了enhancer,那么在执行时会将createStore传入enhancer中,执行enhancer, 而enhancer的返回值也是一个函数。后面我们还会一起来看applyMiddleware的源码
return enhancer(createStore)(reducer, preloadedState)
}
// 如果没传入enhancer,就继续下面的逻辑
// reducer是要求为一个函数的,如果不是一个函数,报错
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
.....
.....
// 最后createStore就会返回dispatch,subscribe, getState等几个常用的api
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
};
}
//上面的源码就是封装了一些api,最后暴露给用户使用。
复制代码
getState: 用来获取store中的state的。因为redux为了防止小B犯错小C、小D背锅现象的情况出现,对于state的获取,是得通过getState的api来获取store内部的state。
function getState() {
// 为了确保用户每次都能拿到最新的state,这个地方需要做一次判断,如果isDispatching=true, 说明新的state正在计算中,就报错,反之,返回现在的state
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
}
复制代码
dispatch: 该函数是与getState对应的,getState是读,那dispatch就是写。redux中如果要改动state,只能通过发起一个dispatch,传递一个action给reducer,告诉reducer要改动的数据,reducer会根据action和currentState以及自己的内部实现逻辑,来计算出新的state,从而更新state。
function dispatch(action) {
// 这里的action要求是一个简单对象,而一个简单对象就是指通过对象字面量和new Object()创建的对象,如果不是就报错。
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// reducer内部是根据action的type属性来switch-case,决定怎么处理state的,所以type属性是必须的,如果没有type就报错。
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 如果是已经在dispatch的,避免连续两次更改state,就报错
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
// 这里就是计算新的state,并赋值给currentState
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// state更新了后,会将注册的回调都触发一遍。大家要注意这里,是都触发一遍哦!
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
复制代码
subscribe: redux提供的用户一个监听state变化的api,保证当state发生变化时通知到各个组件。
function subscribe(listener) {
// listener是state变化时的回调,必须是个函数
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 如果是正在dispatch中,就报错。因为要确保state变化时,监听器的队列也必须是最新的。所以监听器的注册要在计算新的state之前。
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-reference/store#subscribe(listener) for more details.'
)
}
// 标志有订阅的listener
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-reference/store#subscribe(listener) for more details.'
)
}
// 标志已经取消了
isSubscribed = false
//保存一下订阅快照
ensureCanMutateNextListeners()
// 删除
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
复制代码
combineReducers:这个函数的作用是用来整合多个reducer,在一个实际项目中,不同的reducer处理不同的state,我们当然也可以用一个reducer去处理所有的的state、,但是这样一个reducer中就会包含很多的判断逻辑,很容易产生混乱,不易维护,所以我们最好是将reducer分开写,但是createSrore只能接受一个reducer,所以当有多个reducer时,就需要整合成一个暴露出去。
export default function combineReducers(reducers) {
//先获取传入reducers对象的所有key
const reducerKeys = Object.keys(reducers)
const finalReducers = {} // 最后真正有效的reducer存在这里
//下面从reducers中筛选出有效的reducer
for(let i = 0; i < reducerKeys.length; i++){
const key = reducerKeys[i]
if(typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers);
//这里assertReducerShape函数做的事情是:
// 检查finalReducer中的reducer接受一个初始action或一个未知的action时,是否依旧能够返回有效的值。
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
//返回合并后的reducer
return function combination(state = {}, action) {
// hasChanged来标志是否计算出了新的state
let hasChanged = false
// 存储新的state
const nextState = {}
// 遍历每一个reducer,把action传进去,计算state
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
//得到该子reducer对应的旧状态
const previousStateForKey = state[key]
//调用子reducer得到新state
const nextStateForKey = reducer(previousStateForKey, action)
// 如果某个reducer没有返回新的state,就报错
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 此处来判断是否有新的state,如果所有的子reducer都不能处理action,那么就会返回旧的state,如果能处理就返回新的state
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 根据该标志,决定返回原来的state, 还是新的state
return hasChanged ? nextState : state
}
}
复制代码
applyMiddleware: redux中间件的作用是用来改造dispatch函数的,由于dispatch是一个纯函数,只能单纯的派发action,有时这样并不能满足我们的需求,比如我们需要打印出每次dispatch的action,或者我们需要处理一个异步的action,这个时候就需要用到像logger、redux-thunk、redux-saga之类的中间件来帮助我们完成这些操作,所以我认为对于redux的精准定义是一个用来加工dispatch的加工厂,要加工什么样的dispatch出来,则需要我们传入对应的中间件函数,先来看一下有无中间件的工作流程:
export default function applyMiddleware(...middlewares) {
//这里可以看到applyMiddleware是一个三级柯里化函数,第一个参数是middlewares数组((...middlewares),第二个是createStore,第三个是reducer(...args)
return createStore => (...args) => {
// 利用传入的createStore和reducer和创建一个store
const store = createStore(...args)
let dispatch = store.dispatch
//将store的getState方法和dispatch方法赋值给middlewareAPI
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
复制代码
我觉得redux最难理解的部分就是中间件这一部分,实际项目中用到的中间件不止一个,那么中间件是如何进行串联的呢?在这里我们先自己写一个中间件
const logger = store => next => action => {
console.log('dispatching', action)
return next(action)
}
复制代码
每一个中间件函数,都接收一个dispatch,然后将改造后的dispatch返回,作为next传递给下一个中间件函数,那么上面的部分
const chain = middlewares.map(middleware => middleware(middlewareAPI))
复制代码
也就能够理解了,再往下,看过的所有关于redux中间件解析的博客中差不多都会出现这么一句话:这一层只有一行代码,但却是 applyMiddleware 精华所在
dispatch = compose(...chain)(store.dispatch);,
复制代码
那么他到底精华在哪呢,我们一起看一下。上一行代码中,chan数组中包含了所有中间件处理过的函数(执行到了传入store的地步,那么接下来返回的函数就是等待dispatch的传入),通过compose函数对chin中的函数做处理,组装成一个新的函数,即新的dispatch,假设我们传入的chan=[f1,f2,f3],那么最终我们得到的dispatch是这样的
dispatch = f1(f2(f3(store.dispatch))));
复制代码
当处理完dispatch以后,我们会将最终的action传递给reducer中,计算出新的state。
总结:
网上讲解redux源码的文章很多,在写这篇文章的过程中我也看过很多其他前辈的文章,写这篇文章的主要目的是在看过源码以后梳理一下自己对这部分的理解,加深自己印象,也为了之后忘了可以快速重温。对于我而言比较难理解的部分是中间件的那一部分,前些日子觉得看懂源码以后就没有再去花时间在这上面,但这两天做项目的过程中发现自己对这部分的理解还是不够透彻,也就写了这篇文章。总的来说,redux的源码部分谈不上多复杂,但更多的是学习他的设计思想,这部分代码之所以简洁,我觉得大部分原因还是作者设计的好,而不是逻辑简单。多读读源码,我觉得对于自己编程思想的转变是很有帮助的,也能够帮助自己更好的理解这部分知识,从而在项目中更好的使用。