詳解JavaScript狀態(tài)容器Redux
一、Why Redux
在說(shuō)為什么用 Redux 之前,讓我們先聊聊組件通信有哪些方式。常見(jiàn)的組件通信方式有以下幾種:
- 父子組件:props、state/callback回調(diào)來(lái)進(jìn)行通信
- 單頁(yè)面應(yīng)用:路由傳值
- 全局事件比如EventEmitter監(jiān)聽(tīng)回調(diào)傳值
- react中跨層級(jí)組件數(shù)據(jù)傳遞Context(上下文)
在小型、不太復(fù)雜的應(yīng)用中,一般用以上幾種組件通信方式基本就足夠了。
但隨著應(yīng)用逐漸復(fù)雜,數(shù)據(jù)狀態(tài)過(guò)多(比如服務(wù)端響應(yīng)數(shù)據(jù)、瀏覽器緩存數(shù)據(jù)、UI狀態(tài)值等)以及狀態(tài)可能會(huì)經(jīng)常發(fā)生變化的情況下,使用以上組件通信方式會(huì)很復(fù)雜、繁瑣以及很難定位、調(diào)試相關(guān)問(wèn)題。
因此狀態(tài)管理框架(如 Vuex、MobX、Redux等)就顯得十分必要了,而 Redux 就是其中使用最廣、生態(tài)最完善的。
二、Redux Data flow
在一個(gè)使用了 Redux 的 App應(yīng)用里面會(huì)遵循下面四步:
第一步:通過(guò)store.dispatch(action)來(lái)觸發(fā)一個(gè)action,action就是一個(gè)描述將要發(fā)生什么的對(duì)象。如下:
{ type: 'LIKE_ARTICLE', articleId: 42 } { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } } { type: 'ADD_TODO', text: '金融前端.' }
第二步:Redux會(huì)調(diào)用你提供的 Reducer函數(shù)。
第三步:根 Reducer 會(huì)將多個(gè)不同的 Reducer 函數(shù)合并到單獨(dú)的狀態(tài)樹(shù)中。
第四步:Redux store會(huì)保存從根 Reducer 函數(shù)返回的完整狀態(tài)樹(shù)。
所謂一圖勝千言,下面我們結(jié)合 Redux 的數(shù)據(jù)流圖來(lái)熟悉這一過(guò)程。
三、Three Principles(三大原則)
1、Single source of truth:?jiǎn)我粩?shù)據(jù)源,整個(gè)應(yīng)用的state被存儲(chǔ)在一個(gè)對(duì)象樹(shù)中,并且只存在于唯一一個(gè)store中。
2、State is read-only:state里面的狀態(tài)是只讀的,不能直接去修改state,只能通過(guò)觸發(fā)action來(lái)返回一個(gè)新的state。
3、Changes are made with pure functions:要使用純函數(shù)來(lái)修改state。
四、Redux源碼解析
Redux 源碼目前有js和ts版本,本文先介紹 js 版本的 Redux 源碼。Redux 源碼行數(shù)不多,所以對(duì)于想提高源碼閱讀能力的開(kāi)發(fā)者來(lái)說(shuō),很值得前期來(lái)學(xué)習(xí)。
Redux源碼主要分為6個(gè)核心js文件和3個(gè)工具js文件,核心js文件分別為index.js、createStore.js、compose.js、combineRuducers.js、bindActionCreators.js和applyMiddleware.js文件。
接下來(lái)我們來(lái)一一學(xué)習(xí)。
4.1、index.js
index.js是入口文件,提供核心的API,如createStore、combineReducers、applyMiddleware等。
export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }
4.2、createStore.js
createStore是 Redux 提供的API,用來(lái)生成唯一的store。store提供getState、dispatch、subscibe等方法,Redux 中的store只能通過(guò)dispatch一個(gè)action,通過(guò)action來(lái)找對(duì)應(yīng)的 Reducer函數(shù)來(lái)改變。
export default function createStore(reducer, preloadedState, enhancer) { ... }
從源碼中可以知道,createStore接收三個(gè)參數(shù):Reducer、preloadedState、enhancer。
Reducer是action對(duì)應(yīng)的一個(gè)可以修改store中state的純函數(shù)。
preloadedState代表之前state的初始化狀態(tài)。
enhancer是中間件通過(guò)applyMiddleware生成的一個(gè)加強(qiáng)函數(shù)。store中的getState方法是獲取當(dāng)前應(yīng)用中store中的狀態(tài)樹(shù)。
/** * 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方法是用來(lái)分發(fā)一個(gè)action的,這是唯一的一種能觸發(fā)狀態(tài)發(fā)生改變的方法。subscribe是一個(gè)監(jiān)聽(tīng)器,當(dāng)一個(gè)action被dispatch的時(shí)候或者某個(gè)狀態(tài)發(fā)生改變的時(shí)候會(huì)被調(diào)用。
4.3、combineReducers.js
/** * 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是否發(fā)生改變 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } //根據(jù)是否發(fā)生改變,來(lái)決定返回新的state還是老的state return hasChanged ? nextState : state } }
從源碼可以知道,入?yún)⑹?Reducers,返回一個(gè)function。combineReducers就是將所有的 Reducer合并成一個(gè)大的 Reducer 函數(shù)。核心關(guān)鍵的地方就是每次 Reducer 返回新的state的時(shí)候會(huì)和老的state進(jìn)行對(duì)比,如果發(fā)生改變,則hasChanged為true,觸發(fā)頁(yè)面更新。反之,則不做處理。
4.4、bindActionCreators.js
/** * 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是將單個(gè)actionCreator綁定到dispatch上,bindActionCreators就是將多個(gè)actionCreators綁定到dispatch上。
bindActionCreator就是將發(fā)送actions的過(guò)程簡(jiǎn)化,當(dāng)調(diào)用這個(gè)返回的函數(shù)時(shí)就自動(dòng)調(diào)用dispatch,發(fā)送對(duì)應(yīng)的action。
bindActionCreators根據(jù)不同類(lèi)型的actionCreators做不同的處理,actionCreators是函數(shù)就返回函數(shù),是對(duì)象就返回一個(gè)對(duì)象。主要是將actions轉(zhuǎn)化為dispatch(action)格式,方便進(jìn)行actions的分離,并且使代碼更加簡(jiǎn)潔。
4.5、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ù)式變成里面非常重要的一個(gè)概念,在介紹compose之前,先來(lái)認(rèn)識(shí)下什么是 Reduce?官方文檔這么定義reduce:reduce()方法對(duì)累加器和數(shù)組中的每個(gè)元素(從左到右)應(yīng)用到一個(gè)函數(shù),簡(jiǎn)化為某個(gè)值。compose是柯里化函數(shù),借助于Reduce來(lái)實(shí)現(xiàn),將多個(gè)函數(shù)合并到一個(gè)函數(shù)返回,主要是在middleware中被使用。
4.6、applyMiddleware.js
/** * 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中間件主要用來(lái)對(duì)store.dispatch進(jìn)行重寫(xiě),來(lái)完善和擴(kuò)展dispatch功能。
那為什么需要中間件呢?
首先得從Reducer說(shuō)起,之前 Redux三大原則里面提到了reducer必須是純函數(shù),下面給出純函數(shù)的定義:
- 對(duì)于同一參數(shù),返回同一結(jié)果
- 結(jié)果完全取決于傳入的參數(shù)
- 不產(chǎn)生任何副作用
至于為什么reducer必須是純函數(shù),可以從以下幾點(diǎn)說(shuō)起?
- 因?yàn)?Redux 是一個(gè)可預(yù)測(cè)的狀態(tài)管理器,純函數(shù)更便于 Redux進(jìn)行調(diào)試,能更方便的跟蹤定位到問(wèn)題,提高開(kāi)發(fā)效率。
- Redux 只通過(guò)比較新舊對(duì)象的地址來(lái)比較兩個(gè)對(duì)象是否相同,也就是通過(guò)淺比較。如果在 Reducer 內(nèi)部直接修改舊的state的屬性值,新舊兩個(gè)對(duì)象都指向同一個(gè)對(duì)象,如果還是通過(guò)淺比較,則會(huì)導(dǎo)致 Redux 認(rèn)為沒(méi)有發(fā)生改變。但要是通過(guò)深比較,會(huì)十分耗費(fèi)性能。最佳的辦法是 Redux返回一個(gè)新對(duì)象,新舊對(duì)象通過(guò)淺比較,這也是 Reducer是純函數(shù)的重要原因。
Reducer是純函數(shù),但是在應(yīng)用中還是會(huì)需要處理記錄日志/異常、以及異步處理等操作,那該如何解決這些問(wèn)題呢?
這個(gè)問(wèn)題的答案就是中間件??梢酝ㄟ^(guò)中間件增強(qiáng)dispatch的功能,示例(記錄日志和異常)如下:
const store = createStore(reducer); const next = store.dispatch; // 重寫(xiě)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); } }
五、從零開(kāi)始實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Redux
既然是要從零開(kāi)始實(shí)現(xiàn)一個(gè)Redux(簡(jiǎn)易計(jì)數(shù)器),那么在此之前我們先忘記之前提到的store、Reducer、dispatch等各種概念,只需牢記Redux是一個(gè)狀態(tài)管理器。
首先我們來(lái)看下面的代碼:
let state = { count : 1 } //修改之前 console.log (state.count); //修改count的值為2 state.count = 2; //修改之后 console.log (state.count);
我們定義了一個(gè)有count字段的state對(duì)象,同時(shí)能輸出修改之前和修改之后的count值。但此時(shí)我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題?就是其它如果引用了count的地方是不知道count已經(jīng)發(fā)生修改的,因此我們需要通過(guò)訂閱-發(fā)布模式來(lái)監(jiān)聽(tīng),并通知到其它引用到count的地方。因此我們進(jìn)一步優(yōu)化代碼如下:
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();//監(jiān)聽(tīng) } }
此時(shí)我們對(duì)count進(jìn)行修改,所有的listeners都會(huì)收到通知,并且能做出相應(yīng)的處理。但是目前還會(huì)存在其它問(wèn)題?比如說(shuō)目前state只含有一個(gè)count字段,如果要是有多個(gè)字段是否處理方式一致。同時(shí)還需要考慮到公共代碼需要進(jìn)一步封裝,接下來(lái)我們?cè)龠M(jìn)一步優(yōu)化:
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 } }
我們可以從代碼看出,最終我們提供了三個(gè)API,是不是與之前Redux源碼中的核心入口文件index.js比較類(lèi)似。但是到這里還沒(méi)有實(shí)現(xiàn)Redux,我們需要支持添加多個(gè)字段到state里面,并且要實(shí)現(xiàn)Redux計(jì)數(shù)器。
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}`); });
通過(guò)測(cè)試,我們發(fā)現(xiàn)目前已經(jīng)支持了state里面存多個(gè)屬性字段,接下來(lái)我們把之前changeState改造一下,讓它能支持自增和自減。
//自增 store.changeState({ count: store.getState().count + 1 }); //自減 store.changeState({ count: store.getState().count - 1 }); //隨便改成什么 store.changeState({ count: 金融 });
我們發(fā)現(xiàn)可以通過(guò)changeState自增、自減或者隨便改,但這其實(shí)不是我們所需要的。我們需要對(duì)修改count做約束,因?yàn)槲覀冊(cè)趯?shí)現(xiàn)一個(gè)計(jì)數(shù)器,肯定是只希望能進(jìn)行加減操作的。所以我們接下來(lái)對(duì)changeState做約束,約定一個(gè)plan方法,根據(jù)type來(lái)做不同的處理。
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' });
我們?cè)诖a中已經(jīng)對(duì)不同type做了不同處理,這個(gè)時(shí)候我們發(fā)現(xiàn)再也不能隨便對(duì)state中的count進(jìn)行修改了,我們已經(jīng)成功對(duì)changeState做了約束。我們把plan方法做為createStore的入?yún)?,在修改state的時(shí)候按照plan方法來(lái)執(zhí)行。到這里,恭喜大家,我們已經(jīng)用Redux實(shí)現(xiàn)了一個(gè)簡(jiǎn)單計(jì)數(shù)器了。
這就實(shí)現(xiàn)了 Redux?這怎么和源碼不一樣啊
然后我們?cè)侔裵lan換成reducer,把changeState換成dispatch就會(huì)發(fā)現(xiàn),這就是Redux源碼所實(shí)現(xiàn)的基礎(chǔ)功能,現(xiàn)在再回過(guò)頭看Redux的數(shù)據(jù)流圖是不是更加清晰了。
六、Redux Devtools
Redux devtools是Redux的調(diào)試工具,可以在Chrome上安裝對(duì)應(yīng)的插件。對(duì)于接入了Redux的應(yīng)用,通過(guò) Redux devtools可以很方便看到每次請(qǐng)求之后所發(fā)生的改變,方便開(kāi)發(fā)同學(xué)知道每次操作后的前因后果,大大提升開(kāi)發(fā)調(diào)試效率。
如上圖所示就是 Redux devtools的可視化界面,左邊操作界面就是當(dāng)前頁(yè)面渲染過(guò)程中執(zhí)行的action,右側(cè)操作界面是State存儲(chǔ)的數(shù)據(jù),從State切換到action面板,可以查看action對(duì)應(yīng)的 Reducer參數(shù)。切換到Diff面板,可以查看前后兩次操作發(fā)生變化的屬性值。
七、總結(jié)
Redux 是一款優(yōu)秀的狀態(tài)管理器,源碼短小精悍,社區(qū)生態(tài)也十分成熟。如常用的react-redux、dva都是對(duì) Redux 的封裝,目前在大型應(yīng)用中被廣泛使用。這里推薦通過(guò)Redux官網(wǎng)以及源碼來(lái)學(xué)習(xí)它核心的思想,進(jìn)而提升閱讀源碼的能力。
以上就是詳解JavaScript狀態(tài)容器Redux的詳細(xì)內(nèi)容,更多關(guān)于JavaScript狀態(tài)容器Redux的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Javascript?基于長(zhǎng)連接的服務(wù)框架問(wèn)題
本文針對(duì)經(jīng)常使用長(zhǎng)連接進(jìn)行消息收發(fā)的應(yīng)答場(chǎng)景,采用 Websocket 長(zhǎng)連接作為服務(wù)監(jiān)聽(tīng)的對(duì)象,模擬了一套類(lèi) http 服務(wù)框架,通過(guò)實(shí)例代碼介紹了Javascript?基于長(zhǎng)連接的服務(wù)框架,需要的朋友可以參考下2022-07-07Javascript實(shí)現(xiàn)商品秒殺倒計(jì)時(shí)(時(shí)間與服務(wù)器時(shí)間同步)
在一些購(gòu)物商城經(jīng)??吹接泻芏嗌唐纷雒霘⒒顒?dòng),也就是倒計(jì)時(shí),本篇文章給大家介紹Javascript實(shí)現(xiàn)商品秒殺倒計(jì)時(shí)(時(shí)間與服務(wù)器時(shí)間同步),需要的朋友可以了解下2015-09-09談?wù)凧avaScript異步函數(shù)發(fā)展歷程
對(duì)大部分JavaScript開(kāi)發(fā)者而言,async函數(shù)仍是新鮮事物,其發(fā)展經(jīng)歷了漫長(zhǎng)的旅程。本文將梳理總結(jié)JavaScript異步函數(shù)的發(fā)展歷程,并表示未來(lái)async函數(shù)將成為實(shí)現(xiàn)異步的主要方式。2015-09-09JS獲取當(dāng)前網(wǎng)址、主機(jī)地址項(xiàng)目根路徑
本文為大家提供JS如何獲取當(dāng)前網(wǎng)址、主機(jī)地址之后的目錄及項(xiàng)目根路徑的方法,喜歡的朋友可以收藏下2013-11-11學(xué)習(xí)JavaScript設(shè)計(jì)模式(代理模式)
這篇文章主要帶領(lǐng)大家學(xué)習(xí)JavaScript設(shè)計(jì)模式,其中重點(diǎn)介紹代理模式,對(duì)代理模式進(jìn)行詳細(xì)剖析,感興趣的小伙伴們可以參考一下2015-12-12