從零開始實(shí)現(xiàn)一個(gè) Redux恃轩,知其然并知其所以然

前言

記得開始接觸 react 技術(shù)棧的時(shí)候,最難理解的地方就是 redux黎做。全是新名詞:reducer叉跛、store、dispatch蒸殿、middleware 等等筷厘,我就理解 state 一個(gè)名詞。

網(wǎng)上找的 redux 文章宏所,要不有一本書的厚度酥艳,要不很玄乎,晦澀難懂楣铁,越看越覺得難玖雁,越看越怕,信心都沒有了盖腕!

花了很長(zhǎng)時(shí)間熟悉 redux赫冬,慢慢的發(fā)現(xiàn)它其實(shí)真的很簡(jiǎn)單浓镜。本章不會(huì)把 redux 的各種概念,名詞解釋一遍劲厌,這樣和其他教程沒有任何區(qū)別膛薛,沒有太大意義。我會(huì)帶大家從零實(shí)現(xiàn)一個(gè)完整的 redux补鼻,讓大家知其然哄啄,知其所以然。

開始前风范,你必須知道一些事情:

redux 和 react 沒有關(guān)系咨跌,redux 可以用在任何框架中,忘掉 react硼婿。

connect 不屬于 redux锌半,它其實(shí)屬于 react-redux,請(qǐng)先忘掉它寇漫,下一章節(jié)刊殉,我們會(huì)介紹它。

請(qǐng)一定先忘記 reducer州胳、store记焊、dispatch、middleware 等等這些名詞栓撞。

redux 是一個(gè)狀態(tài)管理器遍膜。

Let's Go!

狀態(tài)管理器

簡(jiǎn)單的狀態(tài)管理器

redux 是一個(gè)狀態(tài)管理器腐缤,那什么是狀態(tài)呢捌归?狀態(tài)就是數(shù)據(jù),比如計(jì)數(shù)器中的 count岭粤。

letstate?=?{

count:?1

}

我們來(lái)使用下狀態(tài)

console.log(state.count);

我們來(lái)修改下狀態(tài)

state.count = 2;

好了惜索,現(xiàn)在我們實(shí)現(xiàn)了狀態(tài)(計(jì)數(shù))的修改和使用了。

讀者:你當(dāng)我傻嗎剃浇?你說(shuō)的這個(gè)誰(shuí)不知道巾兆?捶你??!

筆者:哎哎哎虎囚,別打我角塑!有話好好說(shuō)!redux 核心就是這個(gè)呀淘讥!我們一步一步擴(kuò)展開來(lái)嘛圃伶!

當(dāng)然上面的有一個(gè)很明顯的問(wèn)題:修改 count 之后,使用 count 的地方不能收到通知。我們可以使用發(fā)布-訂閱模式來(lái)解決這個(gè)問(wèn)題窒朋。

/*------count?的發(fā)布訂閱者實(shí)踐------*/

letstate?=?{

count:1

};

letlisteners?=?[];

/*訂閱*/

functionsubscribe(listener){

listeners.push(listener);

}

functionchangeCount(count){

state.count?=?count;

/*當(dāng)?count?改變的時(shí)候搀罢,我們要去通知所有的訂閱者*/

for(leti?=0;?i?<?listeners.length;?i++)?{

constlistener?=?listeners[i];

listener();

}

}

我們來(lái)嘗試使用下這個(gè)簡(jiǎn)單的計(jì)數(shù)狀態(tài)管理器。

/*來(lái)訂閱一下侥猩,當(dāng)?count?改變的時(shí)候榔至,我要實(shí)時(shí)輸出新的值*/

subscribe(()=>{

console.log(state.count);

});

/*我們來(lái)修改下?state,當(dāng)然我們不能直接去改?state?了欺劳,我們要通過(guò)?changeCount?來(lái)修改*/

changeCount(2);

changeCount(3);

changeCount(4);

現(xiàn)在我們可以看到唧取,我們修改 count 的時(shí)候,會(huì)輸出相應(yīng)的 count 值划提。

現(xiàn)在有兩個(gè)新的問(wèn)題擺在我們面前

這個(gè)狀態(tài)管理器只能管理 count枫弟,不通用

公共的代碼要封裝起來(lái)

我們嘗試來(lái)解決這個(gè)問(wèn)題,把公共的代碼封裝起來(lái)

constcreateStore?=function(initState){

letstate?=?initState;

letlisteners?=?[];

/*訂閱*/

functionsubscribe(listener){

listeners.push(listener);

}

functionchangeState(newState){

state?=?newState;

/*通知*/

for(leti?=0;?i?<?listeners.length;?i++)?{

constlistener?=?listeners[i];

listener();

}

}

functiongetState(){

returnstate;

}

return{

subscribe,

changeState,

getState

}

}

我們來(lái)使用這個(gè)狀態(tài)管理器管理多個(gè)狀態(tài) counter 和 info 試試

letinitState?=?{

counter:?{

count:0

},

info:?{

name:'',

description:''

}

}

letstore?=?createStore(initState);

store.subscribe(()=>{

letstate?=?store.getState();

console.log(`${state.info.name}:${state.info.description}`);

});

store.subscribe(()=>{

letstate?=?store.getState();

console.log(state.counter.count);

});

store.changeState({

...store.getState(),

info:?{

name:'前端九部',

description:'我們都是前端愛好者鹏往!'

}

});

store.changeState({

...store.getState(),

counter:?{

count:1

}

});

到這里我們完成了一個(gè)簡(jiǎn)單的狀態(tài)管理器媒区。

這里需要理解的是 createStore,提供了 changeState掸犬,getState,subscribe 三個(gè)能力绪爸。

本小節(jié)完整源碼見 demo-1

有計(jì)劃的狀態(tài)管理器

我們用上面的狀態(tài)管理器來(lái)實(shí)現(xiàn)一個(gè)自增湾碎,自減的計(jì)數(shù)器。

letinitState?=?{

count:0

}

letstore?=?createStore(initState);

store.subscribe(()=>{

letstate?=?store.getState();

console.log(state.count);

});

/*自增*/

store.changeState({

count:?store.getState().count?+1

});

/*自減*/

store.changeState({

count:?store.getState().count?-1

});

/*我想隨便改*/

store.changeState({

count:'abc'

});

你一定發(fā)現(xiàn)了問(wèn)題奠货,count 被改成了字符串 abc介褥,因?yàn)槲覀儗?duì) count 的修改沒有任何約束,任何地方递惋,任何人都可以修改柔滔。

我們需要約束,不允許計(jì)劃外的 count 修改萍虽,我們只允許 count 自增和自減兩種改變方式睛廊!

那我們分兩步來(lái)解決這個(gè)問(wèn)題

制定一個(gè) state 修改計(jì)劃,告訴 store杉编,我的修改計(jì)劃是什么超全。

修改 store.changeState 方法,告訴它修改 state 的時(shí)候邓馒,按照我們的計(jì)劃修改嘶朱。

我們來(lái)設(shè)置一個(gè) plan 函數(shù),接收現(xiàn)在的 state光酣,和一個(gè) action疏遏,返回經(jīng)過(guò)改變后的新的 state。

/*注意:action =?{type:'',other:''}, action 必須有一個(gè) type 屬性*/

functionplan(state,?action){

switch(action.type)?{

case'INCREMENT':

return{

...state,

count:?state.count?+1

}

case'DECREMENT':

return{

...state,

count:?state.count?-1

}

default:

returnstate;

}

}

我們把這個(gè)計(jì)劃告訴 store,store.changeState 以后改變 state 要按照我的計(jì)劃來(lái)改财异。

/*增加一個(gè)參數(shù)?plan*/

constcreateStore?=function(plan,?initState){

letstate?=?initState;

letlisteners?=?[];

functionsubscribe(listener){

listeners.push(listener);

}

functionchangeState(action){

/*請(qǐng)按照我的計(jì)劃修改?state*/

state?=?plan(state,?action);

for(leti?=0;?i?<?listeners.length;?i++)?{

constlistener?=?listeners[i];

listener();

}

}

functiongetState(){

returnstate;

}

return{

subscribe,

changeState,

getState

}

}

我們來(lái)嘗試使用下新的 createStore 來(lái)實(shí)現(xiàn)自增和自減

letinitState?=?{

count:0

}

/*把plan函數(shù)*/

letstore?=?createStore(plan,?initState);

store.subscribe(()=>{

letstate?=?store.getState();

console.log(state.count);

});

/*自增*/

store.changeState({

type:'INCREMENT'

});

/*自減*/

store.changeState({

type:'DECREMENT'

});

/*我想隨便改?計(jì)劃外的修改是無(wú)效的倘零!*/

store.changeState({

count:'abc'

});

到這里為止,我們已經(jīng)實(shí)現(xiàn)了一個(gè)有計(jì)劃的狀態(tài)管理器宝当!

我們商量一下吧视事?我們給 plan 和 changeState 改下名字好不好?plan 改成 reducer庆揩,changeState 改成 dispatch俐东!不管你同不同意,我都要換订晌,因?yàn)樾旅直容^厲害(其實(shí)因?yàn)?redux 是這么叫的)!

本小節(jié)完整源碼見 demo-2

多文件協(xié)作

reducer 的拆分和合并

這一小節(jié)我們來(lái)處理下 reducer 的問(wèn)題虏辫。啥問(wèn)題?

我們知道 reducer 是一個(gè)計(jì)劃函數(shù)锈拨,接收老的 state砌庄,按計(jì)劃返回新的 state。那我們項(xiàng)目中奕枢,有大量的 state娄昆,每個(gè) state 都需要計(jì)劃函數(shù),如果全部寫在一起會(huì)是啥樣子呢缝彬?

所有的計(jì)劃寫在一個(gè) reducer 函數(shù)里面萌焰,會(huì)導(dǎo)致 reducer 函數(shù)及其龐大復(fù)雜。按經(jīng)驗(yàn)來(lái)說(shuō)谷浅,我們肯定會(huì)按組件維度來(lái)拆分出很多個(gè) reducer 函數(shù)扒俯,然后通過(guò)一個(gè)函數(shù)來(lái)把他們合并起來(lái)。

我們來(lái)管理兩個(gè) state一疯,一個(gè) counter撼玄,一個(gè) info。

letstate?=?{

counter:?{

count:?0

},

info:?{

name:'前端九部',

description:'我們都是前端愛好者墩邀!'

}

}

他們各自的 reducer

/*counterReducer,?一個(gè)子reducer*/

/*注意:counterReducer 接收的 state 是 state.counter*/

functioncounterReducer(state,?action){

switch(action.type)?{

case'INCREMENT':

return{

count:?state.count?+1

}

case'DECREMENT':

return{

...state,

count:?state.count?-1

}

default:

returnstate;

}

}

/*InfoReducer掌猛,一個(gè)子reducer*/

/*注意:InfoReducer 接收的 state 是 state.info*/

functionInfoReducer(state,?action){

switch(action.type)?{

case'SET_NAME':

return{

...state,

name:?action.name

}

case'SET_DESCRIPTION':

return{

...state,

description:?action.description

}

default:

returnstate;

}

}

那我們用 combineReducers 函數(shù)來(lái)把多個(gè) reducer 函數(shù)合并成一個(gè) reducer 函數(shù)。大概這樣用

constreducer?=?combineReducers({

counter:?counterReducer,

info:?InfoReducer

});

我們嘗試實(shí)現(xiàn)下?combineReducers?函數(shù)

functioncombineReducers(reducers){

/*?reducerKeys?=?['counter',?'info']*/

constreducerKeys?=Object.keys(reducers)

/*返回合并后的新的reducer函數(shù)*/

returnfunctioncombination(state?=?{},?action){

/*生成的新的state*/

constnextState?=?{}

/*遍歷執(zhí)行所有的reducers眉睹,整合成為一個(gè)新的state*/

for(leti?=0;?i?<?reducerKeys.length;?i++)?{

constkey?=?reducerKeys[i]

constreducer?=?reducers[key]

/*之前的?key?的?state*/

constpreviousStateForKey?=?state[key]

/*執(zhí)行?分?reducer留潦,獲得新的state*/

constnextStateForKey?=?reducer(previousStateForKey,?action)

nextState[key]?=?nextStateForKey

}

returnnextState;

}

}

我們來(lái)嘗試下 combineReducers 的威力吧

constreducer?=?combineReducers({

counter:?counterReducer,

info:?InfoReducer

});

letinitState?=?{

counter:?{

count:0

},

info:?{

name:'前端九部',

description:'我們都是前端愛好者!'

}

}

letstore?=?createStore(reducer,?initState);

store.subscribe(()=>{

letstate?=?store.getState();

console.log(state.counter.count,?state.info.name,?state.info.description);

});

/*自增*/

store.dispatch({

type:'INCREMENT'

});

/*修改?name*/

store.dispatch({

type:'SET_NAME',

name:'前端九部2號(hào)'

});

本小節(jié)完整源碼見 demo-3

state 的拆分和合并

上一小節(jié)辣往,我們把 reducer 按組件維度拆分了兔院,通過(guò) combineReducers 合并了起來(lái)。但是還有個(gè)問(wèn)題站削, state 我們還是寫在一起的坊萝,這樣會(huì)造成 state 樹很龐大,不直觀,很難維護(hù)十偶。我們需要拆分菩鲜,一個(gè) state,一個(gè) reducer 寫一塊惦积。

這一小節(jié)比較簡(jiǎn)單接校,我就不賣關(guān)子了,用法大概是這樣(注意注釋)

/*?counter?自己的?state?和?reducer?寫在一起*/

letinitState?=?{

count:0

}

functioncounterReducer(state,?action){

/*注意:如果 state 沒有初始值狮崩,那就給他初始值V朊恪!*/

if(!state)?{

state?=?initState;

}

switch(action.type)?{

case'INCREMENT':

return{

count:?state.count?+1

}

default:

returnstate;

}

}

我們修改下 createStore 函數(shù)睦柴,增加一行 dispatch({ type: Symbol() })

constcreateStore?=function(reducer,?initState){

letstate?=?initState;

letlisteners?=?[];

functionsubscribe(listener){

listeners.push(listener);

}

functiondispatch(action){

state?=?reducer(state,?action);

for(leti?=0;?i?<?listeners.length;?i++)?{

constlistener?=?listeners[i];

listener();

}

}

functiongetState(){

returnstate;

}

/*?注意7塘琛!坦敌!只修改了這里侣诵,用一個(gè)不匹配任何計(jì)劃的 type,來(lái)獲取初始值?*/

dispatch({type:Symbol()?})

return{

subscribe,

dispatch,

getState

}

}

我們思考下這行可以帶來(lái)什么效果狱窘?

createStore 的時(shí)候杜顺,用一個(gè)不匹配任何 type 的 action,來(lái)觸發(fā) state = reducer(state, action)

因?yàn)?action.type 不匹配蘸炸,每個(gè)子 reducer 都會(huì)進(jìn)到 default 項(xiàng)哑舒,返回自己初始化的 state,這樣就獲得了初始化的 state 樹了幻馁。

你可以試試

/*這里沒有傳?initState?哦?*/

conststore?=?createStore(reducer);

/*這里看看初始化的?state?是什么*/

console.dir(store.getState());

本小節(jié)完整源碼見 demo-4

到這里為止,我們已經(jīng)實(shí)現(xiàn)了一個(gè)七七八八的 redux 啦越锈!

中間件 middleware

中間件 middleware 是 redux 中最難理解的地方仗嗦。但是我挑戰(zhàn)一下用最通俗的語(yǔ)言來(lái)講明白它。如果你看完這一小節(jié)甘凭,還沒明白中間件是什么稀拐,不知道如何寫一個(gè)中間件,那就是我的鍋了丹弱!

中間件是對(duì) dispatch 的擴(kuò)展德撬,或者說(shuō)重寫,增強(qiáng) dispatch 的功能躲胳!

記錄日志

我現(xiàn)在有一個(gè)需求蜓洪,在每次修改 state 的時(shí)候,記錄下來(lái) 修改前的 state 坯苹,為什么修改了隆檀,以及修改后的 state。我們可以通過(guò)重寫?store.dispatch?來(lái)實(shí)現(xiàn),直接看代碼

conststore?=?createStore(reducer);

constnext?=?store.dispatch;

/*重寫了store.dispatch*/

store.dispatch?=(action)?=>{

console.log('this?state',?store.getState());

console.log('action',?action);

next(action);

console.log('next?state',?store.getState());

}

我們來(lái)使用下

store.dispatch({

type:'INCREMENT'

});

日志輸出為

thisstate{counter:?{?count:0}?}

action{type:'INCREMENT'}

1

nextstate{counter:?{?count:1}?}

現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)完美的記錄 state 修改日志的功能恐仑!

記錄異常

我又有一個(gè)需求泉坐,需要記錄每次數(shù)據(jù)出錯(cuò)的原因,我們擴(kuò)展下 dispatch

conststore?=?createStore(reducer);

constnext?=?store.dispatch;

store.dispatch?=(action)?=>{

try{

next(action);

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

這樣每次?dispatch?出異常的時(shí)候裳仆,我們都會(huì)記錄下來(lái)腕让。

多中間件的合作

我現(xiàn)在既需要記錄日志,又需要記錄異常歧斟,怎么辦纯丸?當(dāng)然很簡(jiǎn)單了,兩個(gè)函數(shù)合起來(lái)唄构捡!

store.dispatch?=(action)=>{

try{

console.log('this?state',?store.getState());

console.log('action',?action);

next(action);

console.log('next?state',?store.getState());

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

如果又來(lái)一個(gè)需求怎么辦液南?接著改?dispatch?函數(shù)?那再來(lái)10個(gè)需求呢勾徽?到時(shí)候 dispatch 函數(shù)肯定龐大混亂到無(wú)法維護(hù)了滑凉!這個(gè)方式不可取呀!

我們需要考慮如何實(shí)現(xiàn)擴(kuò)展性很強(qiáng)的多中間件合作模式喘帚。

1畅姊、我們把?loggerMiddleware?提取出來(lái)

conststore?=?createStore(reducer);

constnext?=?store.dispatch;

constloggerMiddleware?=(action)?=>{

console.log('this?state',?store.getState());

console.log('action',?action);

next(action);

console.log('next?state',?store.getState());

}

store.dispatch?=(action)?=>{

try{

loggerMiddleware(action);

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

2、我們把?exceptionMiddleware?提取出來(lái)

constexceptionMiddleware?=(action)?=>{

try{

/*next(action)*/

loggerMiddleware(action);

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

store.dispatch?=?exceptionMiddleware;

3吹由、現(xiàn)在的代碼有一個(gè)很嚴(yán)重的問(wèn)題若未,就是 exceptionMiddleware 里面寫死了loggerMiddleware,我們需要讓?next(action)變成動(dòng)態(tài)的倾鲫,隨便哪個(gè)中間件都可以

constexceptionMiddleware?=(next)?=>(action)?=>?{

try{

/*loggerMiddleware(action);*/

next(action);

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

/*loggerMiddleware?變成參數(shù)傳進(jìn)去*/

store.dispatch?=?exceptionMiddleware(loggerMiddleware);

4粗合、同樣的道理,loggerMiddleware?里面的?next?現(xiàn)在恒等于?store.dispatch乌昔,導(dǎo)致?loggerMiddleware?里面無(wú)法擴(kuò)展別的中間件了隙疚!我們也把 next 寫成動(dòng)態(tài)的

const?loggerMiddleware?=(next)=>(action)?=>?{

console.log('this?state',?store.getState());

console.log('action',?action);

next(action);

console.log('next?state',?store.getState());

}

到這里為止,我們已經(jīng)探索出了一個(gè)擴(kuò)展性很高的中間件合作模式磕道!

conststore?=?createStore(reducer);

constnext?=?store.dispatch;

constloggerMiddleware?=(next)?=>(action)?=>?{

console.log('this?state',?store.getState());

console.log('action',?action);

next(action);

console.log('next?state',?store.getState());

}

constexceptionMiddleware?=(next)?=>(action)?=>?{

try{

next(action);

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

store.dispatch?=?exceptionMiddleware(loggerMiddleware(next));

這時(shí)候我們開開心心的新建了一個(gè)?loggerMiddleware.js供屉,一個(gè)exceptionMiddleware.js文件,想把兩個(gè)中間件獨(dú)立到單獨(dú)的文件中去溺蕉。會(huì)碰到什么問(wèn)題嗎伶丐?

loggerMiddleware?中包含了外部變量?store,導(dǎo)致我們無(wú)法把中間件獨(dú)立出去疯特。那我們把 store 也作為一個(gè)參數(shù)傳進(jìn)去好了~

conststore?=?createStore(reducer);

constnext??=?store.dispatch;

constloggerMiddleware?=(store)?=>(next)?=>(action)?=>{

console.log('this?state',?store.getState());

console.log('action',?action);

next(action);

console.log('next?state',?store.getState());

}

constexceptionMiddleware?=(store)?=>(next)?=>(action)?=>{

try{

next(action);

}catch(err)?{

console.error('錯(cuò)誤報(bào)告:?',?err)

}

}

constlogger?=?loggerMiddleware(store);

constexception?=?exceptionMiddleware(store);

store.dispatch?=?exception(logger(next));

到這里為止哗魂,我們真正的實(shí)現(xiàn)了兩個(gè)可以獨(dú)立的中間件啦!

現(xiàn)在我有一個(gè)需求漓雅,在打印日志之前輸出當(dāng)前的時(shí)間戳啡彬。用中間件來(lái)實(shí)現(xiàn)羹与!

consttimeMiddleware?=(store)?=>(next)?=>(action)?=>{

console.log('time',newDate().getTime());

next(action);

}

...

const?time?=?timeMiddleware(store);

store.dispatch?=?exception(time(logger(next)));

本小節(jié)完整源碼見 demo-6

中間件使用方式優(yōu)化

上一節(jié)我們已經(jīng)完全實(shí)現(xiàn)了正確的中間件!但是中間件的使用方式不是很友好

importloggerMiddlewarefrom'./middlewares/loggerMiddleware';

importexceptionMiddlewarefrom'./middlewares/exceptionMiddleware';

importtimeMiddlewarefrom'./middlewares/timeMiddleware';

...

const?store?=?createStore(reducer);

constnext?=?store.dispatch;

constlogger?=?loggerMiddleware(store);

constexception?=?exceptionMiddleware(store);

consttime?=?timeMiddleware(store);

store.dispatch?=?exception(time(logger(next)));

其實(shí)我們只需要知道三個(gè)中間件庶灿,剩下的細(xì)節(jié)都可以封裝起來(lái)纵搁!我們通過(guò)擴(kuò)展 createStore 來(lái)實(shí)現(xiàn)!

先來(lái)看看期望的用法

/*接收舊的?createStore往踢,返回新的?createStore*/

constnewCreateStore?=?applyMiddleware(exceptionMiddleware,?timeMiddleware,?loggerMiddleware)(createStore);

/*返回了一個(gè)?dispatch?被重寫過(guò)的?store*/

conststore?=?newCreateStore(reducer);

實(shí)現(xiàn)?applyMiddleware

constapplyMiddleware?=function(...middlewares){

/*返回一個(gè)重寫createStore的方法*/

returnfunctionrewriteCreateStoreFunc(oldCreateStore){

/*返回重寫后新的?createStore*/

returnfunctionnewCreateStore(reducer,?initState){

/*1.?生成store*/

conststore?=?oldCreateStore(reducer,?initState);

/*給每個(gè)?middleware?傳下store腾誉,相當(dāng)于?const?logger?=?loggerMiddleware(store);*/

/*?const?chain?=?[exception,?time,?logger]*/

constchain?=?middlewares.map(middleware=>middleware(store));

letdispatch?=?store.dispatch;

/*?實(shí)現(xiàn)?exception(time((logger(dispatch))))*/

chain.reverse().map(middleware=>{

dispatch?=?middleware(dispatch);

});

/*2.?重寫?dispatch*/

store.dispatch?=?dispatch;

returnstore;

}

}

}

讓用戶體驗(yàn)美好

現(xiàn)在還有個(gè)小問(wèn)題,我們有兩種?createStore?了

/*沒有中間件的?createStore*/

import{?createStore?}from'./redux';

conststore?=?createStore(reducer,?initState);

/*有中間件的?createStore*/

constrewriteCreateStoreFunc?=?applyMiddleware(exceptionMiddleware,?timeMiddleware,?loggerMiddleware);

constnewCreateStore?=?rewriteCreateStoreFunc(createStore);

conststore?=?newCreateStore(reducer,?initState);

為了讓用戶用起來(lái)統(tǒng)一一些峻呕,我們可以很簡(jiǎn)單的使他們的使用方式一致利职,我們修改下createStore?方法

constcreateStore?=(reducer,?initState,?rewriteCreateStoreFunc)?=>{

/*如果有?rewriteCreateStoreFunc,那就采用新的?createStore?*/

if(rewriteCreateStoreFunc){

constnewCreateStore?=??rewriteCreateStoreFunc(createStore);

returnnewCreateStore(reducer,?initState);

}

/*否則按照正常的流程走*/

...

}

最終的用法

constrewriteCreateStoreFunc?=?applyMiddleware(exceptionMiddleware,?timeMiddleware,?loggerMiddleware);

conststore?=?createStore(reducer,?initState,?rewriteCreateStoreFunc);

本小節(jié)完整源碼見 demo-7

完整的 redux

退訂

不能退訂的訂閱都是耍流浪瘦癌!我們修改下 store.subscribe 方法猪贪,增加退訂功能

functionsubscribe(listener){

listeners.push(listener);

returnfunctionunsubscribe(){

constindex?=?listeners.indexOf(listener)

listeners.splice(index,1)

}

}

使用

constunsubscribe?=?store.subscribe(()=>{

letstate?=?store.getState();

console.log(state.counter.count);

});

/*退訂*/

unsubscribe();

中間件拿到的store

現(xiàn)在的中間件拿到了完整的?store,他甚至可以修改我們的?subscribe?方法讯私,按照最小開放策略热押,我們只用把?getState?給中間件就可以了!因?yàn)槲覀冎辉试S你用getState?方法斤寇!

修改下?applyMiddleware?中給中間件傳的?store

/*const?chain?=?middlewares.map(middleware?=>?middleware(store));*/

constsimpleStore?=?{getState:?store.getState?};

constchain?=?middlewares.map(middleware=>middleware(simpleStore));

compose

我們的?applyMiddleware?中桶癣,把?[A, B, C]?轉(zhuǎn)換成?A(B(C(next))),是這樣實(shí)現(xiàn)的

constchain?=?[A,?B,?C];

letdispatch?=?store.dispatch;

chain.reverse().map(middleware=>{

dispatch?=?middleware(dispatch);

});

redux 提供了一個(gè)?compose?方式娘锁,可以幫我們做這個(gè)事情

const?chain?=?[A,?B,?C];

dispatch?=?compose(...chain)(store.dispatch)

看下他是如何實(shí)現(xiàn)的

exportdefaultfunctioncompose(...funcs){

if(funcs.length?===1)?{

returnfuncs[0]

}

returnfuncs.reduce((a,?b)?=>(...args)?=>?a(b(...args)))

}

當(dāng)然?compose?函數(shù)對(duì)于新人來(lái)說(shuō)可能比較難理解牙寞,你只需要他是做什么的就行啦!

省略initState

有時(shí)候我們創(chuàng)建?store?的時(shí)候不傳?initState莫秆,我們?cè)趺从茫?/p>

conststore?=?createStore(reducer,?{},?rewriteCreateStoreFunc);

redux 允許我們這樣寫

conststore?=?createStore(reducer,?rewriteCreateStoreFunc);

我們僅需要改下?createStore?函數(shù)间雀,如果第二個(gè)參數(shù)是一個(gè)object,我們認(rèn)為他是?initState镊屎,如果是?function惹挟,我們就認(rèn)為他是?rewriteCreateStoreFunc。

functioncraeteStore(reducer,?initState,?rewriteCreateStoreFunc){

if(typeofinitState?==='function'){

rewriteCreateStoreFunc?=?initState;

initState?=undefined;

}

...

}

2 行代碼的 replaceReducer

reducer?拆分后杯道,和組件是一一對(duì)應(yīng)的。我們就希望在做按需加載的時(shí)候责蝠,reducer也可以跟著組件在必要的時(shí)候再加載党巾,然后用新的?reducer?替換老的?reducer。

constcreateStore?=function(reducer,?initState){

...

functionreplaceReducer(nextReducer){

reducer?=?nextReducer

/*刷新一遍?state?的值霜医,新來(lái)的?reducer?把自己的默認(rèn)狀態(tài)放到?state?樹上去*/

dispatch({?type:?Symbol()?})

}

...

return{

...

replaceReducer

}

}

我們來(lái)嘗試使用下

constreducer?=?combineReducers({

counter:?counterReducer

});

conststore?=?createStore(reducer);

/*生成新的reducer*/

constnextReducer?=?combineReducers({

counter:?counterReducer,

info:?infoReducer

});

/*replaceReducer*/

store.replaceReducer(nextReducer);

replaceReducer?示例源碼見 demo-5

bindActionCreators

bindActionCreators 我們很少很少用到齿拂,一般只有在 react-redux 的 connect 實(shí)現(xiàn)中用到。

他是做什么的肴敛?他通過(guò)閉包署海,把 dispatch 和 actionCreator 隱藏起來(lái)吗购,讓其他地方感知不到 redux 的存在。

我們通過(guò)普通的方式來(lái) 隱藏 dispatch 和 actionCreator 試試砸狞,注意最后兩行代碼

constreducer?=?combineReducers({

counter:?counterReducer,

info:?infoReducer

});

conststore?=?createStore(reducer);

/*返回?action?的函數(shù)就叫?actionCreator*/

functionincrement(){

return{

type:'INCREMENT'

}

}

functionsetName(name){

return{

type:'SET_NAME',

name:?name

}

}

constactions?=?{

increment:function(){

returnstore.dispatch(increment.apply(this,arguments))

},

setName:function(){

returnstore.dispatch(setName.apply(this,arguments))

}

}

/*注意:我們可以把 actions 傳到任何地方去*/

/*其他地方在實(shí)現(xiàn)自增的時(shí)候捻勉,根本不知道?dispatch,actionCreator等細(xì)節(jié)*/

actions.increment();/*自增*/

actions.setName('九部威武');/*修改?info.name*/

我眼睛一看刀森,這個(gè)?actions?生成的時(shí)候踱启,好多公共代碼,提取一下

constactions?=?bindActionCreators({?increment,?setName?},?store.dispatch);

來(lái)看一下?bindActionCreators?的源碼研底,超級(jí)簡(jiǎn)單(就是生成了剛才的 actions)

/*核心的代碼在這里埠偿,通過(guò)閉包隱藏了?actionCreator?和?dispatch*/

functionbindActionCreator(actionCreator,?dispatch){

returnfunction(){

returndispatch(actionCreator.apply(this,arguments))

}

}

/*?actionCreators?必須是?function?或者?object?*/

exportdefaultfunctionbindActionCreators(actionCreators,?dispatch){

if(typeofactionCreators?==='function')?{

returnbindActionCreator(actionCreators,?dispatch)

}

if(typeofactionCreators?!=='object'||?actionCreators?===null)?{

thrownewError()

}

constkeys?=Object.keys(actionCreators)

constboundActionCreators?=?{}

for(leti?=0;?i?<?keys.length;?i++)?{

constkey?=?keys[i]

constactionCreator?=?actionCreators[key]

if(typeofactionCreator?==='function')?{

boundActionCreators[key]?=?bindActionCreator(actionCreator,?dispatch)

}

}

returnboundActionCreators

}

bindActionCreators?示例源碼見 demo-8

大功告成

完整的示例源碼見 demo-9,你可以和 redux 源碼做一下對(duì)比榜晦,你會(huì)發(fā)現(xiàn)冠蒋,我們已經(jīng)實(shí)現(xiàn)了 redux 所有的功能了。

當(dāng)然乾胶,為了保證代碼的理解性抖剿,我們少了一些參數(shù)驗(yàn)證。比如 createStore(reducer)的參數(shù) reducer 必須是 function 等等胚吁。

最佳實(shí)踐

純函數(shù)

什么是純函數(shù)牙躺?

純函數(shù)是這樣一種函數(shù),即相同的輸入腕扶,永遠(yuǎn)會(huì)得到相同的輸出孽拷,而且沒有任何可觀察的副作用。

通俗來(lái)講半抱,就兩個(gè)要素

相同的輸入脓恕,一定會(huì)得到相同的輸出

不會(huì)有 “觸發(fā)事件”,更改輸入?yún)?shù)窿侈,依賴外部參數(shù)炼幔,打印 log 等等副作用

/*不是純函數(shù),因?yàn)橥瑯拥妮斎胧芳颍敵鼋Y(jié)果不一致*/

functiona(?count?){

returncount?+Math.random();

}

/*不是純函數(shù)乃秀,因?yàn)橥獠康?arr?被修改了*/

functionb(?arr?){

returnarr.push(1);

}

letarr?=?[1,2,3];

b(arr);

console.log(arr);//[1,?2,?3,?1]

/*不是純函數(shù),以為依賴了外部的?x*/

letx?=1;

functionc(?count?){

returncount?+?x;

}

我們的?reducer?計(jì)劃函數(shù)圆兵,就必須是一個(gè)純函數(shù)跺讯!

只要傳入?yún)?shù)相同,返回計(jì)算得到的下一個(gè) state 就一定相同殉农。沒有特殊情況刀脏、沒有副作用,沒有 API 請(qǐng)求超凳、沒有變量修改愈污,單純執(zhí)行計(jì)算耀态。

總結(jié)

到了最后,我想把 redux 中關(guān)鍵的名詞列出來(lái)暂雹,你每個(gè)都知道是干啥的嗎首装?

createStore

創(chuàng)建 store 對(duì)象,包含 getState, dispatch, subscribe, replaceReducer

reducer

reducer 是一個(gè)計(jì)劃函數(shù)擎析,接收舊的 state 和 action簿盅,生成新的 state

action

action 是一個(gè)對(duì)象,必須包含 type 字段

dispatch

dispatch( action ) 觸發(fā) action揍魂,生成新的 state

subscribe

實(shí)現(xiàn)訂閱功能桨醋,每次觸發(fā) dispatch 的時(shí)候,會(huì)執(zhí)行訂閱函數(shù)

combineReducers

多 reducer 合并成一個(gè) reducer

replaceReducer

替換 reducer 函數(shù)

middleware

擴(kuò)展 dispatch 函數(shù)现斋!

你再看 redux 流程圖喜最,是不是大徹大悟了?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末庄蹋,一起剝皮案震驚了整個(gè)濱河市瞬内,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌限书,老刑警劉巖虫蝶,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異倦西,居然都是意外死亡能真,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門扰柠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)粉铐,“玉大人,你說(shuō)我怎么就攤上這事卤档◎茫” “怎么了?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵劝枣,是天一觀的道長(zhǎng)汤踏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)舔腾,這世上最難降的妖魔是什么溪胶? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮琢唾,結(jié)果婚禮上载荔,老公的妹妹穿的比我還像新娘盾饮。我一直安慰自己采桃,他們只是感情好懒熙,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著普办,像睡著了一般工扎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衔蹲,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天肢娘,我揣著相機(jī)與錄音,去河邊找鬼舆驶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的础锐。 我是一名探鬼主播胖翰,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼撬陵!你這毒婦竟也來(lái)了珊皿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤巨税,失蹤者是張志新(化名)和其女友劉穎蟋定,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體草添,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驶兜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了果元。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片促王。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖而晒,靈堂內(nèi)的尸體忽然破棺而出蝇狼,到底是詐尸還是另有隱情,我是刑警寧澤倡怎,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布迅耘,位于F島的核電站,受9級(jí)特大地震影響监署,放射性物質(zhì)發(fā)生泄漏颤专。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一钠乏、第九天 我趴在偏房一處隱蔽的房頂上張望栖秕。 院中可真熱鬧,春花似錦晓避、人聲如沸簇捍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)暑塑。三九已至吼句,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間事格,已是汗流浹背惕艳。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驹愚,地道東北人远搪。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像逢捺,于是被迫代替她去往敵國(guó)和親终娃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容