原文連結:https://github.com/ecmadao/Co…
轉載請註明出處本文不涉及redux的使用方法,因此可能更適合使用過redux的玩家翻閱?
預熱
柯里化函式(curry
)
通俗的來講,可以用一句話概括柯里化函式:返回函式的函式
// example
const funcA = (a) => {
return const funcB = (b) => {
return a + b
}
};
上述的funcA
函式接收一個引數,並返回同樣接收一個引數的funcB
函式。
柯里化函式有什麼好處呢?
-
避免了給一個函式傳入大量的引數–我們可以通過柯里化來構建類似上例的函式巢狀,將引數的代入分離開,更有利於除錯
-
降低耦合度和程式碼冗餘,便於複用
舉個例子:
// 已知listA, listB兩個Array,都由int組成,需要篩選出兩個Array的交集
const listA = [1, 2, 3, 4, 5];
const listB = [2, 3, 4];
const checkIfDataExist = (list) => {
return (target) => {
return list.some(value => value === target)
};
};
// 呼叫一次checkIfDataExist函式,並將listA作為引數傳入,來構建一個新的函式。
// 而新函式的作用則是:檢查傳入的引數是否存在於listA裡
const ifDataExist = checkIfDataExist(listA);
// 使用新函式來對listB裡的每一個元素進行篩選
const intersectionList = listB.filter(value => ifDataExist(value));
console.log(intersectionList); // [2, 3, 4]
程式碼組合(compose
)
程式碼組合就像是數學中的結合律:
const compose = (f, g) => {
return (x) => {
return f(g(x));
};
};
// 還可以再簡潔點
const compose = (f, g) => (x) => f(g(x));
通過這樣函式之間的組合,可以大大增加可讀性,效果遠大於巢狀一大堆的函式呼叫,並且我們可以隨意更改函式的呼叫順序
Redux
combineReducers
// 回顧一下combineReducers的使用格式
// 兩個reducer
const todos = (state = INIT.todos, action) => {
// ....
};
const filterStatus = (state = INIT.filterStatus, action) => {
// ...
};
const appReducer = combineReducers({
todos,
filterStatus
});
還記得
combineReducers
的黑魔法嗎?即:
傳入的Object引數中,物件的
key
與value
所代表的reducer function
同名各個
reducer function
的名稱和需要傳入該reducer的state
引數同名
原始碼標註解讀(省略部分):
export default function combineReducers(reducers) {
// 第一次篩選,引數reducers為Object
// 篩選掉reducers中不是function的鍵值對
var reducerKeys = Object.keys(reducers);
var finalReducers = {}
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i];
if (typeof reducers[key] === `function`) {
finalReducers[key] = reducers[key]
}
}
var finalReducerKeys = Object.keys(finalReducers)
// 二次篩選,判斷reducer中傳入的值是否合法(!== undefined)
// 獲取篩選完之後的所有key
var sanityError
try {
// assertReducerSanity函式用於遍歷finalReducers中的reducer,檢查傳入reducer的state是否合法
assertReducerSanity(finalReducers)
} catch (e) {
sanityError = e
}
// 返回一個function。該方法接收state和action作為引數
return function combination(state = {}, action) {
// 如果之前的判斷reducers中有不法值,則丟擲錯誤
if (sanityError) {
throw sanityError
}
// 如果不是production環境則丟擲warning
if (process.env.NODE_ENV !== `production`) {
var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
if (warningMessage) {
warning(warningMessage)
}
}
var hasChanged = false
var nextState = {}
// 遍歷所有的key和reducer,分別將reducer對應的key所代表的state,代入到reducer中進行函式呼叫
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i]
var reducer = finalReducers[key]
// 這也就是為什麼說combineReducers黑魔法--要求傳入的Object引數中,reducer function的名稱和要和state同名的原因
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
// 如果reducer返回undefined則丟擲錯誤
if (typeof nextStateForKey === `undefined`) {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
// 將reducer返回的值填入nextState
nextState[key] = nextStateForKey
// 如果任一state有更新則hasChanged為true
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
// 檢查傳入reducer的state是否合法
function assertReducerSanity(reducers) {
Object.keys(reducers).forEach(key => {
var reducer = reducers[key]
// 遍歷全部reducer,並給它傳入(undefined, action)
// 當第一個引數傳入undefined時,則為各個reducer定義的預設引數
var initialState = reducer(undefined, { type: ActionTypes.INIT })
// ActionTypes.INIT幾乎不會被定義,所以會通過switch的default返回reducer的預設引數。如果沒有指定預設引數,則返回undefined,丟擲錯誤
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.`
)
}
var type = `@@redux/PROBE_UNKNOWN_ACTION_` + Math.random().toString(36).substring(7).split(``).join(`.`)
if (typeof reducer(undefined, { type }) === `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.`
)
}
})
}
createStore
// 回顧下使用方法
const store = createStore(reducers, state, enhance);
原始碼標註解讀(省略部分):
// 對於未知的action.type,reducer必須返回預設的引數state。這個ActionTypes.INIT就可以用來監測當reducer傳入未知type的action時,返回的state是否合法
export var ActionTypes = {
INIT: `@@redux/INIT`
}
export default function createStore(reducer, initialState, enhancer) {
// 檢查你的state和enhance引數有沒有傳反
if (typeof initialState === `function` && typeof enhancer === `undefined`) {
enhancer = initialState
initialState = undefined
}
// 如果有傳入合法的enhance,則通過enhancer再呼叫一次createStore
if (typeof enhancer !== `undefined`) {
if (typeof enhancer !== `function`) {
throw new Error(`Expected the enhancer to be a function.`)
}
return enhancer(createStore)(reducer, initialState)
}
if (typeof reducer !== `function`) {
throw new Error(`Expected the reducer to be a function.`)
}
var currentReducer = reducer
var currentState = initialState
var currentListeners = []
var nextListeners = currentListeners
var isDispatching = false // 是否正在分發事件
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 我們在action middleware中經常使用的getState()方法,返回當前state
function getState() {
return currentState
}
// 註冊listener,同時返回一個取消事件註冊的方法。當呼叫store.dispatch的時候呼叫listener
function subscribe(listener) {
if (typeof listener !== `function`) {
throw new Error(`Expected listener to be a function.`)
}
var isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
// 從nextListeners中去除掉當前listener
ensureCanMutateNextListeners()
var index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// dispatch方法接收的action是個物件,而不是方法。
// 這個物件實際上就是我們自定義action的返回值,因為dispatch的時候,已經呼叫過我們的自定義action了,比如 dispatch(addTodo())
function dispatch(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?`
)
}
// 呼叫dispatch的時候只能一個個呼叫,通過dispatch判斷呼叫的狀態
if (isDispatching) {
throw new Error(`Reducers may not dispatch actions.`)
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 遍歷呼叫各個linster
var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
// Replaces the reducer currently used by the store to calculate the state.
function replaceReducer(nextReducer) {
if (typeof nextReducer !== `function`) {
throw new Error(`Expected the nextReducer to be a function.`)
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.INIT })
}
// 當create store的時候,reducer會接受一個type為ActionTypes.INIT的action,使reducer返回他們預設的state,這樣可以快速的形成預設的state的結構
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}
thunkMiddleware
原始碼及其簡單簡直給跪…
// 返回以 dispatch 和 getState 作為引數的action
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
if (typeof action === `function`) {
return action(dispatch, getState);
}
return next(action);
};
}
applyMiddleware
先複習下用法:
// usage
import {createStore, applyMiddleware} from `redux`;
import thunkMiddleware from `redux-thunk`;
const store = createStore(
reducers,
state,
applyMiddleware(thunkMiddleware)
);
applyMiddleware
首先接收thunkMiddleware
作為引數,兩者組合成為一個新的函式(enhance
),之後在createStore
內部,因為enhance
的存在,將會變成返回enhancer(createStore)(reducer, initialState)
原始碼標註解讀(省略部分):
// 定義一個程式碼組合的方法
// 傳入一些function作為引數,返回其鏈式呼叫的形態。例如,
// compose(f, g, h) 最終返回 (...args) => f(g(h(...args)))
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
} else {
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
}
export default function applyMiddleware(...middlewares) {
// 最終返回一個以createStore為引數的匿名函式
// 這個函式返回另一個以reducer, initialState, enhancer為引數的匿名函式
return (createStore) => (reducer, initialState, enhancer) => {
var store = createStore(reducer, initialState, enhancer)
var dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 每個 middleware 都以 middlewareAPI 作為引數進行注入,返回一個新的鏈。此時的返回值相當於呼叫 thunkMiddleware 返回的函式: (next) => (action) => {} ,接收一個next作為其引數
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 並將鏈代入進 compose 組成一個函式的呼叫鏈
// compose(...chain) 返回形如(...args) => f(g(h(...args))),f/g/h都是chain中的函式物件。
// 在目前只有 thunkMiddleware 作為 middlewares 引數的情況下,將返回 (next) => (action) => {}
// 之後以 store.dispatch 作為引數進行注入
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
一臉懵逼?沒關係,來結合實際使用總結一下:
當我們搭配redux-thunk
這個庫的時候,在redux
配合components
時,通常這麼寫
import thunkMiddleware from `redux-thunk`;
import { createStore, applyMiddleware, combineReducer } from `redux`;
import * as reducers from `./reducers.js`;
const appReducer = combineReducer(reducers);
const store = createStore(appReducer, initialState, applyMiddleware(thunkMiddleware));
還記得當createStore
收到的引數中有enhance
時會怎麼做嗎?
// createStore.js
if (typeof enhancer !== `undefined`) {
if (typeof enhancer !== `function`) {
throw new Error(`Expected the enhancer to be a function.`)
}
return enhancer(createStore)(reducer, initialState)
}
也就是說,會變成下面的情況
applyMiddleware(thunkMiddleware)(createStore)(reducer, initialState)
-
applyMiddleware(thunkMiddleware)
applyMiddleware
接收thunkMiddleware
作為引數,返回形如(createStore) => (reducer, initialState, enhancer) => {}
的函式。
-
applyMiddleware(thunkMiddleware)(createStore)
以 createStore 作為引數,呼叫上一步返回的函式(reducer, initialState, enhancer) => {}
-
applyMiddleware(thunkMiddleware)(createStore)(reducer, initialState)
以(reducer, initialState)為引數進行呼叫。
在這個函式內部,thunkMiddleware
被呼叫,其作用是監測type
是function
的action
因此,如果dispatch
的action
返回的是一個function
,則證明是中介軟體,則將(dispatch, getState)
作為引數代入其中,進行action
內部下一步的操作。否則的話,認為只是一個普通的action
,將通過next
(也就是dispatch
)進一步分發。
也就是說,applyMiddleware(thunkMiddleware)
作為enhance
,最終起了這樣的作用:
對dispatch
呼叫的action
(例如,dispatch(addNewTodo(todo)))
進行檢查,如果action
在第一次呼叫之後返回的是function
,則將(dispatch, getState)
作為引數注入到action
返回的方法中,否則就正常對action
進行分發,這樣一來我們的中介軟體就完成嘍~
因此,當action
內部需要獲取state
,或者需要進行非同步操作,在操作完成之後進行事件呼叫分發的話,我們就可以讓action
返回一個以(dispatch, getState)
為引數的function
而不是通常的Object
,enhance
就會對其進行檢測以便正確的處理。
bindActionCreator
這個方法感覺比較少見,我個人也很少用到
在傳統寫法下,當我們要把 state 和 action 注入到子元件中時,一般會這麼做:
import { connect } from `react-redux`;
import {addTodo, deleteTodo} from `./action.js`;
class TodoComponect extends Component {
render() {
return (
<ChildComponent
deleteTodo={this.props.deleteTodo}
addTodo={this.props.addTodo}
/>
)
}
}
function mapStateToProps(state) {
return {
state
}
}
function mapDispatchToProps(dispatch) {
return {
deleteTodo: (id) => {
dispatch(deleteTodo(id));
},
addTodo: (todo) => {
dispatch(addTodo(todo));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoComponect);
使用bindActionCreators
可以把 action 轉為同名 key 的物件,但使用 dispatch 把每個 action 包圍起來呼叫
惟一使用 bindActionCreators 的場景是當你需要把 action creator 往下傳到一個元件上,卻不想讓這個元件覺察到 Redux 的存在,而且不希望把 Redux store 或 dispatch 傳給它。
import { bindActionCreators } from `redux`;
import { connect } from `react-redux`;
import {addTodo, deleteTodo} as TodoActions from `./action.js`;
class TodoComponect extends React.Component {
// 在本元件內的應用
addTodo(todo) {
let action = TodoActions.addTodo(todo);
this.props.dispatch(action);
}
deleteTodo(id) {
let action = TodoActions.deleteTodo(id);
this.props.dispatch(action);
}
render() {
let dispatch = this.props.dispatch;
// 傳遞給子元件
let boundActionCreators = bindActionCreators(TodoActions, dispatch);
return (
<ChildComponent
{...boundActionCreators}
/>
)
}
}
function mapStateToProps(state) {
return {
state
}
}
export default connect(mapStateToProps)(TodoComponect)
bindActionCreator
原始碼解析
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
// bindActionCreators期待一個Object作為actionCreators傳入,裡面是 key: action
export default function bindActionCreators(actionCreators, dispatch) {
// 如果只是傳入一個action,則通過bindActionCreator返回被繫結到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"?`
)
}
// 遍歷並通過bindActionCreator分發繫結至dispatch
var keys = Object.keys(actionCreators)
var boundActionCreators = {}
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
var actionCreator = actionCreators[key]
if (typeof actionCreator === `function`) {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
react-redux
Provider
export default class Provider extends Component {
getChildContext() {
// 將其宣告為 context 的屬性之一
return { store: this.store }
}
constructor(props, context) {
super(props, context)
// 接收 redux 的 store 作為 props
this.store = props.store
}
render() {
return Children.only(this.props.children)
}
}
if (process.env.NODE_ENV !== `production`) {
Provider.prototype.componentWillReceiveProps = function (nextProps) {
const { store } = this
const { store: nextStore } = nextProps
if (store !== nextStore) {
warnAboutReceivingStore()
}
}
}
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired
}
Provider.childContextTypes = {
store: storeShape.isRequired
}
connect
傳入mapStateToProps
,mapDispatchToProps
,mergeProps
,options
。
首先獲取傳入的引數,如果沒有則以預設值代替
const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars
const defaultMapDispatchToProps = dispatch => ({ dispatch })
const { pure = true, withRef = false } = options
之後,通過
const finalMergeProps = mergeProps || defaultMergeProps
選擇合併stateProps
,dispatchProps
,parentProps
的方式,預設的合併方式 defaultMergeProps
為:
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
返回一個以 Component 作為引數的函式。在這個函式內部,生成了一個叫做Connect
的 Component
// ...
return function wrapWithConnect(WrappedComponent) {
const connectDisplayName = `Connect(${getDisplayName(WrappedComponent)})`
// 檢查引數合法性
function checkStateShape(props, methodName) {}
// 合併props
function computeMergedProps(stateProps, dispatchProps, parentProps) {
const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
if (process.env.NODE_ENV !== `production`) {
checkStateShape(mergedProps, `mergeProps`)
}
return mergedProps
}
// start of Connect
class Connect extends Component {
constructor(props, context) {
super(props, context);
this.store = props.store || context.store
const storeState = this.store.getState()
this.state = { storeState }
this.clearCache()
}
computeStateProps(store, props) {
// 呼叫configureFinalMapState,使用傳入的mapStateToProps方法(或預設方法),將state map進props
}
configureFinalMapState(store, props) {}
computeDispatchProps(store, props) {
// 呼叫configureFinalMapDispatch,使用傳入的mapDispatchToProps方法(或預設方法),將action使用dispatch封裝map進props
}
configureFinalMapDispatch(store, props) {}
// 判斷是否更新props
updateStatePropsIfNeeded() {}
updateDispatchPropsIfNeeded() {}
updateMergedPropsIfNeeded() {}
componentDidMount() {
// 內部呼叫this.store.subscribe(this.handleChange.bind(this))
this.trySubscribe()
}
handleChange() {
const storeState = this.store.getState()
const prevStoreState = this.state.storeState
// 對資料進行監聽,傳送改變時呼叫
this.setState({ storeState })
}
// 取消監聽,清除快取
componentWillUnmount() {
this.tryUnsubscribe()
this.clearCache()
}
render() {
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
return this.renderedElement
}
}
// end of Connect
Connect.displayName = connectDisplayName
Connect.WrappedComponent = WrappedComponent
Connect.contextTypes = {
store: storeShape
}
Connect.propTypes = {
store: storeShape
}
return hoistStatics(Connect, WrappedComponent)
}
// ...
我們看見,在connect的最後,返回了使用hoistStatics
包裝的Connect
和WrappedComponent
hoistStatics是什麼鬼?為什麼使用它?
Copies non-react specific statics from a child component to a parent component. Similar to Object.assign, but with React static keywords blacklisted from being overridden.
也就是說,它類似於Object.assign
,作用是將子元件中的 static 方法複製進父元件,但不會覆蓋元件中的關鍵字方法(如 componentDidMount)
import hoistNonReactStatic from `hoist-non-react-statics`;
hoistNonReactStatic(targetComponent, sourceComponent);