react
和狀態管理redux
是緊密結合的,而自己又沒有任何聯繫。react
能夠不使用redux
管理狀態,redux
也能夠脫離react
獨立存在。隨着react
的項目愈來愈複雜,state
變的繁重,各類prop
和state
的轉變讓咱們在開發過程當中變得頭暈眼花,react
原本就是一個專一於UI層的庫,本不該該讓繁雜的prop
和state
的邏輯摻雜進來。因而Flux
的架構出現了,Flux
架構模式用於抽離react
的state
能更好的去構建項目,Flux
架構模式的實踐有好多中,顯然redux
是成功的。前端
我在接觸react
和redux
以前老是聽好多人提起redux
這個東西,我心想它到底有什麼魔力,讓那麼多的人爲之驚歎,今天就來揭開redux
的真面目。react
前面提到redux
是能夠脫離react
存在的,這句話的意思是redux
並非依附於react
的,即使是用jQuery
+redux
也是能夠的。redux
提供的是一種狀態管理的方式,同時也定義了一種管理狀態的規則,全部須要使用這個小而美的庫的項目都必須遵循這個規則,也正是這個規則使用redux再書寫過程當中有了可預測性和可追溯性。面試
談redux
必然要談談它的設計原則,就如同想要更明白的瞭解同樣東西,就須要先了解它是怎麼來的,固然歷史明白上面這些就夠了。編程
redux
有三大設計原則redux
單一數據源的意思是說整個react
項目的state
都存放在一塊兒,也能夠認爲存在一個大對象中,單一數據源可讓咱們在項目中更專一於數據源的設計與構建上。數組
使用過redux
都知道,視圖是經過store.getState()
方法來獲取狀態的,經過dispatch
派發action
來改變狀態。狀態是隻讀的也就是說咱們只能經過stiore.getState()
來獲取狀態,只能經過dispatch
派發action
來改變狀態。這也體現了單一數據流動,讓咱們在構建項目的時候只須要關於一個方向的數據流動。架構
我當時在學的時候也是有這樣的疑問:爲何要使純函數來寫,什麼是純函數?併發
所謂純函數:對於一個函數來講相同的輸入一定有相同的輸出, 即不依賴外部環境,也不改變外部環境,這樣的函數就叫作純函數。純函數純的,是沒有反作用的。app
明白了純函數,那麼在寫reducer
的時候必定見過這麼一段代碼。異步
const state = reducer(initstate = {},action);
複製代碼
上面代碼,再結合純函數,就能夠說對於特定的action
和initstate
一定會獲得相同的state
,這裏正是體現了redux
的可預測性。
redux
提供了一系列規則來規定咱們來寫代碼。能夠大體分爲四個角色:
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更清晰一些。
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
也是有必定的套路的:好比拆分一個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進行一個糅合。
dispatch
的做用是派發一個action
去執行reducer
。我以爲dispatch
就是一個發佈者,和subscribe
一塊兒組合成訂閱發佈者模式。使dispatch
派發:
const action = {
type: "ADD",
value: "Hello Redux",
}
dispatch(action);
複製代碼
store
能夠說是redux
的核心了。開頭也提到store
是redux
狀態管理的惟一數據源,除此以外,store
仍是將dispatch
、reducer
等聯繫起來的命脈。
store
經過redux
提供的createStore
建立,它是一個對象,有以下屬性:
建立store:
const store = Redux.createStore(reducer,initialState,enhancer);
//1. reducer就是咱們書寫的reducer
//2. initialState是初始化狀態
//3. enhancer是中間件
複製代碼
在建立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
。
新的react-hooks中除了useReducer
,集成了redux
的功能,爲何還要深刻了解redux
呢?
隨着前端技術的迭代,技術的快速更新,咱們目前並無能力去預知或者去引領前端的發展,惟一能作的就是在時代中吸取知識並消化知識,雖然將來有可能redux
會被useReducer
所取代,可是思想是不變的,redux
這個小而美的庫設計是奇妙的,也許有哪一天在寫業務的時候遇到了某種類似的需求,咱們也能夠經過藉助於這個庫的思想去作一些事情。
要想了解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內部
getStore
方法的做用就是返回當前的store
。
let state = initialState;
const getState = () => {
return state;
}
複製代碼
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
是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
顧名思義就是替換reducer
的意思。再執行createState
方法的時候reducer
就做爲第一個參數傳進去,若是後面想要從新換一個reducer
,來代碼寫一下。
const replaceReducer = (reducer) => {
//傳入一個reduce做爲參數,把它賦予currentReducer就能夠了。
currentReducer = reducer;
//更該以後會派發一次dispatch,爲何會派發等下再說。
dispatch({type:"REPLACE"});
}
複製代碼
上面已經實現了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
後派發。
如今已經實現的差很少了,只要再加一些容錯就能夠了。
/** * * @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;
複製代碼
在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
函數。
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
}
複製代碼
這就是一箇中間件的邏輯了。
在寫邏輯的時候必然會用到異步數據的,咱們知道reducer
是純函數,不容許有反作用操做的,從上面到如今也能夠明白整個redux
都是函數式編程的思想,是不存在反作用的,那麼異步數據怎麼實現呢?必然是經過applyMiddleware
提供的中間件接口實現了。
異步中間件必需要求action
是一個函數,根據上面中間件的邏輯,咱們來寫一下。
const middleware = (store) => next => action => {
if(typeof action === 'function'){
action(store.dispatch,store.getState);
}
next(action);
}
複製代碼
判斷傳入的action
是不是一個函數,若是是函數使用加強dispatch
,若是不是函數使用普通的dispatch
。
到此爲止就是我能力範圍內所理解的Redux
。我我的認爲,要學習一個東西必定要看一下它的源碼,學習它的思想。技術更新迭代,思想是不變的,無非就是思想的轉變。若是有不對的地方,還望大佬們指點。