摘要:函數(shù)用于為應(yīng)用的狀態(tài)添加監(jiān)聽,以一個(gè)回調(diào)函數(shù)監(jiān)聽函數(shù)為參數(shù)當(dāng)觸發(fā)時(shí)執(zhí)行所有監(jiān)聽函數(shù),返回能夠取消訂閱的函數(shù)。函數(shù)用于組合多個(gè)中間件返回的函數(shù),其實(shí)質(zhì)是生成新的暴露給用戶。
redux 源碼閱讀筆記 redux 中的 reducer 為什么要叫這個(gè)名字?(UPDATED)
筆者在學(xué)習(xí) redux 的時(shí)候一直感覺 reducer 很不好理解(非要進(jìn)行翻譯的話,可以稱之為縮減器/折疊器),而像 action 和 store 這兩個(gè)概念就比較清晰,直接按照字面意思理解即可,在百思不得其解時(shí)發(fā)現(xiàn)官方文檔中專門有一句對 reducer 命名的解釋:
It"s called a reducer because it"s the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue).
這句話翻譯過來應(yīng)該是:之所以被稱為 reducer,是因?yàn)樵摵瘮?shù)與被傳入 Array.prototype.reduce 方法的參數(shù)(回調(diào)函數(shù))是一種類型的。
我們先來看一個(gè) reduce 方法的例子:
[0, 1, 2, 3, 4].reduce((prev, curr) => prev + curr) // 10
這與 redux 中的 reducer 模型((prevState, action) => newState)看起來是不是十分相似呢!
reducer 這個(gè)術(shù)語其實(shí)來自于函數(shù)式編程,在很多情況下 reduce 操作也被稱為 Fold 折疊,它屬于一種高階函數(shù),可以將其參數(shù)(回調(diào)函數(shù)/reducer)遞歸應(yīng)用到數(shù)組的所有元素上并返回一個(gè)獨(dú)立的值,這也就是“縮減”或“折疊”的含義所在。
正文如下:
src ├── applyMiddleware.js ├── bindActionCreators.js ├── combineReducers.js ├── compose.js ├── createStore.js ├── index.js └── utils ├── actionTypes.js ├── isPlainObject.js └── warning.jsindex.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" import __DO_NOT_USE__ActionTypes from "./utils/actionTypes" /* * 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 setting mode to production in webpack (https://webpack.js.org/concepts/mode/) " + "to ensure you have the correct code for your production build." ) } export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }
這里聲明的 isCrushed 函數(shù)并不具有實(shí)際意義,僅用來判斷在生產(chǎn)環(huán)境下 redux 項(xiàng)目是否被壓縮,如果有被壓縮則給開發(fā)人員 log 一個(gè)警告提示。
createStore.jscreateStore 函數(shù)是 redux 最核心的 API,它創(chuàng)建一個(gè) store 存放應(yīng)用的整課 state 樹,并且一個(gè)應(yīng)用中僅能存在一個(gè) store,我們只能通過調(diào)用 dispatch 的方法來對 store 中的數(shù)據(jù)作出更改。
// createStore.js import $$observable from "symbol-observable" import ActionTypes from "./utils/actionTypes" import isPlainObject from "./utils/isPlainObject" /** * Creates a Redux store that holds the state tree. * The only way to change the data in the store is to call `dispatch()` on it. * * There should only be a single store in your app. To specify how different * parts of the state tree respond to actions, you may combine several reducers * into a single reducer function by using `combineReducers`. * * @param {Function} reducer A function that returns the next state tree, given * the current state tree and the action to handle. * * @param {any} [preloadedState] The initial state. You may optionally specify it * to hydrate the state from the server in universal apps, or to restore a * previously serialized user session. * If you use `combineReducers` to produce the root reducer function, this must be * an object with the same shape as `combineReducers` keys. * * @param {Function} [enhancer] The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ export default function createStore(reducer, preloadedState, enhancer) { // 兼容函數(shù)參數(shù)不同的情況:應(yīng)用不存在初始狀態(tài),第二個(gè)參數(shù)即傳入增強(qiáng)器函數(shù)的情況 if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState 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) } if (typeof reducer !== "function") { throw new Error("Expected the reducer to be a function.") } let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false // 確保 nextListeners 和 currentListeners 具有不同的引用 function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } function getState() {/* ... */} function subscribe() {/* ... */} function dispatch() {/* ... */} function replaceReducer() {/* ... */} function observable() {/* ... */} // 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 }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
createStore 函數(shù)接受三個(gè)參數(shù):
reducer:通過 state 和 action 計(jì)算 nextState 的函數(shù)
preloadedState:初始狀態(tài)(需要注意的是如果使用 combineReducers 來生成 reducer,那么必須保證 preloadedState 和 combineReducers 數(shù)據(jù)結(jié)構(gòu)中的鍵相對應(yīng))
enhancer:store 的增強(qiáng)器函數(shù),可以指定為第三方的中間件/時(shí)間旅行/持久化等(需要注意的是該函數(shù)只能用 redux 提供的 applyMiddleware 函數(shù)來生成)
getState/** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */ function getState() { 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 }
getState 函數(shù)可以用于獲取應(yīng)用當(dāng)前的狀態(tài),可以看到這里使用閉包來持久變量,并且所有改變狀態(tài)的行為都是改變 currentState 的引用。
subscribesubscribe 函數(shù)用于為應(yīng)用的狀態(tài)添加監(jiān)聽,以一個(gè)回調(diào)函數(shù)(監(jiān)聽函數(shù))為參數(shù)(當(dāng)觸發(fā) dispatch 時(shí)執(zhí)行所有監(jiān)聽函數(shù)),返回能夠取消訂閱的 unsubscribe 函數(shù)。
/** * 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 {Function} listener A callback to be invoked on every dispatch. * @returns {Function} A function to remove this change listener. */ function subscribe(listener) { 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-reference/store#subscribe(listener) for more details." ) } let isSubscribed = true // 保持監(jiān)聽序列的不變性 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) } }dispatch
dispatch 函數(shù)是改變應(yīng)用狀態(tài)的唯一方法,接受一個(gè) action 作為參數(shù),通過調(diào)用 reducer 計(jì)算生成一個(gè)新的狀態(tài)。
/** * 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 {Object} 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 {Object} 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) { // 基礎(chǔ)版本的實(shí)現(xiàn)僅支持 action 為樸素對象的情況 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 } // 觸發(fā) dispatch 時(shí)執(zhí)行所有監(jiān)聽函數(shù) const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }replaceReducer
/** * 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 {Function} nextReducer The reducer for the store to use instead. * @returns {void} */ function replaceReducer(nextReducer) { if (typeof nextReducer !== "function") { throw new Error("Expected the nextReducer to be a function.") } currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) }observable
observable 用于提供另外一種觀察者模式的交互方式。
/** * Interoperability point for observable/reactive libraries. * @returns {observable} 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 {Object} observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns {subscription} 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) { if (typeof observer !== "object" || observer === null) { 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 } } }compose.js
/** * Composes single-argument functions from right to left. The rightmost * function can take multiple arguments as it provides the signature for * the resulting composite function. * * @param {...Function} funcs The functions to compose. * @returns {Function} A function obtained by composing the argument functions * from right to left. For example, compose(f, g, h) is identical to doing * (...args) => f(g(h(...args))). */ 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))) }
compose 函數(shù)的設(shè)計(jì)十分巧妙,從右到左把接受到的函數(shù)組合,然后返回一個(gè)最終函數(shù),其結(jié)果也并不復(fù)雜,旨在讓開發(fā)人員能夠在不寫深度右括號的情況下來寫深度嵌套的函數(shù)。
const fn1 = val => `(fn1: ${val})` const fn2 = val => `(fn2: ${val})` const fn3 = val => `(fn3: ${val})` console.log(compose(fn1,fn2,fn3)("Hello World!")); // (fn1: (fn2: (fn3: Hello World!)))applyMiddleware.js
applyMiddleware 函數(shù)用于組合多個(gè)中間件返回的 enhancer 函數(shù),其實(shí)質(zhì)是生成新的 dispatch 暴露給用戶。
在認(rèn)識 applyMiddleware 之前,我們先來看看中間件是個(gè)什么東西 :)
假設(shè)我們現(xiàn)在有這樣的需求:打印出觸發(fā) dispatch 的 action,以及改變后的 state(nextState)。我們很快可以寫出如下代碼,然而這樣做的壞處是我們要在每一個(gè)需要 log 的地方加上這些代碼,看起來十分不整潔,即使封裝成一個(gè)函數(shù)把 dispatch 包裹起來,也需要在很多地方進(jìn)行 import,這是非常冗余的操作。
console.log("dispatch", action); store.dispatch(action); console.log("nextState", store.getState());
考慮到這兒,我們知道可以把 dispatch 方法進(jìn)行重寫,這樣的話每次在我們調(diào)用 dispatch 的時(shí)候都可以自動(dòng)進(jìn)行 log 操作。
const next = store.dispatch(); store.dispatch = function (action) { console.log("dispatch", action); store.dispatch(action); console.log("nextState", store.getState()); }
進(jìn)一步封裝就是中間件!
function logger(store) { const next = store.dispatch(); return function(action) { console.log("dispatch", action); const r = next(action); console.log("nextState", store.getState()); return r; }; }
這樣在我們用多個(gè)中間件的時(shí)候,就可以層層給 dispatch 進(jìn)行賦值:
middlewares.forEach((middleware) => store.dispatch = middleware(store); )
在我們上面的做法中,為了讓下一個(gè)中間件可以得到最新的 dispatch 函數(shù),在每個(gè)中間件中都要給 store.dispatch 賦值,那還有沒有別的做法呢?答案是肯定的,那就是把 next 作為參數(shù)傳遞,而不是在中間件中讀取 store.dispatch 獲得。
function logger(store) { return function(next) { return function(action) { console.log("dispatch", action); const r = next(action); console.log("nextState", store.getState()); return r; }; }; }
柯里化一下:
const logger = store => next => action => { console.log("dispatch", action); const r = next(action); console.log("nextState", store.getState()); return r; };
這樣的話,多個(gè)中間件的使用就如下代碼所示:
middlewares.forEach((middleware) => { dispatch = middleware(store)(store.dispatch); });
這樣做的好處是,可以不改變 store.dispatch 本身,僅僅是不把它暴露給用戶而已。
接下來再看 redux 的 applyMiddleware 實(shí)現(xiàn)就很清晰:先使用 compose 把多個(gè)中間件組合起來,然后再封裝出一個(gè)新的 dispatch 返回給用戶。
import compose from "./compose" /** * Creates a store enhancer that applies middleware to the dispatch method * of the Redux store. This is handy for a variety of tasks, such as expressing * asynchronous actions in a concise manner, or logging every action payload. * * See `redux-thunk` package as an example of the Redux middleware. * * Because middleware is potentially asynchronous, this should be the first * store enhancer in the composition chain. * * Note that each middleware will be given the `dispatch` and `getState` functions * as named arguments. * * @param {...Function} middlewares The middleware chain to be applied. * @returns {Function} A store enhancer applying the middleware. */ export default function applyMiddleware(...middlewares) { return createStore => (...args) => { 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) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }combineReducers.js
import ActionTypes from "./utils/actionTypes" import warning from "./utils/warning" import isPlainObject from "./utils/isPlainObject" function getUndefinedStateErrorMessage(key, action) { const actionType = action && action.type const actionDescription = (actionType && `action "${String(actionType)}"`) || "an action" return ( `Given ${actionDescription}, reducer "${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.` ) } function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) { const reducerKeys = Object.keys(reducers) const argumentName = action && action.type === ActionTypes.INIT ? "preloadedState argument passed to createStore" : "previous state received by the reducer" if (reducerKeys.length === 0) { return ( "Store does not have a valid reducer. Make sure the argument passed " + "to combineReducers is an object whose values are reducers." ) } if (!isPlainObject(inputState)) { return ( `The ${argumentName} has unexpected type of "` + {}.toString.call(inputState).match(/s([a-z|A-Z]+)/)[1] + `". Expected argument to be an object with the following ` + `keys: "${reducerKeys.join("", "")}"` ) } const unexpectedKeys = Object.keys(inputState).filter( key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] ) unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) if (action && action.type === ActionTypes.REPLACE) return if (unexpectedKeys.length > 0) { return ( `Unexpected ${unexpectedKeys.length > 1 ? "keys" : "key"} ` + `"${unexpectedKeys.join("", "")}" found in ${argumentName}. ` + `Expected to find one of the known reducer keys instead: ` + `"${reducerKeys.join("", "")}". Unexpected keys will be ignored.` ) } } function assertReducerShape(reducers) { Object.keys(reducers).forEach(key => { const reducer = reducers[key] const initialState = reducer(undefined, { type: ActionTypes.INIT }) if (typeof initialState === "undefined") { throw new Error( `Reducer "${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( `Reducer "${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.` ) } }) } /** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results * into a single state object, whose keys correspond to the keys of the passed * reducer functions. * * @param {Object} reducers An object whose values correspond to different * reducer functions that need to be combined into one. One handy way to obtain * it is to use ES6 `import * as reducers` syntax. The reducers may never return * undefined for any action. Instead, they should return their initial state * if the state passed to them was undefined, and the current state for any * unrecognized action. * * @returns {Function} A reducer function that invokes every reducer inside the * passed object, and builds a state object with the same shape. */ export default function combineReducers(reducers) { // 對象的 key 是對應(yīng) value(reducer 函數(shù))的狀態(tài)名稱 const reducerKeys = Object.keys(reducers) const finalReducers = {} 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}"`) } } if (typeof reducers[key] === "function") { finalReducers[key] = reducers[key] } } const finalReducerKeys = Object.keys(finalReducers) let unexpectedKeyCache if (process.env.NODE_ENV !== "production") { unexpectedKeyCache = {} } let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } return function combination(state = {}, action) { if (shapeAssertionError) { throw shapeAssertionError } if (process.env.NODE_ENV !== "production") { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { // 有效的狀態(tài)字段 const key = finalReducerKeys[i] // 該狀態(tài)對應(yīng)的 reducer 函數(shù) const reducer = finalReducers[key] // 當(dāng)前狀態(tài)值 const previousStateForKey = state[key] // 計(jì)算后的新狀態(tài)值 const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === "undefined") { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } // 更新整體狀態(tài)中的相應(yīng)字段 nextState[key] = nextStateForKey // 判斷整體狀態(tài)是否發(fā)生改變 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state } }
combineReducers 函數(shù)接受一組 reducers,組合后返回一個(gè)新的 reducer 函數(shù)(combine)供 createStore 使用。
bindActionCreators.jsbindActionCreators 用來生成可以直接觸發(fā) action 的函數(shù),并沒什么好說的 = =
function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } } /** * Turns an object whose values are action creators, into an object with the * same keys, but with every function wrapped into a `dispatch` call so they * may be invoked directly. This is just a convenience method, as you can call * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. * * For convenience, you can also pass a single function as the first argument, * and get a function in return. * * @param {Function|Object} actionCreators An object whose values are action * creator functions. One handy way to obtain it is to use ES6 `import * as` * syntax. You may also pass a single function. * * @param {Function} dispatch The `dispatch` function available on your Redux * store. * * @returns {Function|Object} The object mimicking the original object, but with * every action creator wrapped into the `dispatch` call. If you passed a * function as `actionCreators`, the return value will also be a single * function. */ 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 }utils actionTypes.js
/** * These are private action types ≈ by Redux. * For any unknown actions, you must return the current state. * If the current state is undefined, you must return the initial state. * Do not reference these action types directly in your code. */ const randomString = () => Math.random() .toString(36) .substring(7) .split("") .join(".") const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` } export default ActionTypesisPlainObject.js
/** * @param {any} obj The object to inspect. * @returns {boolean} True if the argument appears to be a plain object. */ export default function isPlainObject(obj) { if (typeof obj !== "object" || obj === null) return false let proto = obj while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto }
顧名思義 isPlainObject 函數(shù)用于判斷 obj 是否為樸素對象(即原型鏈上直接繼承 Object.prototype 的對象,也就是由 {} 或者 new Object() 創(chuàng)建的對象)。
warning.js/** * Prints a warning in the console if it exists. * * @param {String} message The warning message. * @returns {void} */ export default function warning(message) { /* eslint-disable no-console */ if (typeof console !== "undefined" && typeof console.error === "function") { console.error(message) } /* eslint-enable no-console */ try { // This error was thrown as a convenience so that if you enable // "break on all exceptions" in your console, // it would pause the execution at this line. throw new Error(message) } catch (e) {} // eslint-disable-line no-empty }
至此結(jié)束。
決定代碼質(zhì)量的是思維,而非框架或語法。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/95193.html
摘要:今天給大家?guī)砹撕贸绦騿T實(shí)戰(zhàn)項(xiàng)目商城管理后臺。配合項(xiàng)目學(xué)習(xí)會讓你更快掌握它的使用方法下面就來看看好程序員這套實(shí)戰(zhàn)項(xiàng)目課程介紹好程序員項(xiàng)目本項(xiàng)目是一個(gè)使用開發(fā)的商城系統(tǒng)的管理后臺,里面登錄判斷,接口調(diào)用,數(shù)據(jù)展示和編輯,文件上傳等后臺功能。 眾所周知,項(xiàng)目經(jīng)驗(yàn)對于一個(gè)程序員變得越來越重要。在面...
摘要:前端支持同域才能發(fā)送本月初,瀏覽器發(fā)布。所謂攻擊,就是使用真實(shí)的進(jìn)行惡意行為。鏈接發(fā)布此次發(fā)布亮點(diǎn)包括類型以及對和映射對象類型中的符號和數(shù)字文字的支持。但直到看到了,總算覺得社區(qū)又進(jìn)了一步。微信已將對弈源碼和訓(xùn)練好的模型開源。 【前端】 1、Firefox 60 支持同域才能發(fā)送 Cookie 本月初,F(xiàn)irefox 60 瀏覽器發(fā)布。它有一個(gè)很大的亮點(diǎn),就是它解決了 CSRF 攻擊。...
摘要:前端支持同域才能發(fā)送本月初,瀏覽器發(fā)布。所謂攻擊,就是使用真實(shí)的進(jìn)行惡意行為。鏈接發(fā)布此次發(fā)布亮點(diǎn)包括類型以及對和映射對象類型中的符號和數(shù)字文字的支持。但直到看到了,總算覺得社區(qū)又進(jìn)了一步。微信已將對弈源碼和訓(xùn)練好的模型開源。 【前端】 1、Firefox 60 支持同域才能發(fā)送 Cookie 本月初,F(xiàn)irefox 60 瀏覽器發(fā)布。它有一個(gè)很大的亮點(diǎn),就是它解決了 CSRF 攻擊。...
摘要:前端支持同域才能發(fā)送本月初,瀏覽器發(fā)布。所謂攻擊,就是使用真實(shí)的進(jìn)行惡意行為。鏈接發(fā)布此次發(fā)布亮點(diǎn)包括類型以及對和映射對象類型中的符號和數(shù)字文字的支持。但直到看到了,總算覺得社區(qū)又進(jìn)了一步。微信已將對弈源碼和訓(xùn)練好的模型開源。 【前端】 1、Firefox 60 支持同域才能發(fā)送 Cookie 本月初,F(xiàn)irefox 60 瀏覽器發(fā)布。它有一個(gè)很大的亮點(diǎn),就是它解決了 CSRF 攻擊。...
閱讀 3495·2023-04-26 02:44
閱讀 1632·2021-11-25 09:43
閱讀 1523·2021-11-08 13:27
閱讀 1888·2021-09-09 09:33
閱讀 906·2019-08-30 15:53
閱讀 1768·2019-08-30 15:53
閱讀 2780·2019-08-30 15:53
閱讀 3114·2019-08-30 15:44