前面有一個Redux,我們去撩(聊)一下它。

lihuanji發表於2018-04-24

什麼是Redux?

管理整個前端專案(單頁應用)所有的狀態資料,統一把整個應用的狀態存到一個地方(store),儲存成一個狀態樹,修改資料需要派發(dispatch)一個動作(action)通知store修改。元件通過訂閱(subscribe)修改事件,獲取最新資料來修改自身狀態。

三大原則

  1. 整個應用的state儲存在store中,有且只存在一個store。
  2. store裡面的state是隻讀的,唯一改變state的方法就是派發(dispatch)一個動作(action)。
  3. 純函式(reducer)修改state,每次返回一個新的state,不能直接修改原物件。

為什麼要使用Redux(應用場景)

  1. 單頁應用複雜,管理不斷變化的state非常困難。
  2. 非父子關係元件通訊。
  3. 所有頁面的公用狀態。

從原始碼出發,整個redux原始碼,總共匯出5個函式,和一個__DO_NOT_USE__ActionTypes(預設的action,所有action型別不能重複,下面會細撩這個點),那麼下面就來細細的撩一下5個函式。

1.createStore(reducer, preloadedState, enhancer)

想要使用redux,首先就是建立一個全域性的store(當然是唯一的一個),就得呼叫這個函式(createStore)。該函式接收三個引數。store裡面儲存了所有的資料,可以看做一個容器。

前面有一個Redux,我們去撩(聊)一下它。

傳入reducerinitState建立store。

store返回給鑰匙,修改器,電話

有了鑰匙就能隨時取資料,如果需要修改資料就得通過修改器,如需要需要知道資料什麼時候修改了,就打一個電話給store,告訴它,資料修改好了給我說。

先來看看它返回的函式:

getState() => currentState

返回當前的store狀態樹,包含所有state。這裡在讀原始碼的時候發現一個疑問。

前面有一個Redux,我們去撩(聊)一下它。
這裡返回的是原本的物件,那麼外部拿到這個state,不是可以直接修改嗎?這樣違背了只讀。為什麼不返回一個新引用物件,防止此操作。

帶著這個疑問,給Redux提了一個PR. 得到回覆:

Yes - getState() just returns whatever the current state value is. It's up to you to avoid accidentally mutating it.

(譯文)是的,getState()只返回當前狀態值。你要避免不小心把它弄亂。

也就是說在這裡需要注意一下,getState()返回的值不能直接修改,否則你將陷入深淵

subscribe(listener) => unsubscribe()

傳入一個函式用來監聽store發生變化,一旦store發生了變化,將迴圈執行所有的監聽函式。呼叫該函式還返回了一個取消監聽的函式。呼叫返回的函式,則取消該listener監聽。

dispatch(action) => action

dispatch接收一個action。在函式內部會把所有的reducer執行一遍並把當前的state和action傳入reducer,然後再執行所有的監聽函式。從原始碼中截了一段出來:

const dispatch = (action) => {
	currentState = currentReducer(currentState, action);
	
	const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    
    return action
}
複製程式碼
action

一個物件,必須包含一個type,表示所要發生的動作,其他屬性自由設定,社群中有一個規範,把其他所有資料放入payload中,一般開發中會寫一個函式來生成action。

	function login(){
		retrun {
			type: 'LOGIN',
			payload: {
				username: 'xxx',
				passworld: 'xxxx'
			}
		}
	}
複製程式碼

replaceReducer(nextReducer)

傳入新的reducer,替換之前的reducer,並且派發一個ActionType為REPLACE的action。

再來看看它接收的三個引數

reducer

一個純函式,接收當前的state和action做為引數,返回新的state。

Store 收到 Action 以後,會呼叫reducer,必須給出一個新的 State,這樣 Store裡的資料才會發生變化。

編寫一個簡單的reducer

const reducer = function (state, action) {
	switch(action.type){
		case 'TYPE':
  			return {...state, newState}
  		default:
  			return state;
	}
};	
複製程式碼

preloadedState

原始碼裡面的引數名叫這個,其實我更願意叫它initState,初始化的狀態,為什麼原始碼不叫initState呢?因為不準確,在原始碼裡會預設傳送一個type為init的dispatch(),這個時候走到reducer裡面(看看上面的reducer程式碼),state如果在這個時候設定一個預設值,比如:

const reducer = function (state = {list: []}, action) {
	switch(action.type){
		case 'TYPE':
  			return {...state, newState}
  		default:
  			return state;
	}
};	
複製程式碼

這個時候就預設返回了{list: []},就給出了一個真正的initState。

enhancer

用來配合快速建立中介軟體的。


上面提到的__DO_NOT_USE__ActionTypes,就是2個actionType:

  • @@redux/INIT: 用來內部傳送一個預設的dispatch
  • @@redux/REPLACE: 用來替換reducer

2.bindActionCreators(actionCreators, dispatch) => boundActionCreators

遍歷所有生成action函式並執行,返回被dispatch包裹的函式,可供直接呼叫派發一個請求。

actionCreators

該引數為一個物件,包含生成action的函式,例如:

const actionCreators = {
	login: () => {
		return {
			type: 'LOGIN',
			payload: {
				username: 'xxx',
				passworld: 'xxxx'
			}
		}
	},
	logout: () => {
		retrun {
			type: 'LOGOUT'
		}
	}	
}
複製程式碼

如果傳入一個函式,則執行函式得到action,返回一個dispatch(action)。

dispatch

這裡就是createStore所返回的dispatch


該函式返回物件或函式(根據傳入的actionCreators來決定),可以直接呼叫xx.login()去派發一個登陸。

3.combineReducers(reducers)

在專案開發中,需要分模組寫reducer,利用該函式合併多個reducer模組。傳入一個reducer集合。

a.js
export (state = {list: []}, action) => {
	switch (action.type) {
	    case "LIST_CHANGE":
	        return { ...state, ...action.payload };
	    default:
	        return state;
	}
}

b.js
export (state = {list: []}, action) => {
	switch (action.type) {
	    case "LIST":
	        return { ...state, ...action.payload };
	    default:
	        return state;
	}
}

combineReducers.js
import a from './a';
import b from './b';

combineReducers({
	a: a,
	b: b
})
複製程式碼

ab都有list這個state,但是他們並不相關,要把他們分開使用,就得用combineReducers去合併。

下面簡單實現了該函式:

function combineReducers(reducers) {
    return (state = {}, action) => {
        return Object.keys(reducers).reduce((currentState, key) => {
            currentState[key] = reducers[key](state[key], action);
            return currentState;
        }, {})
    };
}
複製程式碼

4.compose

可以說是js中函式式中很重要的方法,把一堆函式串聯起來執行,從右至左執行(也就是倒序),函式的引數是上一個函式的結果。看一個使用例子:

const fn1 = (val) => val + 'fn1';

const fn2 = (val) => val + 'fn2';

const fn3 = (val) => val + 'fn3';

const dispatch = compose(fn1, fn2, fn3);

console.log(dispatch('test'));
複製程式碼

最終輸出結果testfn3fn2fn1

test傳給fn3當引數,fn3的返回值給了fn2....

compose.js

function compose(...fns){
    if(fns.length==1) return fns[0];
    return fns.reduce((a,b)=>(...args)=>a(b(...args)));
}
複製程式碼

5.applyMiddleware

該函式是用來新增中介軟體,在修改資料的時候做一些其他操作,redux通過改造dispatch來實現中介軟體.

使用中介軟體

const middleware = applyMiddleware(logger);
複製程式碼

需要傳入中介軟體函式,可以是多個函式,依次傳入,在applyMiddleware裡面用到了compose,所以我們傳入的函式的從右至左依次執行的,這裡需要注意一下。

const createStoreWithMiddleware = middleware(createStore);
複製程式碼

因為中介軟體是通過改造dispatch來實現,所以需要把建立store的方法傳進去。

const store = createStoreWithMiddleware(reducer, preloadedState)
複製程式碼

這裡再傳入createStore需要接收的引數,返回store物件。

實現一個logger中介軟體

const logger = function({dispatch, getState}){
   return function(next){
      return function(action){
	        console.log('oldState',getState());
	        next(action); // 真實派發動作
	        console.log('newState',getState());
	    }
    }
}
複製程式碼

首先middleware會把未改造的dispatchgetState傳入進來,這裡的next相當於dispatch,去派發一個真正的修改資料動作。

原始碼貼上:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

middlewareAPI是儲存未改造的方法,compose上面講過,第一個引數傳入的就是dispatch,返回的一個改造後dispatch就是通過compose過後的一個函式,會依次執行。

最後

一些學習心得,如有不對歡迎指正
我的github
謝謝你閱讀我的文章

相關文章