本文首發於 vivo互聯網技術 微信公衆號
連接: mp.weixin.qq.com/s/jhgQXKp4s…
做者:曾超前端
Redux官網上是這樣描述Redux,Redux is a predictable state container for JavaScript apps.(Redux是JavaScript狀態容器,提供可預測性的狀態管理)。 目前Redux GitHub有5w多star,足以說明 Redux 受歡迎的程度。react
在說爲何用 Redux 以前,讓咱們先聊聊組件通訊有哪些方式。常見的組件通訊方式有如下幾種:git
父子組件:props、state/callback回調來進行通訊github
單頁面應用:路由傳值express
全局事件好比EventEmitter監聽回調傳值redux
react中跨層級組件數據傳遞Context(上下文)數組
在小型、不太複雜的應用中,通常用以上幾種組件通訊方式基本就足夠了。瀏覽器
但隨着應用逐漸複雜,數據狀態過多(好比服務端響應數據、瀏覽器緩存數據、UI狀態值等)以及狀態可能會常常發生變化的狀況下,使用以上組件通訊方式會很複雜、繁瑣以及很難定位、調試相關問題。緩存
所以狀態管理框架(如 Vuex、MobX、Redux等)就顯得十分必要了,而 Redux 就是其中使用最廣、生態最完善的。bash
在一個使用了 Redux 的 App應用裏面會遵循下面四步:
第一步:經過store.dispatch(action)來觸發一個action,action就是一個描述將要發生什麼的對象。以下:
{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: '金融前端.' }複製代碼
第二步:Redux會調用你提供的 Reducer函數。
第三步:根 Reducer 會將多個不一樣的 Reducer 函數合併到單獨的狀態樹中。
第四步:Redux store會保存從根 Reducer 函數返回的完整狀態樹。
所謂一圖勝千言,下面咱們結合 Redux 的數據流圖來熟悉這一過程。
一、Single source of truth:單一數據源,整個應用的state被存儲在一個對象樹中,而且只存在於惟一一個store中。
二、State is read-only:state裏面的狀態是隻讀的,不能直接去修改state,只能經過觸發action來返回一個新的state。
三、Changes are made with pure functions:要使用純函數來修改state。
Redux 源碼目前有js和ts版本,本文先介紹 js 版本的 Redux 源碼。Redux 源碼行數很少,因此對於想提升源碼閱讀能力的開發者來講,很值得前期來學習。
Redux源碼主要分爲6個核心js文件和3個工具js文件,核心js文件分別爲index.js、createStore.js、compose.js、combineRuducers.js、bindActionCreators.js和applyMiddleware.js文件。
接下來咱們來一一學習。
index.js是入口文件,提供核心的API,如createStore、combineReducers、applyMiddleware等。
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}複製代碼
createStore是 Redux 提供的API,用來生成惟一的store。store提供getState、dispatch、subscibe等方法,Redux 中的store只能經過dispatch一個action,經過action來找對應的 Reducer函數來改變。
export default function createStore(reducer, preloadedState, enhancer) {
...
}複製代碼
從源碼中能夠知道,createStore接收三個參數:Reducer、preloadedState、enhancer。
Reducer是action對應的一個能夠修改store中state的純函數。
preloadedState表明以前state的初始化狀態。
enhancer是中間件經過applyMiddleware生成的一個增強函數。store中的getState方法是獲取當前應用中store中的狀態樹。
/**
* 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
}複製代碼
dispatch方法是用來分發一個action的,這是惟一的一種能觸發狀態發生改變的方法。subscribe是一個監聽器,當一個action被dispatch的時候或者某個狀態發生改變的時候會被調用。
/**
* 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.
*/
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
...
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)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
//判斷state是否發生改變
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
//根據是否發生改變,來決定返回新的state仍是老的state
return hasChanged ? nextState : state
}
}複製代碼
從源碼能夠知道,入參是 Reducers,返回一個function。combineReducers就是將全部的 Reducer合併成一個大的 Reducer 函數。核心關鍵的地方就是每次 Reducer 返回新的state的時候會和老的state進行對比,若是發生改變,則hasChanged爲true,觸發頁面更新。反之,則不作處理。
/**
* 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.
*/
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
...
...
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
}複製代碼
bindActionCreator是將單個actionCreator綁定到dispatch上,bindActionCreators就是將多個actionCreators綁定到dispatch上。
bindActionCreator就是將發送actions的過程簡化,當調用這個返回的函數時就自動調用dispatch,發送對應的action。
bindActionCreators根據不一樣類型的actionCreators作不一樣的處理,actionCreators是函數就返回函數,是對象就返回一個對象。主要是將actions轉化爲dispatch(action)格式,方便進行actions的分離,而且使代碼更加簡潔。
/**
* 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是函數式變成裏面很是重要的一個概念,在介紹compose以前,先來認識下什麼是 Reduce?官方文檔這麼定義reduce:reduce()方法對累加器和數組中的每一個元素(從左到右)應用到一個函數,簡化爲某個值。compose是柯里化函數,藉助於Reduce來實現,將多個函數合併到一個函數返回,主要是在middleware中被使用。
/**
* 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.
*/
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
...
...
return {
...store,
dispatch
}
}
}複製代碼
applyMiddleware.js文件提供了middleware中間件重要的API,middleware中間件主要用來對store.dispatch進行重寫,來完善和擴展dispatch功能。
那爲何須要中間件呢?
首先得從Reducer提及,以前 Redux三大原則裏面提到了reducer必須是純函數,下面給出純函數的定義:
對於同一參數,返回同一結果
結果徹底取決於傳入的參數
不產生任何反作用
至於爲何reducer必須是純函數,能夠從如下幾點提及?
由於 Redux 是一個可預測的狀態管理器,純函數更便於 Redux進行調試,能更方便的跟蹤定位到問題,提升開發效率。
Redux 只經過比較新舊對象的地址來比較兩個對象是否相同,也就是經過淺比較。若是在 Reducer 內部直接修改舊的state的屬性值,新舊兩個對象都指向同一個對象,若是仍是經過淺比較,則會致使 Redux 認爲沒有發生改變。但要是經過深比較,會十分耗費性能。最佳的辦法是 Redux返回一個新對象,新舊對象經過淺比較,這也是 Reducer是純函數的重要緣由。
Reducer是純函數,可是在應用中仍是會須要處理記錄日誌/異常、以及異步處理等操做,那該如何解決這些問題呢?
這個問題的答案就是中間件。能夠經過中間件加強dispatch的功能,示例(記錄日誌和異常)以下:
const store = createStore(reducer);
const next = store.dispatch;
// 重寫store.dispatch
store.dispatch = (action) => {
try {
console.log('action:', action);
console.log('current state:', store.getState());
next(action);
console.log('next state', store.getState());
} catch (error){
console.error('msg:', error);
}
}複製代碼
既然是要從零開始實現一個Redux(簡易計數器),那麼在此以前咱們先忘記以前提到的store、Reducer、dispatch等各類概念,只需牢記Redux是一個狀態管理器。
首先咱們來看下面的代碼:
let state = {
count : 1
}
//修改以前
console.log (state.count);
//修改count的值爲2
state.count = 2;
//修改以後
console.log (state.count);複製代碼
咱們定義了一個有count字段的state對象,同時能輸出修改以前和修改以後的count值。但此時咱們會發現一個問題?就是其它若是引用了count的地方是不知道count已經發生修改的,所以咱們須要經過訂閱-發佈模式來監聽,並通知到其它引用到count的地方。所以咱們進一步優化代碼以下:
let state = {
count: 1
};
//訂閱
function subscribe (listener) {
listeners.push(listener);
}
function changeState(count) {
state.count = count;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();//監聽
}
}複製代碼
此時咱們對count進行修改,全部的listeners都會收到通知,而且能作出相應的處理。可是目前還會存在其它問題?好比說目前state只含有一個count字段,若是要是有多個字段是否處理方式一致。同時還須要考慮到公共代碼須要進一步封裝,接下來咱們再進一步優化:
const createStore = function (initState) {
let state = initState;
//訂閱
function subscribe (listener) {
listeners.push(listener);
}
function changeState (count) {
state.count = count;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();//通知
}
}
function getState () {
return state;
}
return {
subscribe,
changeState,
getState
}
}複製代碼
咱們能夠從代碼看出,最終咱們提供了三個API,是否是與以前Redux源碼中的核心入口文件index.js比較相似。可是到這裏尚未實現Redux,咱們須要支持添加多個字段到state裏面,而且要實現Redux計數器。
let initState = {
counter: {
count : 0
},
info: {
name: '',
description: ''
}
}
let store = createStore(initState);
//輸出count
store.subscribe(()=>{
let state = store.getState();
console.log(state.counter.count);
});
//輸出info
store.subscribe(()=>{
let state = store.getState();
console.log(`${state.info.name}:${state.info.description}`);
});複製代碼
經過測試,咱們發現目前已經支持了state裏面存多個屬性字段,接下來咱們把以前changeState改造一下,讓它能支持自增和自減。
//自增
store.changeState({
count: store.getState().count + 1
});
//自減
store.changeState({
count: store.getState().count - 1
});
//隨便改爲什麼
store.changeState({
count: 金融
});複製代碼
咱們發現能夠經過changeState自增、自減或者隨便改,但這其實不是咱們所須要的。咱們須要對修改count作約束,由於咱們在實現一個計數器,確定是只但願能進行加減操做的。因此咱們接下來對changeState作約束,約定一個plan方法,根據type來作不一樣的處理。
function plan (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
default:
return state
}
}
let store = createStore(plan, initState);
//自增
store.changeState({
type: 'INCREMENT'
});
//自減
store.changeState({
type: 'DECREMENT'
});複製代碼
咱們在代碼中已經對不一樣type作了不一樣處理,這個時候咱們發現不再能隨便對state中的count進行修改了,咱們已經成功對changeState作了約束。咱們把plan方法作爲createStore的入參,在修改state的時候按照plan方法來執行。到這裏,恭喜你們,咱們已經用Redux實現了一個簡單計數器了。
這就實現了 Redux?這怎麼和源碼不同啊
而後咱們再把plan換成reducer,把changeState換成dispatch就會發現,這就是Redux源碼所實現的基礎功能,如今再回過頭看Redux的數據流圖是否是更加清晰了。
Redux devtools是Redux的調試工具,能夠在Chrome上安裝對應的插件。對於接入了Redux的應用,經過 Redux devtools能夠很方便看到每次請求以後所發生的改變,方便開發同窗知道每次操做後的來龍去脈,大大提高開發調試效率。
如上圖所示就是 Redux devtools的可視化界面,左邊操做界面就是當前頁面渲染過程當中執行的action,右側操做界面是State存儲的數據,從State切換到action面板,能夠查看action對應的 Reducer參數。切換到Diff面板,能夠查看先後兩次操做發生變化的屬性值。
Redux 是一款優秀的狀態管理器,源碼短小精悍,社區生態也十分紅熟。如經常使用的react-redux、dva都是對 Redux 的封裝,目前在大型應用中被普遍使用。這裏推薦經過 Redux官網以及源碼來學習它核心的思想,進而提高閱讀源碼的能力。
更多內容敬請關注 vivo 互聯網技術 微信公衆號
注:轉載文章請先與微信號:labs2020 聯繫。