寫在前面
用最基礎的方法講解 Redux 實現原理?說白了其實是我能力有限,只能用最基礎的方法來講解,為了講的更加清楚,文章可能比較拖沓。不過我相信,不是很瞭解 Redux 的同學,看完我今天分享的文章一定會有所收穫!
什麼是 Redux ?
這不是我今天要說的重點,想知道什麼是 Redux 點選傳送門
開始
在開始之前我想先講一種常用的設計模式:觀察者模式。先來說一下我對
觀察者模式
的個人理解:觀察者模式(Publish/Subscribe)模式。對於這種模式很清楚的同學下面這段程式碼可以跳過。如果你還不清楚,你可以試著手敲一遍下面的程式碼!!
觀察者模式
觀察者模式,基於一個主題/事件通道,希望接收通知的物件(稱為subscriber)通過自定義事件訂 閱主題,通過deliver釋出主題事件的方式被通知。就和使用者訂閱微信公眾號道理一樣,只要釋出,使用者就能接收到最新的內容。
/**
* describe: 實現一個觀察者模式
*/
let data = {
hero: '鳳凰',
};
//用來儲存 訂閱者 的陣列
let subscribers = [];
//訂閱 新增訂閱者 方法
const addSubscriber = function(fn) {
subscribers.push(fn)
}
//釋出
const deliver = function(name) {
data.hero = name;
//當資料發生改變,呼叫(通知)所有方法(訂閱者)
for(let i = 0; i<subscribers.length; i++){
const fn = subscribers[i]
fn()
}
}
//通過 addSubscriber 發起訂閱
addSubscriber(() => {
console.log(data.hero)
})
//改變data,就會自動列印名稱
deliver('發條')
deliver('狐狸')
deliver('卡牌')
複製程式碼
這個釋出訂閱通過 addSubscriber 來儲存訂閱者(方法fn),當通過呼叫 deliver 來改變資料的時候,就會自動遍歷 addSubscriber 來執行裡面的 fn 方法 。
為啥要講這個釋出訂閱模式呢?因為搞清楚了這個模式那麼接下來你讀該文章就會感覺更加清晰。
Redux 起步
首先我們把上面那個釋出訂閱程式碼優化一下,順便改一下命名,為什麼要改命名?主要是緊跟 Redux 的步伐。讓同學們更加眼熟。
let state = {hero: '鳳凰'};
let subscribers = [];
//訂閱 定義一個 subscribe
const subscribe = (fn) => {
subscribers.push(fn)
}
//釋出
const dispatch = (name) => {
state.hero = name;
//當資料發生改變,呼叫(通知)所有方法(訂閱者)
subscribers.forEach(fn=>fn())
}
//通過 subscribe 發起訂閱
subscribe(() => {
console.log(state.hero)
})
//改變state狀態,就會自動列印名稱
//這裡要注意的是,state狀態不能直接去修改
dispatch('發條')
dispatch('狐狸')
dispatch('卡牌')
複製程式碼
現在這樣一改是不是很眼熟了,沒錯這就是一個類似redux改變狀態的思路。但是光一個釋出訂閱還是不夠的,不可能改變一個狀態需要去定義這麼多方法。所以我們把他封裝起來。
creatStore 方法
const creatStore = (initState) => {
let state = initState;
let subscribers = [];
//訂閱 定義一個 subscribe
const subscribe = (fn) => {
subscribers.push(fn)
}
//釋出
const dispatch = (currentState) => {
state = currentState;
//當資料發生改變,呼叫(通知)所有方法(訂閱者)
subscribers.forEach(fn=>fn())
}
// 這裡需要新增這個獲取 state 的方法
const getState = () => {
return state;
}
return {
subscribe,
dispatch,
getState,
}
}
複製程式碼
這樣就建立好了一個 createStore 方法。沒有什麼新東西,就傳進去一個初始狀態,然後在返回 subscribe, dispatch, getState 三大方法。這裡新增了個 getState 方法,程式碼很簡單就是一個 return state 為了獲取 state.
creatStore 使用
實現了 createStore 下面我們來試試如何使用他,那就拿那個非常經典的案例--計數器來試試
let initState = {
num: 0,
}
const store = creatStore(initState);
//訂閱
store.subscribe(() => {
let state = store.getState()
console.log(state.num)
})
// +1
store.dispatch({
num: store.getState().num + 1
})
//-1
store.dispatch({
num: store.getState().num - 1
})
複製程式碼
這個樣子又接近了一點 Redux 的模樣。 不過這樣有個問題。如果你使用 store.dispatch 方法時,中間萬一寫錯了或者傳了個其他東西那就比較麻煩了。就比如下面這樣:
其實我是想 +1,+1,-1 最後應該是 1 (初始 num 為0)!但是由於寫錯了一個導致後面的都會錯。而且他還有個問題就是可以隨便的給一個新的狀態。那麼就顯得不那麼單純了。比如下面這樣:
因為惡意修改 num 為 String 型別,導致後面在使用 dispatch 由於 num 不再是 Number 型別,導致列印出 NaN,這就不是我們想要的啦。所以我們要在改造一下,讓 dispatch 變得單純一些。那要怎麼做呢?我們請一個管理者來幫我們管理,暫且給他命名 reducer
為什麼叫 reducer
我在 reducer 官網中找到下面這段介紹 reducer
什麼意思,對於這種英語上來我就是有道翻譯一下當然這個翻譯感覺並沒什麼作用,
找一找中文 Redux 官網,他是這樣說的:
之所以將這樣的函式稱之為reducer,是因為這種函式與被傳入 Array.prototype.reduce(reducer, ?initialValue) 裡的回撥函式屬於相同的型別。保持 reducer 純淨非常重要。永遠不要在 reducer 裡做這些操作。
誒,這個翻譯似乎就清楚了很多。正如下面評論者說的一樣 靈感來自於陣列中reduce方法,是一種運算合成。那麼說到這裡我就來介紹一下 reduce。
什麼是 reduce
話不多說直接上程式碼
const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10
// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15
/*該減速作用有四個引數:
*累加器(acc)
*當前價值(cur)
*當前指數(idx)
*源陣列(src)
*您的reducer函式的返回值被分配給累加器,其值在整個陣列的每次迭代中被記住,並最終成為最終的單個結果值。
*/
複製程式碼
具體引數介紹
callback
函式在陣列中的每個元素上執行,有四個引數:
accumulator
累加器累加回撥的返回值; 它是先前在回撥呼叫中返回的累計值,或者initialValue,如果提供(參見下文)。
currentValue
當前元素在陣列中處理。
currentIndex可選的
陣列中正在處理的當前元素的索引。如果initialValue提供了an,則從索引0開始,否則從索引1開始
array可選的
該陣列reduce()被召喚。
initialValue可選的
用作第一次呼叫的第一個引數的值callback。如果未提供初始值,則將使用陣列中的第一個元素。呼叫reduce()沒有初始值的空陣列是一個錯誤。
複製程式碼
這個方法相對比 forEach, map, filter 這個理解起來還是算比較困難的。也可以看 MDN 的 Array.prototype.reduce() 詳細介紹
注:首先感謝下面評論者 panda080 的指導,受他的建議,我重新去 Rudex 官網尋找。通過學習自己也更加的理解了 reducer 和 reduce reducer官網
ps:理解完之後,其實個人覺得 reducer 這個命名從翻譯過來的角度總覺得很怪異。可能英語有限,或許他有更加貼切的意思我還不知道。
什麼是 reducer
reducer 在我學習的過程中我把他認為是個管理者(可能這個認為是不正確的),然後我們每次想做什麼就去通知管理者,讓他在來根據我們說的去做。如果我們不小心說錯了,那麼他就不會去做。直接按預設的事情來。噔噔蹬蹬 reducer 登場!!
function reducer(state, action) {
//通過傳進來的 action.type 讓管理者去匹配要做的事情
switch (action.type){
case 'add':
return {
...state,
count: state.count + 1
}
case 'minus':
return {
...state,
count: state.count - 1
}
// 沒有匹配到的方法 就返回預設的值
default:
return state;
}
}
複製程式碼
增加了這個管理者,那麼我們就要重新來寫一下之前的 createStroe 方法了:把 reducer 放進去
const creatStore = (reducer,initState) => {
let state = initState;
let subscribers = [];
//訂閱 定義一個 subscribe
const subscribe = (fn) => {
subscribers.push(fn)
}
//釋出
const dispatch = (action) => {
state = reducer(state,action);
subscribers.forEach(fn=>fn())
}
const getState = () => {
return state;
}
return {
subscribe,
dispatch,
getState,
}
}
複製程式碼
很簡單的一個修改,為了讓你們方便看出修改的地方,和區別,我特意重新碼了這兩個前後的方法對比,如下圖
好,接下來我們試試新增了管理者的 creatStore 效果如何。
function reducer(state, action) {
//通過傳進來的 action.type 讓管理者去匹配要做的事情
switch (action.type){
case 'add':
return {
...state,
num: state.num + 1
}
case 'minus':
return {
...state,
num: state.num - 1
}
// 沒有匹配到的方法 就返回預設的值
default:
return state;
}
}
let initState = {
num: 0,
}
const store = creatStore(reducer,initState);
//訂閱
store.subscribe(() => {
let state = store.getState()
console.log(state.num)
})
複製程式碼
為了看清楚結果,dispatch(訂閱)我直接在控制檯輸出,如下圖:
效果很好,我們不會再因為寫錯,而出現 NaN 或者其他不可描述的問題。現在這個 dispatch 比較純粹了一點。
我們只是給他一個 type ,然後讓管理者自己去幫我們處理如何更改狀態。如果不小心寫錯,或者隨便給個 type 那麼管理者匹配不到那麼這個動作那麼我們這次 dispatch 就是無效的,會返回我們自己的預設 state。
好叻,現在這個樣子基本上就是我腦海中第一次使用 redux 看到的樣子。那個時候我使用起來都非常困難。當時勉強實現了一下這個計數器 demo 我就默默的關閉了 vs code。
接下來我們再完善一下這個 reducer,給他再新增一個方法。並且這次我們再給 state 一個
function reducer(state, action) {
//通過傳進來的 action.type 讓管理者去匹配要做的事情
switch (action.type){
case 'add':
return {
...state,
num: state.num + 1
}
case 'minus':
return {
...state,
num: state.num - 1
}
// 增加一個可以傳參的方法,讓他更加靈活
case 'changeNum':
return {
...state,
num: state.num + action.val
}
// 沒有匹配到的方法 就返回預設的值
default:
return state;
}
}
let initState = {
num: 0,
}
const store = creatStore(reducer,initState);
//訂閱
store.subscribe(() => {
let state = store.getState()
console.log(state.num)
})
複製程式碼
控制檯再使用一次新的方法:
好叻,這樣是不是就讓 dispatch 更加靈活了。
現在我們在 reducer 中就寫了 3 個方法,但是實際專案中,方法一定是很多的,那麼都這樣寫下去,一定是不利於開發和維護的。那麼這個問題就留給大家去思考一下。
提示:Redux 也知道這一點,所以他提供了
combineReducers
去實現這個模式。這是一個高階 Reducer 的示例,他接收一個拆分後 reducer 函式組成的物件,返回一個新的 Reducer 函式。
思考完之後可以參考 redux 中文文件 的combineReducers介紹
具體實現原理我將會在下次分享。
總結
Redux 這個專案裡,有很多非常巧妙的方法,很多地方可以借鑑。畢竟這可是在 github 上有 47W+ 的 Star。
今天也只是講了他的一小部分。自己也在努力學習中,希望今後能分享更多的看法,並和大家深入探討。
寫在最後
上述每個案例,和程式碼我都託管在 github 上了,分享給大家可以直接開啟即用。github 傳送門
全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!
參考:
基於下面 dsying 評論。我又扒了一上午找到曾經看的這個觀察者模式,如有知道思路類似哪裡的可以提供一個連結給我,必修改上去。