Redux讲解
程序员文章站
2022-07-02 23:45:14
...
Redux
- 对应用中多组件的状态进行集中式的管理(与vuex类似);
- redux并不是专门用于react中的库,本质上它是一个独立的第三方库,与任何库都是可以配合使用的,只是react配合使用最常见;
- 与react配置使用,经常会使用react-redux插件(链接react和redux,简化redux的使用)
- redux flow
- action是一个对象;
- reducers是一个函数:不能修改state,返回一个新的state;
- store将state,action和reducer联系在一起的对象;
redux的三个核心概念
-
action
- 标识要进行行为的对象;
- 包含两个方面的属性
- type: 标识属性, 值为字符串, 唯一, 必要属性
- xxx: 数据属性, 值类型任意, 可选属性
- 例子:
const action = { type: 'INCREMENT', number: 2 }
- Action Creator(Action的创建函数):
const incrementAction = (number) => ({type: 'INCREMENT', number})
-
reducer
- 1.处理action, 产生新的state的纯函数
- 2.样例
export default function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + action.number case 'DECREMENT': return state - action.number default: return state } }
- 3.注意
- 返回一个新的状态
- 不要修改原来的状态
-
3). store
- 1.将state,action与reducer联系在一起的对象
- 2.如何得到此对象?
import {createStore} from 'redux' import reducer from './reducers' const store = createStore(reducer)
- 3.此对象的功能?
- getState(): 得到state
- dispatch(action): 分发action, 触发reducer调用, 更新state
- subscribe(listener): 注册监听
- 内部维持应用的 state
Redux流程图
流程详述
- 整个项目的数据存储在Store中,每个状态下Store会生成一个state,一个state对应着一个view,而用户只能接触到view,不能接触到store;
- 所以,必须要通过view来间接改变,即用户点击,产生action,不能点击一次,创建一个action,所以需要一个action creator;
- 然后将这个action通过dispatch函数送到store中,这个过程中,可以使用一些中间件来处理一些异步操作;
- 然后将数据交给store,store拿到数据之后,通过reducer来根据不同的action的type来处理数据;
- 返回一个新的state,通过新的state,就可以再产生一个新的view了。 并且可以看到 store、view、action这样的一个单项数据流。
流程实现
- 如何创建store;
- import {creatStore} from ‘redux’;
- import reducer from ‘./reduces’;
- const store = creatStore(reducer);//调用reducer产生初始state;
- store的行为?
- getState();得到当前状态值;
- dispatch(action):分发action,内部调用reducer得到新state,更新state,调用监听回调;
- subscribe(callback):订阅更新的监听(重新渲染组件);
- store的内部数据
- reducer函数
- state
- reducer
- 它是一个函数
- 根据老state和action,产生一个新state;
- 不能直接修改老state
- action creator
- 用来创建action对象的函数;
- action={type:’XXX’,}
源码
- 为了更好地理解redux,我们可以读一下redux的源码。
- 首先,我们将redux源码得到,整体目录如下:
- 而redux源码的核心当然是处在src中的,dist、es、lib都不是最重要的,所以,我们展开src,可以看到下面的目录:
- 下面主要说一下整体:
- utils下的warning.js文件用于控制台错误日志输出,可以忽略。
- index.js 为入口文件,看源码时应该首先看这个文件。
- createStore是用于创建store的,所以是主流程,也是比较重要的一环。
- 其他四个文件 — applyMiddlewares.js、 bindActionCreators.js、combineReducers.js、compose.js这四个文件属于助攻型文件。
入口文件 index.js
import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
/*
* This is a dummy function to check if the function name has been altered by minification.
* If the function has been minified and NODE_ENV !== 'production', warn the user.
*/
function isCrushed() {}
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
warning(
'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
'This means that you are running a slower development build of Redux. ' +
'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
'or DefinePlugin for webpack (http://*.com/questions/30030031) ' +
'to ensure you have the correct code for your production build.'
)
}
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose
}
我们可以看到: 在index.js中,主要是从主流程文件、几个辅助api文件以及日志打印文件中获取了接口,然后中间是一些环境方面的警告,可以忽略,最后就通过这个index文件导出了所有我们在redux使用的过程中所需要的api,所以入口文件index.js就是起了一个桥梁了作用,非常简单,但是很重要。
主流程文件: createStore.js
- createStore.js主要是用于生成store的,我们还可以从最后暴露的对象看出其暴露了几个方法:
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
- 也就是说,这个主流程文件整个就是在定义了这么几个方法,下面看卡源码(被精简了,只留下重要部分):
// 这个action一定是会被最先触发的,从redux-devtools就可以看得出来。
export const ActionTypes = {
INIT: '@@redux/INIT'
}
// 接受三个参数
// 第一个参数是必须的,reducer,用来根据给定的状态和action来返回下一个状态树,进一步导致页面发生变化。
// 第二个参数是一个状态,这个不是必须的,大部分时候不需要。
// 第三个参数是enhancer,通常是一个中间件,比如使用redux-devtools这个中间件。
export default function createStore(reducer, preloadedState, enhancer) {
// 当前的reducer
let currentReducer = reducer
// 当前的状态。
let currentState = preloadedState
// 可以看出,这个是一个订阅者,state有变化,需要告诉这些Listeners。
let currentListeners = []
// 后续的listeners,是不断更新的。
let nextListeners = currentListeners
// 是否dispatch。
let isDispatching = false
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 获取当前的state树
function getState() {
return currentState
}
/**
* @param {Function} listener A callback to be invoked on every dispatch.
* @returns {Function} A function to remove this change listener.
*/
// 接收的是一个函数作为参数, 这个函数会在每一次dispatch的时候被调用。
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
// 取消订阅
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// 唯一触发state改变的方式。 dispatch a action.
// 这里是dispatch的一个基本的实现,只能提供一个普通的对象。 如果你希望dispatch一个Promise、thunk、obserbable等,你需要包装你的store
// 创建函数进入一个相应的中间件,如 redux-thunk。
// 这个action一定要包含type类型。最后也会返回这个action
function dispatch(action) {
// 如果正在dispatch,则抛出错误。
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// dispatch的过程中,每一个listener都会被调用。 因为在subscribe中传入的参数就是一个listener函数。
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// 替换一个新的reducer
function replaceReducer(nextReducer) {
currentReducer = nextReducer
dispatch({ type: ActionTypes.INIT })
}
// observable函数
function observable() {
const outerSubscribe = subscribe
return {
subscribe(observer) {
if (typeof observer !== 'object') {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
// store被创建,就会有一个INIT action被处罚,所以每个reducer就会返回他们的初始值了。
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
- getState方法非常简单,就是获取当前的store中的state,不用过多赘述。
- replaceReducer方法也很简单, 就是简单的替换reducer。
- 其中,subscribe用于注册监听事件,然后返回取消订阅的函数,把所有的订阅事件都放在了一个订阅数组里,只要维护这个数组就好了,subscribe的作用就是这么简单。
- 每次dispatch的时候就会依次调用数组中的监听事件。
- store.subscribe()方法总结:
- 入参函数放入监听队列;
- 返回取消订阅函数;
- 再看看dispatch方法,dispatch是触发state改变的唯一方式,最为核心的就是下面的这段代码了:
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()
}
- 这段代码中,首先,将isDispatching设置为了true,然后就调用currentReducer返回了一个新的currentState, 这样就成功了改变了当前的状态,最后, 改变了状态之后,就开始把subscribe函数中注册的事件开始以此执行。OK! 到这里,dispatch方法就比较清楚了。
-
所以,这里对dispatch方法做一个总结:
- 调用reducer, 传入参数(currentState, action)。
- 按顺序执行订阅的listener。
- 返回action。
ok! 至此,主流程文件就已经分析完了,整体还是比较简单的,比较重要的一个函数就是dispatch,但也是很好理解的。
下面,主要讲一讲剩下的几个辅助文件:
bindActionCreators.js
- bindActionCreators把action creators转成拥有同名keys的对象,使用dispatch把每个action creator包装起来,这样可以直接调用它们。
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
- 实际情况用到的并不多,惟一的应用场景是当你需要把action creator往下传到一个组件上,却不想让这个组件觉察到Redux的存在,而且不希望把Redux Store或dispatch传给它。
CombineReducers.js
- 这个文件中暴露的方法是我们常用的,因为在写reducer的时候,往往需要根据类别不同,写多个reducer,但是根据createStore可以知道,只有一个reducer可以被传入,所以这里的combineReducers就是为了将多个reducer合并成一个reducer的。具体源码如下(经过精简之后,就只剩下30多行了):
// 接受一个对象作为参数,这个对象的值是需要被combine的不同的reducer函数。
// 返回一个函数, 这个函数就是一个大的reducer了。
export default function combineReducers(reducers) {
// 获取reducer的所有的keys数组。
const reducerKeys = Object.keys(reducers)
// 最终的reducer对象。
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 将所有的reducer重新放在finalReducers中,相当于浅拷贝。
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
// 获取最终的所有的reducers数组。
const finalReducerKeys = Object.keys(finalReducers)
// 返回了一个函数,可以看出这个函数和我们一个一个定义的reducer函数是类似的,所以,这就是一个大的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]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
- 所以这个combineReducers还是很明确的,就是将所有的reducer组合成一个大的。
compose.js
- 这个函数用于组合传进来的一系列函数,在中间件的时候会用到,可以看到,执行的最终结果就是把一系列函数串联起来:
export default function 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.js
- 这个函数用于 store 增强。
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
- 用法大致如下:
const store = createStore(reducer,applyMiddleware(…middlewares))
or
const store = createStore(reducer,{},applyMiddleware(…middlewares))
- 比如一个比较常用的redux-thunk中间件,源码的关键代码如下:
function createThunkMiddleware(extraArgument) {
return function (_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function (next) {
return function (action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
- 作用的话可以看到,这里有个判断:如果当前action是个函数的话,return一个action执行,参数有dispatch和getState,否则返回给下个中间件。这种写法就拓展了中间件的用法,让action可以支持函数传递。即如果action是一个函数,那么我们就可以进一步来处理了,如果这个action是一个对象,说明就要直接又dispatch来触发了,即这里的action实际上是在真正的dispatch之前所做的一些工作。
其他
- 一般,我们认为redux属于函数式编程,即函数是第一等公民、数据是不可变的(在reducer中,我们希望每次返回一个新的state,而不是修改旧的state,然后返回,所以这里强调的就是不可变的)、有确定的输入就有确定的输出。 整体来说,可能redux不是纯纯的函数式编程,但是也比较符合函数式编程的风格了。
- 如下:
const arr = [1, 2, 3];
arr.push(4); //这样不好,看到这代码我就方了,需要从上往下琢磨一下arr到底存的是啥
const newArr = [...arr, 4]; //这样,arr不会被修改,很放心,要修改过的版本用newArr就好了
const me = {name: 'Morgan'};
me.skill = 'React'; //这样不好,拿不准me里是啥了
const newMe = {...me, skill: 'React'}; //这样,me不会被修改
上一篇: react系列(6)组件生命周期钩子函数
下一篇: 一文告诉你 Why React Hook
推荐阅读