Redux 源码分析
Redux 目录结构
redux
├─ .babelrc.js
├─ .editorconfig
├─ .gitignore
├─ .prettierrc.json
├─ CHANGELOG.md
├─ CNAME
├─ CODE_OF_CONDUCT.md
├─ CONTRIBUTING.md
├─ LICENSE-logo.md
├─ LICENSE.md
├─ PATRONS.md
├─ README.md
├─ docs // 文档
├─ errors.json
├─ logo
├─ netlify.toml
├─ package-lock.json
├─ package.json
├─ rollup.config.js // rollup 打包配置
├─ scripts
├─ src // 源代码
│ ├─ applyMiddleware.ts
│ ├─ bindActionCreators.ts
│ ├─ combineReducers.ts
│ ├─ compose.ts
│ ├─ createStore.ts
│ ├─ index.ts
| └─ types
│ └─ utils // 一些工具方法
│ ├─ isPlainObject.ts
│ ├─ kindOf.ts
│ ├─ symbol-observable.ts
│ └─ warning.ts
├─ tsconfig.json
└─ website // redux 首页网站
主要对src目录下文件进行分析,对于utils中的方法,有意思的也可以看看
index.ts
这个主要导出一些对象或者做运行环境检测的,没有特殊功能
createStore.ts
对于一个redux 应用,整个应用的状态存储在一棵state 树中,由store 维护这个状态树,并通过dispatch 进行修改。
首先看看state 的数据结构
/types/store.ts
/*** 替换当前 store 用来计算最新 state 的 reducer 函数* * app 如果实现了 code splitting,那可能要动态加载 reducer。* 如果对 redux 要实现热重载,也可能要用到这个方法** @param nextReducer 返回新替换了 reducer 的 store*/replaceReducer<NewState, NewActions extends Action>(nextReducer: Reducer<NewState, NewActions>): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext/*** store 是一个维护 app 状态树的对象* 一个 redux app 只能有一个 store,拆分和组合只能发生在 reducer 层面中** @template S state 类型* @template action 类型* @template 来自 store enhancer 的 state 拓展* @template 来自 store enhancer 的 store 拓展*/
export interface Store<S = any,A extends Action = AnyAction,StateExt = never,Ext = {}
> {/*** dispatch action。触发 state 更新的唯一方法* * 创建 store 的时候要传入一个 reducer 函数,调 diapatch 函数的时候就会调那个 reducer 方法,* 并传入对应的 action* dispatch 方法会产生一个新的 state tree,且所有的监听者都会被通知* * 基础实现仅支持普通js对象的 action,如果想要 dispatch promise、observable、thunk* 或其他什么东西要使用 middleware。举例,可以去看 redux-thunk 包的文档。* 但是使用 middleware 之后最终也会使用这个方法 dispatch 一个普通js对象 action** @returns 图方便,返回传入的那个 actio*/dispatch: Dispatch<A>/*** 获取 store 维护的 state*/getState(): S/*** 添加一个变化监听者。* 任意 action 被 dispatch 的时候都会被调用,state 树的某个部分可能会被更新了。* 可以在传入的回调函数中调用 getState 来获取最新的 state 树。** @returns 返回一个取消订阅的方法*/subscribe(listener: () => void): Unsubscribe/*** 替换当前 store 用来计算最新 state 的 reducer 函数** 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.*/replaceReducer<NewState, NewActions extends Action>(nextReducer: Reducer<NewState, NewActions>): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext/*** 观察者/响应式库的互操作点。* * @returns {observable} 变化的最小 observable.* 详情可以查看 observable 提案:* https://github.com/tc39/proposal-observable*/[Symbol.observable](): Observable<S>
因此对于 createStore 方法就是要返回一个这样的数据结构:
/*** 创建一个 Redux store 来持有整个 state 树。* 唯一改变 store 中数据的方法是对它调用 `dispatch()`。** app 中应该只有单一的 store。为了弄清楚 state 树如何针对 state 树的不同部分进行响应,* 也可以使用 `combineReducers` 来将多个 reducer 组合到单一的 reducer 函数中去** @param reducer 一个返回下一个 state 树的函数,需要接收当前的 state 树和要处理的 action。** @param preloadedState 初始 state。* 你可以选择指定它以在通用 app 中从服务器还原状态,或还原以前序列化的用户会话。* 如果你使用 `combineReducers` 来生成根 reducer 函数,* 那么该函数必须是与 `combineReducers` 的键具有相同形状的对象。** @param enhancer store enhancer。 * 你可以选择指定它以使用第三方功能(如 middleware、时间旅行、持久性等)增强 store。* Redux附带的唯一 store enhancer 是 `applyMiddleware()`。** @returns 一个 redux store,让你可以读取 state,dispatch action,并订阅 state 变化*/
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 {// 做一些参数和运行环境的校验,省略// 如果 enhancer 是函数,则返回 enhancer 加强 store,省略let currentReducer = reducerlet currentState = preloadedState as Slet currentListeners: (() => void)[] | null = []let nextListeners = currentListenerslet isDispatching = false/*** 对 currentListeners 做一次浅拷贝,* 使得我们在 dispatch 过程中可以使用 nextListeners 作为临时的 list* * 这一步防止了任何 数据消费者 在 dispatch 过程中* 调用 subscribe/unsubscribe 出现的错误,*/function ensureCanMutateNextListeners() {if (nextListeners === currentListeners) {nextListeners = currentListeners.slice()}}/*** 读取 store 管理的 state 树。** @returns 当前 app 的 state 树*/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}/*** 添加一个 listener。在 action 被 dispatch 的时候,* 或 state tree 中的某些部分可能改变时被随时调用,* 你可以再回调函数中调用 `getState()` 来读取当前的 state * * 你可以从一个 listener 调用 `getState()`,但是伴随有以下警告:* * 1. 每个订阅都是在每个 `dispatch()` 调用之前的快照。* 如果你在 listener 正在被调用的时候 subscribe 或 unsubscribe,那么对于当前的 `dispatch()`* 流程来说根本没用。* 但是,下一次的 `dispatch()` 调用,无论是不是嵌套的调用,都会带上最新的 订阅 list 的快照。* * 2. listener 不应该盯着所有的 state 更改,因为在 listener 被调用之前 state 可能会* 在嵌套 `dispatch()` 过程中被多次更新。* 但是,可以保证在 `dispatch()` 启动之前注册的所有 listener 保证以最新状态调用。** @param listener 每次调用 dispatch 的时候都被触发的回调.* @returns 一个移除此 listener 的函数.*/function subscribe(listener: () => void) {if (typeof listener !== 'function') {throw new Error(`Expected the listener to be a function. Instead, received: '${kindOf(listener)}'`)}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/*** 对于 nextListeners 也用的是不可变更新方式,* 以免在正在 dispatch 的时候添加或者移出 listener 发生错误* 也就是说,只有在对应 action 被 dispatch 之前添加或者移除 listener 才有效*/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 = falseensureCanMutateNextListeners()const index = nextListeners.indexOf(listener)nextListeners.splice(index, 1)currentListeners = null}}/*** dispatche 一个 action。这是改变 state 的唯一方法。** 用来创建 store 的 `reducer` 函数,将会根据当前的 state 树和给定 action被调用。* 它的返回解雇将会被视作 **下一个** state,并且会通知 listener。* * 基本实现仅仅支持普通的 action 对象。日过想要 dispatch 一个 Promise,Observable,* thunk 等,你得使用对应的 middleware 封装 store 创建函数。例如,可以去看 * `redux-thunk` 包的文档。虽然 middleware 最终也是通过这个方法 dispatch 一个普通对象。** @param action 一个用来表示“发生什么”的普通对象。 * 这样 action 能被序列化,你就可以记录和重现用户的会话,或者使用 `redux-devtools 完成时间旅行调试。* 一个 action 必须有 `type` 属性,且不能为 `undefined`。* 使用字符串常量来定义这个属性是个好主意** @returns 为了方便,返回你 dispatch 的那个原对象** 注意到,如果你使用一个通用 middleware,他可能会封装 `dispatch()` 从而返回一些其他东西* (比如,返回一个 Promise 你能 await)。*/function dispatch(action: A) {if (!isPlainObject(action)) {throw new Error(`Actions must be plain objects. Instead, the actual type was: '${kindOf(action)}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`)}// action 必须拥有 type 字段if (typeof action.type === 'undefined') {throw new Error('Actions may not have an undefined "type" property. You may have misspelled an action type string constant.')}if (isDispatching) {throw new Error('Reducers may not dispatch actions.')}try {isDispatching = truecurrentState = currentReducer(currentState, action)} finally {isDispatching = false}// 依次通知 listenerconst listeners = (currentListeners = nextListeners)for (let i = 0; i < listeners.length; i++) {const listener = listeners[i]listener()}return action}/*** 替换当前 store 使用的 reducer 来计算 state。* * 可能你的 app 需要代码分割并动态加载一些 reducer,也可能要实现一些 redux 热重载机制** @param nextReducer 给 store 替换的那个 reducer* @returns 替换过 reducer 的同一个 store 实例*/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. Instead, received: '${kindOf(nextReducer)}`)}// TODO:现在的实现不够优雅;(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer// 这个 action 和 ActionTypes.INIT 效果一样// 新的和旧的 rootReducer 中存在的任何 Reducer 都将接收以前的状态。// 这将使用旧 state 树中的任何相关数据有效地填充新 state 树。dispatch({ type: ActionTypes.REPLACE } as A)// 通过强转类型为新 store 来改变 store 类型return store as unknown as Store<ExtendState<NewState, StateExt>,NewActions,StateExt,Ext> &Ext}/*** 观察式/响应式库的交互切点* @returns state 变化的最小可观察。* 有关更多信息,请参阅可观察提案:* https://github.com/tc39/proposal-observable*/function observable() {const outerSubscribe = subscribereturn {/*** 最小的 observable 订阅的方法* @param observer 可以被用作 observer 的任何对象* observer 对象都应该有 `next` 方法。* @returns 一个具有 `unsubscribe` 方法的对象,这个对象可以用来从 store 取消订阅 observable,* 并防止进一步从 observable 获得值*/subscribe(observer: unknown) {if (typeof observer !== 'object' || observer === null) {throw new TypeError(`Expected the observer to be an object. Instead, received: '${kindOf(observer)}'`)}function observeState() {const observerAsObserver = observer as Observer<S>if (observerAsObserver.next) {observerAsObserver.next(getState())}}observeState()const unsubscribe = outerSubscribe(observeState)return { unsubscribe }},[$$observable]() {return this}}}// 当 store 被初始化以后,一个 "INIT" action 就会被 dispatch,这样每个 reducer 返回他们的初始 state。// 这有效地填充了初始 state 树。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> & Extreturn store
}
bindActionCreators.ts
为了使用方便,redux 提供了一个根据 action 的 key 返回一个封装了 disptch 方法的函数bindActionCreators 。相当于不用手动dispatch 了。
这个方法不是必须的,手动调dispatch(createAction()) 也是可以的。
/*** 给单个 action 绑定 dispatch 函数* @param actionCreator 一个 action creator 函数* @param dispatch store.dispatch 方法* @returns 返回一个函数,这个函数直接调用就相当于 dispatch 一个对应的 action*/
function bindActionCreator<A extends AnyAction = AnyAction>(actionCreator: ActionCreator<A>,dispatch: Dispatch
) {return function (this: any, ...args: any[]) {return dispatch(actionCreator.apply(this, args))}
}
/*** 把一个值都是 action creator 的对象转化成另一个有着相同键的对象,* 但是每个函数都封装了一个 `dispatch` 调用进去,所以都能被直接调用。* 这是个简便方法,你可以自己调用 `store.dispatch(MyActionCreators.doSomething())`* * 为了方便,你也能传一个 action creator 进去作为第一个参数,* 返回值得到了一个 封装了 dispatch 的函数** @param actionCreators 一个值都是 action creator 函数的对象。* 一个简单的获得方法就是使用 ES6 语法 `import * as`,* 你也可以传单个函数。** @param dispatch Redux store 中可用的 `dispatch` 函数。** @returns 模仿原始对象的对象,但是每个 action creator 都封装进了 `dispatch` 调用。* 如果你传入一个函数比如 `actionCreators` ,返回值仍然是单个函数。*/
export default function bindActionCreators(actionCreators: ActionCreator<any> | ActionCreatorsMapObject,dispatch: 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, but instead received: '${kindOf(actionCreators)}'. ` +`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`)}const boundActionCreators: ActionCreatorsMapObject = {}for (const key in actionCreators) {const actionCreator = actionCreators[key]if (typeof actionCreator === 'function') {boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)}}return boundActionCreators
}
applyMiddleware.ts
顾名思义,就是将 middleware 应用到 store 上,来对 store 或者其他的一些东西进行增强。
/*** 创建一个 store enhancer 来将 middleware 应用到 redux store 的 dispatch 方法。* 这对于多种任务来说非常方便,例如,以简洁的方式表达异步操作,或记录每个 action payload。** 看 `redux-thunk` 包可以作为一个 redux middleware 的例子* 因为 middleware 可能是异步的,所以这应该是组合链中的第一个 store enhancer。** 注意每个 middleware 都会有 `dispatch` 和 `getState` 函数作为具名参数。** @param middlewares 需要应用的 middleware 链* @returns 一个应用 middleware 的 store enhancer** @template Ext middleware 添加的 dispatch 签名* @template S middleware 支持的 state 类型。*/
export default function applyMiddleware(...middlewares: Middleware[]
): StoreEnhancer<any> {return (createStore: StoreEnhancerStoreCreator) =><S, A extends AnyAction>(reducer: Reducer<S, A>,preloadedState?: PreloadedState<S>) => {const store = createStore(reducer, preloadedState)let dispatch: Dispatch = () => {throw new Error('Dispatching while constructing your middleware is not allowed. ' +'Other middleware would not be applied to this dispatch.')}const middlewareAPI: MiddlewareAPI = {getState: store.getState,dispatch: (action, ...args) => dispatch(action, ...args)}const chain = middlewares.map(middleware => middleware(middlewareAPI))/*** chain的结构: Array< (next: Dispatch<AnyAction>) => (action: AnyAction) => any >* compose 的作用:compose(A, B, C, arg) === A(B(C(arg))) 最后一个参数是 store.dispatch,* 使得 middleware 依次执行,最后执行 store.dispatch。 柯里化,串联* compose 传入的函数列表后,新生成的 dispatch 调用的时候相当于依次调用这些 middleware 后* 最后调用原生的 store.dispatch*/dispatch = compose<typeof dispatch>(...chain)(store.dispatch)return {...store,dispatch}}
}
applyMiddeware 方法调用后生成一个StoreEnhancer ,可以查看其类型定义:
/*** store enhancer 是一个高阶函数,将一个 store creator 组装成一个新的,增强过的* store creator。和 middleware 相似,以组合式方式更改 store。*/
export type StoreEnhancer<Ext = {}, StateExt = never> = (next: StoreEnhancerStoreCreator<Ext, StateExt>
) => StoreEnhancerStoreCreator<Ext, StateExt>/** 增强过的 storeCreator 类型 */
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <S = any,A extends Action = AnyAction
>(reducer: Reducer<S, A>,preloadedState?: PreloadedState<S>
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
因此,applyMiddleware 的使用方法大概是:
applyMiddleware(thunkMiddleware,middleware1,middleware2,...)({ reducer: ..., preloadedState: {} });
对于一个 middleware 来说,必须要实现 MiddlewareAPI,其类型定义如下:
export interface MiddlewareAPI<D extends Dispatch = Dispatch, S = any> {dispatch: DgetState(): S
}export interface Middleware<_DispatchExt = {}, // TODO: remove unused component (breaking change)S = any,D extends Dispatch = Dispatch
> {(api: MiddlewareAPI<D, S>): (next: D) => (action: D extends Dispatch<infer A> ? A : never) => any
以redux-thunk 的实现为例(https://github.com/reduxjs/redux-thunk/blob/master/src/index.ts),实现一个middleware 需要传入的dispatch 方法和getState 方法作为参数:
function createThunkMiddleware<State = any,BasicAction extends Action = AnyAction,ExtraThunkArg = undefined
>(extraArgument?: ExtraThunkArg) {// Standard Redux middleware definition pattern:// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middlewareconst middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =({ dispatch, getState }) =>next =>action => {// The thunk middleware looks for any functions that were passed to `store.dispatch`.// If this "action" is really a function, call it and return the result.if (typeof action === 'function') {// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"return action(dispatch, getState, extraArgument)}// Otherwise, pass the action down the middleware chain as usualreturn next(action)}return middleware
}
applyMiddleWare方法在生成强化后的dispatch方法的时候,用到了一个compose方法,该方法是将一个函数列表柯里化。
export default function compose(...funcs: Function[]) {if (funcs.length === 0) {// 推断参数类型,使其在推断中可用return <T>(arg: T) => arg}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((a, b) =>(...args: any) =>a(b(...args)))
}
这里使用柯里化主要是:1、代码可读性;2、易于增删中间件
贴一个 ChatGPT 的回答:

combineReducers.ts
combineReducers方法是在 Redux 库中提供的一种工具。它的作用是合并多个 reducer 函数,并将它们合并成一个单一的 reducer 函数,以便在 Redux 中管理多个不同的数据状态。好处是能够让你的应用更加模块化和结构化,并且更加容易维护和管理。
/*** 将多个 Reducer 函数整合为一个 Reducer,以便在 Redux 中管理多个不同的数据状态* 它将调用所有的 子 reducer,并且把调用结果汇集到一个 单一的 state 对象中,* 这个 state 对象的键就是传入的 reducer 函数名。** @template S 组合 state 对象的类型。** @param reducers 一个值对应的 reducer 函数要被合并为一个的对象。* 一个简便方法是使用 ES6 `import * as reducers` 语法来获取。* reducers 对于任何 action 可能都不会返回 undefined。* 相反的,他们需要返回自己的 初始 state。* 相反,如果传递给它们的 state 是 undefined,它们应该返回初始 state,* 而对于任何无法识别的操作,则返回当前 state。** @returns 一个 reducer 函数,它调用传递对象内的每个 reducer,* 并构建具有相同形状的 state 对象。*/
export default function combineReducers(reducers: ReducersMapObject) {// 所有 reducer 的 keyconst reducerKeys = Object.keys(reducers)// 最终生成的 reducer 对象const finalReducers: ReducersMapObject = {}for (let i = 0; i < reducerKeys.length; i++) {const key = reducerKeys[i]if (process.env.NODE_ENV !== 'production') {if (typeof reducers[key] === 'undefined') {warning(`No reducer provided for key "${key}"`)}}// 仅保留 reducer 对象中 值为 function 的键值对if (typeof reducers[key] === 'function') {finalReducers[key] = reducers[key]}}const finalReducerKeys = Object.keys(finalReducers)// 这用于确保我们不担心使用到相同的键。let unexpectedKeyCache: { [key: string]: true }if (process.env.NODE_ENV !== 'production') {unexpectedKeyCache = {}}/*** 校验 reducer 是否都符合规定,见 assertReducerShape 方法(后面介绍)* 1. 能不能接受 init 的 action* 2. 能不能处理未知的 action*/let shapeAssertionError: unknowntry {assertReducerShape(finalReducers)} catch (e) {shapeAssertionError = e}return function combination(state: StateFromReducersMapObject<typeof reducers> = {},action: AnyAction) {// 存在不符合规范的 reducer,直接抛出错误if (shapeAssertionError) {throw shapeAssertionError}if (process.env.NODE_ENV !== 'production') {// getUnexpectedStateShapeWarningMessage 只是一个生成 warning 消息的方法const warningMessage = getUnexpectedStateShapeWarningMessage(state,finalReducers,action,unexpectedKeyCache)if (warningMessage) {warning(warningMessage)}}let hasChanged = falseconst nextState: StateFromReducersMapObject<typeof reducers> = {}/*** 遍历所有的 reducer 并分别执行,将计算出的 state 组合起来生成一个大的 state* 因此对于任何 action,redux 都会遍历所有的 reducer*/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)if (typeof nextStateForKey === 'undefined') {const actionType = action && action.typethrow new Error(`When called with an action of type ${actionType ? `"${String(actionType)}"` : '(unknown type)'}, the slice reducer for key "${key}" returned undefined. ` +`To ignore an action, you must explicitly return the previous state. ` +`If you want this reducer to hold no value, you can return null instead of undefined.`)}nextState[key] = nextStateForKeyhasChanged = hasChanged || nextStateForKey !== previousStateForKey}hasChanged =hasChanged || finalReducerKeys.length !== Object.keys(state).lengthreturn hasChanged ? nextState : state}
}
assertReducerShape方法专门用来检查 reducer 是不是合法的,不合法则抛出错误:
-
试着使用 INIT Action 调用一下 Reducer,看是否能够得到一个初始状态
-
试着处理一个未知的 Action 类型
function assertReducerShape(reducers: ReducersMapObject) {Object.keys(reducers).forEach(key => {const reducer = reducers[key]const initialState = reducer(undefined, { type: ActionTypes.INIT })if (typeof initialState === 'undefined') {throw new Error(`The slice reducer for key "${key}" returned undefined during initialization. ` +`If the state passed to the reducer is undefined, you must ` +`explicitly return the initial state. The initial state may ` +`not be undefined. If you don't want to set a value for this reducer, ` +`you can use null instead of undefined.`)}if (typeof reducer(undefined, {type: ActionTypes.PROBE_UNKNOWN_ACTION()}) === 'undefined') {throw new Error(`The slice reducer for key "${key}" returned undefined when probed with a random type. ` +`Don't try to handle '${ActionTypes.INIT}' or other actions in "redux/*" ` +`namespace. They are considered private. Instead, you must return the ` +`current state for any unknown actions, unless it is undefined, ` +`in which case you must return the initial state, regardless of the ` +`action type. The initial state may not be undefined, but can be null.`)}})
}
其他——utils下一些方法

-
isPlainObject
/*** 通过 {} 或者 new Object() 方式创建的对象是纯粹对象* isPlainObject 函数的功能的判断依据与对象使用什么方式创建无关,而与的函数原型是否 === Object.prototype 有关*/
export default function isPlainObject(obj: any): boolean {if (typeof obj !== 'object' || obj === null) return falselet proto = obj// 当 proto === Object.prototype 时会跳出循环while (Object.getPrototypeOf(proto) !== null) {proto = Object.getPrototypeOf(proto)}// 判断原型链的 首是否等于尾return Object.getPrototypeOf(obj) === proto
}
-
kindOf返回变量或对象的类型
相关文章:
Redux 源码分析
Redux 目录结构 redux ├─ .babelrc.js ├─ .editorconfig ├─ .gitignore …...
第五十二章 BFS进阶(二)——双向广搜
第五十二章 BFS进阶(二)——双向广搜一、双向广搜1、优越之处2、实现逻辑3、复杂度分析二、例题1、问题2、分析3、代码一、双向广搜 1、优越之处 双向广搜是指我们从终点和起点同时开始搜索,当二者到达同一个中间状态的时候,即相…...
业务建模题
一. 单选题:1.在活动图中负责在一个活动节点执行完毕后切换到另一个节点的元素是( A)。A.控制流 B.对象流 C.判断节点 D.扩展区城2.以下说法错误的是(C)。A.活动图中的开始标记一般只有一一个,而终止标记可能有多个B.判断节点的出口条件必须保证不互相重复,并且不缺…...
电子秤专用模拟数字(AD)转换器芯片HX711介绍
HX711简介HX711是一款专为高精度电子秤而设计的24 位A/D 转换器芯片。与同类型其它芯片相比,该芯片集成了包括稳压电源、片内时钟振荡器等其它同类型芯片所需要的外围电路,具有集成度高、响应速度快、抗干扰性强等优点。降低了电子秤的整机成本ÿ…...
微服务 RocketMQ-延时消息 消息过滤 管控台搜索问题
~~微服务 RocketMQ-延时消息 消息过滤 管控台搜索问题~~ RocketMQ-延时消息实现延时消息RocketMQ-消息过滤Tag标签过滤SQL标签过滤管控台搜索问题RocketMQ-延时消息 给消息设置延时时间,到一定时间,消费者才能消费的到,中间件内部通过每秒钟扫…...
js发送邮件(node.js)
以前看别人博客留言或者评论文章时必须填写邮箱信息,感觉甚是麻烦。 后来才知道是为了在博主回复后让访客收到邮件,用心良苦。 于是我也在新增留言和文章评论的接口里,新增了给自己发送邮件提醒的功能。 我用的QQ邮箱,具体如下…...
English Learning - Day58 一周高频问题汇总 2023.2.12 周日
English Learning - Day58 一周高频问题汇总 2023.2.12 周日这周主要内容继续说说状语从句结果状语从句这周主要内容 DAY58【周日总结】 一周高频问题汇总 (打卡作业详见 Day59) 一近期主要讲了 一 01.主动脉修饰 以下是最常问到的知识点拓展ÿ…...
【微电网】基于风光储能和需求响应的微电网日前经济调度(Python代码实现)
目录 1 概述 2 知识点及数学模型 3 算例实现 3.1算例介绍 3.2风光参与的模型求解 3.3 风光和储能参与的模型求解 3.5 风光储能和需求响应都参与模型求解 3.6 结果分析对比 4 Python代码及算例数据 1 概述 近年来,微电网、清洁能源等已成为全球关注的热点…...
四种方式的MySQL安装
mysql安装常见的方法有四种序号 安装方式 说明1 yum\rpm简单、快速,不能定制参数2二进制 解压,简单配置就可使用 免安装 mysql-a.b.c-linux2.x-x86_64.tar.gz3源码编译 可以定制参数,安装时间长 mysql-a.b.c.tar.gz4源码制成rpm包 把源码制…...
软考高级信息系统项目管理师系列之九:项目范围管理
软考高级信息系统项目管理师系列之九:项目范围管理 一、范围管理输入、输出、工具和技术表二、范围管理概述三、规划范围管理四、收集需求1.收集需求:2.需求分类3.收集需求的工具与技术4.收集需求过程主要输出5.需求文件内容6.需求管理7.可跟踪性8.双向可跟踪性9.需求跟踪矩阵…...
【项目精选】javaEE健康管理系统(论文+开题报告+答辩PPT+源代码+数据库+讲解视频)
点击下载源码 javaEE健康管理系统主要功能包括:教师登录退出、教师饮食管理、教师健康日志、体检管理等等。本系统结构如下: (1)用户模块: 实现登录功能 实现用户登录的退出 实现用户注册 (2)教…...
ctfshow nodejs
web 334 大小写转换特殊字符绕过。 “ı”.toUpperCase() ‘I’,“ſ”.toUpperCase() ‘S’。 “K”.toLowerCase() ‘k’. payload: CTFſHOW 123456web 335 通过源码可知 eval(xxx),eval 中可以执行 js 代码,那么我们可以依此执行系…...
无线传感器原理及方法|重点理论知识|2021年19级|期末考试
Min-Max定位 【P63】 最小最大法的基本思想是依据未知节点到各锚节点的距离测量值及锚节点的坐标构造若干个边界框,即以参考节点为圆心,未知节点到该锚节点的距离测量值为半径所构成圆的外接矩形,计算外接矩形的质心为未知节点的估计坐标。 多边定位法的浮点运算量大,计算代…...
带你写出符合 Promise/A+ 规范 Promise 的源码
Promise是前端面试中的高频问题,如果你能根据PromiseA的规范,写出符合规范的源码,那么我想,对于面试中的Promise相关的问题,都能够给出比较完美的答案。 我的建议是,对照规范多写几次实现,也许…...
回流与重绘
触发回流与重绘条件👉回流当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为 回流。引起回流原因1.页面的首次渲染2.浏览器的窗口大小发生变化3.元素的内容发生变化4.元素的尺寸或者位置发生变化…...
openpyxl表格的简单实用
示例:创建简单的电子表格和条形图 在这个例子中,我们将从头开始创建一个工作表并添加一些数据,然后绘制它。我们还将探索一些有限的单元格样式和格式。 我们将在工作表上输入的数据如下: 首先,让我们加载 openpyxl 并创建一个新工作簿。并获取活动表。我们还将输入我们…...
【寒假day4】leetcode刷题
🌈一、选择题❤1.下列哪一个是析构函数的特征( )。A: 析构函数定义只能在类体内 B: 一个类中只能定义一个析构函数 C: 析构函数名与类名相同 D: 析构函数可以有一个或多个参数答案:B答案解析:析构函数是构造函…...
【竞赛题】6355. 统计公平数对的数目
题目: 给你一个下标从 0 开始、长度为 n 的整数数组 nums ,和两个整数 lower 和 upper ,返回 公平数对的数目 。 如果 (i, j) 数对满足以下情况,则认为它是一个 公平数对 : 0 < i < j < n,且 l…...
Redis集群搭建(主从、哨兵、分片)
1.单机安装Redis 首先需要安装Redis所需要的依赖: yum install -y gcc tcl然后将课前资料提供的Redis安装包上传到虚拟机的任意目录: 例如,我放到了/tmp目录: 解压缩: tar -xzf redis-6.2.4.tar.gz解压后࿱…...
Dart语法基础补充
Asynchrony support Dart 库中充满了返回 Future 或 Stream 对象的函数。 这些函数是异步的:它们在设置一个可能耗时的操作(例如 I/O)后返回,而不等待该操作完成。 async 和 await 关键字支持异步编程,让编写看起来类…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...
基于数字孪生的水厂可视化平台建设:架构与实践
分享大纲: 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年,数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段,基于数字孪生的水厂可视化平台的…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
