前言
react
和狀態管理redux
是緊密結合的,而本身又沒有任何聯絡。react
可以不使用redux
管理狀態,redux
也可以脫離react
獨立存在。隨著react
的專案越來越複雜,state
變的繁重,各種prop
和state
的轉變讓我們在開發過程中變得頭暈眼花,react
本來就是一個專注於UI層的庫,本不應該讓繁雜的prop
和state
的邏輯摻雜進來。於是Flux
的架構出現了,Flux
架構模式用於抽離react
的state
能更好的去構建專案,Flux
架構模式的實踐有好多中,顯然redux
是成功的。
我在接觸react
和redux
之前總是聽好多人提起redux
這個東西,我心想它到底有什麼魔力,讓那麼多的人為之驚歎,今天就來揭開redux
的真面目。
redux
前面提到redux
是可以脫離react
存在的,這句話的意思是redux
並不是依附於react
的,即便是用jQuery
+redux
也是可以的。redux
提供的是一種狀態管理的方式,同時也定義了一種管理狀態的規則,所有需要使用這個小而美的庫的專案都必須遵循這個規則,也正是這個規則使用redux再書寫過程中有了可預測性和可追溯性。
redux的設計原則
談redux
必然要談談它的設計原則,就如同想要更明白的瞭解一樣東西,就需要先了解它是怎麼來的,當然歷史明白上面這些就夠了。
redux
有三大設計原則
- 單一資料來源
- 狀態是隻讀的
- 使用純函式編寫reducer
1.單一資料來源
單一資料來源的意思是說整個react
專案的state
都存放在一起,也可以認為存在一個大物件中,單一資料來源可以讓我們在專案中更專注於資料來源的設計與構建上。
2.狀態是隻讀的
使用過redux
都知道,檢視是通過store.getState()
方法來獲取狀態的,通過dispatch
派發action
來改變狀態。狀態是隻讀的也就是說我們只能通過stiore.getState()
來獲取狀態,只能通過dispatch
派發action
來改變狀態。這也體現了單一資料流動,讓我們在構建專案的時候只需要關於一個方向的資料流動。
3.使用純函式編寫reducer
我當時在學的時候也是有這樣的疑問:為什麼要使純函式來寫,什麼是純函式?
所謂純函式:對於一個函式來說相同的輸入必定有相同的輸出, 即不依賴外部環境,也不改變外部環境,這樣的函式就叫做純函式。純函式純的,是沒有副作用的。
明白了純函式,那麼在寫reducer
的時候一定見過這麼一段程式碼。
const state = reducer(initstate = {},action);
複製程式碼
上面程式碼,再結合純函式,就可以說對於特定的action
和initstate
必定會得到相同的state
,這裡正是體現了redux
的可預測性。
redux的四個角色
redux
提供了一系列規則來規定我們來寫程式碼。可以大致分為四個角色:
- action
- reducer
- dispatch
- store
1.action
action
是承載狀態的載體,一般action
將檢視所產出的資料,傳送到reducer進行處理。action
的書寫格式一般是這樣:
const addAction = {
type:"ADD",
value:.....
}
複製程式碼
action
其實就是一個JavaScript物件,它必須要有一個type屬性用來標識這個action
是幹嘛的(也可以認為家的地址,去reducer中找家),value屬性是action攜帶來自檢視的資料。
action
的表示方式也可以是一個函式,這樣可以更方面的構建action
,但這個函式必須返回一個物件。
const addAction = (val) => ({
type:"ADD",
value: val
})
複製程式碼
這樣拿到的資料就靈活多了。
對於action
的type屬性,一般如果action變的龐大的話會把所有的type抽離出來到一個constants中,例如:
const ADDTODO = 'ADDTODO',
const DELETETODO = 'DELETEDOTO'
export {
ADDTODO,
DELETETODO,
}
複製程式碼
這樣可以讓type更清晰一些。
2.reducer
reducer
指定了應用狀態的變化如何響應 actions
併傳送到 store
。 在redux
的設計原則中提到使用純函式來編寫reducer
,目的是為了讓state變的可預測。reducer
的書寫方式一般是這樣:
const reducer = (state ={},action){
switch(action.type){
case :
......
case :
......
case :
......
default :
return state;
}
}
複製程式碼
使用switch判斷出什麼樣的action
應該使用什麼樣的邏輯去處理。
拆分reducer
當隨著業務的增多,那麼reducer
也隨著增大,顯然一個reducer
是不可能的,於是必須要拆分reducer
,拆分reducer
也是有一定的套路的:比如拆分一個TodoList,就可以把todos操作放在一起,把對todo無關的放在一起,最終形成一個根reducer。
function visibilityFilter(state,action){
switch(action.type){
case :
......
case :
......
default :
return state;
}
}
function todos(state,action){
switch(action.type){
case :
......
case :
......
default :
return state;
}
}
//根reducer
function rootReducer(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
複製程式碼
這樣做的好處在於業務邏輯的分離,讓根reducer
不再那麼繁重。好在redux
提供了combineReducers
方法用於構建rootReducer
const rootReducer = combineReducers({
visibilityFilter,
todos,
})
複製程式碼
這部分程式碼和上面rootReducer的作用完全相同。它的原理是通過傳入物件的key-value把所有的state進行一個糅合。
3.dispatch
dispatch
的作用是派發一個action
去執行reducer
。我覺得dispatch
就是一個釋出者,和subscribe
一起組合成訂閱釋出者模式。使dispatch
派發:
const action = {
type: "ADD",
value: "Hello Redux",
}
dispatch(action);
複製程式碼
4.store
store
可以說是redux
的核心了。開頭也提到store
是redux
狀態管理的唯一資料來源,除此之外,store
還是將dispatch
、reducer
等聯絡起來的命脈。
store
通過redux
提供的createStore
建立,它是一個物件,有如下屬性:
- store.getState() 獲取狀態的唯一途徑
- store.dispatch(action) 派發action響應reducer
- store.subscribe(handler) 監聽狀態的變化
建立store:
const store = Redux.createStore(reducer,initialState,enhancer);
//1. reducer就是我們書寫的reducer
//2. initialState是初始化狀態
//3. enhancer是中介軟體
複製程式碼
Middleware
在建立store
的時候createStore
是可以傳入三個引數的,第三個引數就是中介軟體,使用redux
提供的applyMiddleware
來呼叫,applyMiddleware
相當於是對dispatch
的一種增強,通過中介軟體可以在dispatch
過程中做一些事情,比如打logger、thunk(非同步action)等等。
使用方式如下:
//非同步action中介軟體
import thunk from "redux-thunk";
const store = Redux.createStore(reducer,initialState,applMiddleware(thunk));
複製程式碼
思想先告一段落,既然懂得了redux
的思想,那麼接下來手下一個簡易版的redux
。
手寫一個min-Redux
新的react-hooks中除了useReducer
,整合了redux
的功能,為什麼還要深入瞭解redux
呢?
隨著前端技術的迭代,技術的快速更新,我們目前並沒有能力去預知或者去引領前端的發展,唯一能做的就是在時代中吸收知識並消化知識,雖然未來有可能redux
會被useReducer
所取代,但是思想是不變的,redux
這個小而美的庫設計是奇妙的,也許有哪一天在寫業務的時候遇到了某種相似的需求,我們也可以通過藉助於這個庫的思想去做一些事情。
createStore
要想了解redux
,必然要先了解它的核心,它的核心就是createStore
這個函式,store
、getState
,dispatch
都在這裡產出。我個人覺得createStore
是一個提供一系列方法的訂閱釋出者模式:通過subscribe
訂閱store
的變化,通過dispatch
派發。那麼下面就來實現一下這個createStore
。
從上面store
中可以看出。建立一個store
需要三個引數;
//1.接受的rootReducer
//2.初始化的狀態
//3.dispatch的增強器(中介軟體)
const createStore = (reducer,initialState,enhancer) => {
};
複製程式碼
createStore
還返回一些列函式介面提供呼叫
const crateStore = (reducer, initialState, enhancer) => {
return {
getState,
dispatch,
subscribe,
replaceReducer,
}
}
複製程式碼
以下程式碼都是在createStore內部
getState的實現
getStore
方法的作用就是返回當前的store
。
let state = initialState;
const getState = () => {
return state;
}
複製程式碼
subscribe的實現
subscribe
是createStore
的訂閱者,開發者通過這個方法訂閱,當store
改變的時候執行監聽函式。subscribe
是典型的高階函式,它的返回值是一個函式,執行該函式移除當前監聽函式。
//建立一個監聽時間佇列
let subQueue = [];
const subscribe = (listener) => {
//把監聽函式放入到監聽佇列裡面
subQueue.push(listener);
return () => {
//找到當前監聽函式的索引
let idx = subQueue.indexOf(listener);
if(idx > -1){
//通過監聽函式的索引把監聽函式移除掉。
subQueue.splice(idx,1);
}
}
}
複製程式碼
dispatch的實現
dispatch
是createStore
的釋出者,dispatch
接受一個action
,來執行reducer
。dispatch
在執行reducer
的同時會執行所有的監聽函式(也就是釋出)。
let currentReducer = reducer;
let isDispatch = false;
const dispatch = (action) => {
//這裡使用isDispatch做標示,就是說只有當上一個派發完成之後才能派發下一個
if(isDispatch){
throw new Error("dispatch error");
}
try{
state = currentReducer(state,action);
isDispatch = true;
}finally{
isDispatch = false;
}
//執行所有的監聽函式
subQueue.forEach(sub => sub.apply(null));
return action;
}
複製程式碼
replaceReducer
replaceReducer
顧名思義就是替換reducer
的意思。再執行createState
方法的時候reducer
就作為第一個引數傳進去,如果後面想要重新換一個reducer
,來程式碼寫一下。
const replaceReducer = (reducer) => {
//傳入一個reduce作為引數,把它賦予currentReducer就可以了。
currentReducer = reducer;
//更該之後會派發一次dispatch,為什麼會派發等下再說。
dispatch({type:"REPLACE"});
}
複製程式碼
dispatch({type:"INIT"});
上面已經實現了createStore
的四個方法,剩下的就是replaceReducer
中莫名的派發了一個type
為REPLACE
的action
,而且翻到原始碼的最後,也派發一個type
為INIT
的action
,為什麼呢?
其實當使用createStore
建立Store
的時候,我們都知道,第一個引數為reducer
,第二個引數為初始化的state
。當如果不寫第二個引數的時候,我們再來看一下reducer
的寫法
const reducer = (state = {}, action){
switch(action.type){
default:
return state;
}
}
複製程式碼
一般在寫reducer
的時候都會給state
寫一個預設值,並且default
出預設的state
。當createStore
不存在,這個預設值如何儲存在Store
中呢?就是這個最後派發的type:INIT
的作用。在replaceReducer
中派發也是這個原因,更換reducer
後派發。
完整的createStore
現在已經實現的差不多了,只要再加一些容錯就可以了。
/**
*
* @param {*} reducer //reducer
* @param {*} initState //初始狀態
* @param {*} middleware //中介軟體
*/
const createStore = (reducer, initState,enhancer) => {
let initialState; //用於儲存狀態
let currentReducer = reducer; //reducer
let listenerQueue = []; //存放所有的監聽函式
let isDispatch = false;
if(initState){
initialState = initState;
}
if(enhancer){
return enhancer(createStore)(reducer,initState);
}
/**
* 獲取Store
*/
const getState = () => {
//判斷是否正在派發
if(isDispatch){
throw new Error('dispatching...')
}
return initialState;
}
/**
* 派發action 並觸發所有的listeners
* @param {*} action
*/
const dispatch = (action) => {
//判斷是否正在派發
if(isDispatch){
throw new Error('dispatching...')
}
try{
isDispatch = true;
initialState = currentReducer(initialState,action);
}finally{
isDispatch = false;
}
//執行所有的監聽函式
for(let listener of listenerQueue){
listener.apply(null);
}
}
/**
* 訂閱監聽
* @param {*} listener
*/
const subscribe = (listener) => {
listenerQueue.push(listener);
//移除監聽
return function unscribe(){
let index = listenerQueue.indexOf(listener);
let unListener = listenerQueue.splice(index,1);
return unListener;
}
}
/**
* 替換reducer
* @param {*} reducer
*/
const replaceReducer = (reducer) => {
if(reducer){
currentReducer = reducer;
}
dispatch({type:'REPLACE'});
}
dispatch({type:'INIT'});
return {
getState,
dispatch,
subscribe,
replaceReducer
}
}
export default createStore;
複製程式碼
compose
在redux
中提供了一個組合函式,如果你知道函數語言程式設計的話,那麼對compose
一定不陌生。如果不瞭解的話,那我說一個場景肯定就懂了。
//有fn1,fn2,fn3這三個函式,寫出一個compose函式實現一下功能
//1. compose(fn1,fn2,fn3) 從右到左執行。
//2. 上一個執行函式的結果作為下一個執行函式的引數。
const compose = (...) => {
}
複製程式碼
上面的需求就是compose
函式,也是一個常考的面試題。如何實現實現一個compose
?一步一步來。
首先compose
接受的是一系列函式。
const compose = (...fns) => {
}
複製程式碼
從右到左執行,我們採用陣列的reduce
方法,利用惰性求值的方式。
const compose = (...fns) => fns.reduce((f,g) => (...args) => f(g(args)));
複製程式碼
這就是一個compose
函式。
揭開中介軟體的祕密-applayMiddleware
redux
中的中介軟體就是對dispatch
的一種增強,在createStore
中實現這個東西很簡單。原始碼如下:
const createStore = (reducer,state,enhancer) => {
//判斷第三個引數的存在。
if(enhancer && type enhancer === 'function') {
//滿足enhance存在的條件,直接return,組織後面的執行。
//通過柯里化的方式傳參
//為什麼傳入createStore?
//雖然是增強,自然返回之後依然是一個store物件,所以要使用createStore做一些事情。
//後面兩個引數
//中介軟體是增強,必要的reducer和state也必要通過createStore傳進去。
return enhancer(crateStore)(reducer,state);
}
}
複製程式碼
上面就是中介軟體再createStore
中的實現。
中介軟體的構建通過applyMiddleware
實現,來看一下applyMiddleware
是怎麼實現。由上面可以看出applyMiddleware
是一個柯里化函式。
const applyMiddleware = (crateStore) => (...args) => {
}
複製程式碼
在applyMiddleware
中需要執行createStore
來得到介面方法。
const applyMiddleware =(...middlewares) => (createStore) => (...args) => {
let store = createStore(...args);
//佔位dispatch,避免在中介軟體過程中呼叫
let dispatch = () => {
throw new Error('error')
}
let midllewareAPI = {
getState: store.getState,
dispatch,
}
//把middlewareAPI傳入每一箇中介軟體中
const chain = middlewares.map(middleware => middleware(middlewareAPI));
//增強dispatch生成,重寫佔位dispatch,把store的預設dispatch傳進去,
dispatch = compose(...chain)(store.dispatch);
//最後把增強的dispatch和store返回出去。
return {
...store,
dispatch
}
}
複製程式碼
上面就是applyMiddleware
的實現方法。
如何寫一箇中介軟體
根據applyMiddleware
中介軟體引數的傳入,可以想出一個基本的中介軟體是這樣的:
const middleware = (store) => next => action => {
//業務邏輯
//store是傳入的middlewareAPI
//next是store基礎的dispatch
//action是dispatch的action
}
複製程式碼
這就是一箇中介軟體的邏輯了。
非同步action
在寫邏輯的時候必然會用到非同步資料的,我們知道reducer
是純函式,不允許有副作用操作的,從上面到現在也可以明白整個redux
都是函數語言程式設計的思想,是不存在副作用的,那麼非同步資料怎麼實現呢?必然是通過applyMiddleware
提供的中介軟體介面實現了。
非同步中介軟體必須要求action
是一個函式,根據上面中介軟體的邏輯,我們來寫一下。
const middleware = (store) => next => action => {
if(typeof action === 'function'){
action(store.dispatch,store.getState);
}
next(action);
}
複製程式碼
判斷傳入的action
是否是一個函式,如果是函式使用增強dispatch
,如果不是函式使用普通的dispatch
。
總結
到此為止就是我能力範圍內所理解的Redux
。我個人認為,要學習一個東西一定要看一下它的原始碼,學習它的思想。技術更新迭代,思想是不變的,無非就是思想的轉變。如果有不對的地方,還望大佬們指點。