理解Redux的實現原理

爲何要用Redux

React只是一個視圖層的框架,負責把數據映射成DOM元素。但應用程序每每涉及到大量的數據交互和網絡請求,修改數據的頻率會很高,因此須要一種規範來約束對數據的更新,使得任何修改均可以被追蹤,這樣纔不懼應用程序的複雜性,實現良好的調試性能和可擴展能力。Redux是一個數據流和數據容器管理工具。這篇文章中有一組生動的對比圖看有無Redux時數據的處理方式。javascript

經過TodoList的例子推導Redux

下面咱們看一個簡單的 todo-list 的例子。 html

//TodoApp.js
const [todos, setTodos] = useState([])
const addTodo = () => {}
const removeTodo = () => {}
const toggleTodo = () => {}

<AddTodo addTodo={addTodo} />
<TodoList removeTodo={removeTodo} toggleTodo={toggleTodo} todos={todos} /> 複製代碼
//TodoList.js
{
    todos.map(todo => (
        <TodoItem removeTodo={removeTodo} toggleTodo={toggleTodo} /> )) } 複製代碼

經過上圖咱們知道,addTodo,removeTodo,toggleTodo這些功能函數都是經過setTodos對todos這個數據進行操做,對數據todos來講這些操做是不透明的。爲了更透明的對todos進行操做,咱們能夠用這樣的一種結構來描述每次對todos的操做

{
    type: 'add',
    payload: todo
}
複製代碼

咱們稱它爲Action,每次對todos進行操做時都發出這樣一個Action,就能夠很清楚的看到在對todos進行了什麼操做,此次操做攜帶的數據是什麼。直接把這樣一個Action對象丟給todos,todos是不知道該怎麼辦的,因此todos須要一個管家(dispatch)幫它處理而後把處理結果告訴它。下面讓咱們用代碼來實現一下dispatch。java

const dispatch = (action) => {
    const { type, payload } = action
    swicth(type) {
        case 'set':
            //set的邏輯
            break;
        case 'add':
            //add的邏輯
            break;
        case 'remove':
            //remove的邏輯
            break;
        case 'toggle':
            //toggle的邏輯
            break;
    }   
}
複製代碼

有了dispatch這個管家,如今處理addTodo的業務邏輯就很簡單了,只須要segmentfault

dispatch({
    type: 'add',
    payload: todo
})
複製代碼

由於每一次操做都是一個Action,而每個Action都只有兩個參數(type, payload),當操做頻繁時每次都寫上面的代碼會很麻煩,因此咱們考慮構建一個創造Action的函數actionCreator,這樣咱們就不用每次都手動生成Action了。由於有不少個Action,對應就會有不少個actionCreator,因此咱們考慮把全部的actionCreator放在一個單獨的文件actionCreators.js裏bash

//actionCreators.js
export const add = payload => ({
    type: 'add',
    payload
})

export const remove = payload => ({
    type: 'remove',
    payload
})

export const toggle = payload => ({
    type: 'toggle',
    payload
})
複製代碼

而後把actionCreators.js引入到TodoList.js中,如今咱們處理addTodo就只須要網絡

//TodoList.js
import * as actionCreators from './actionCreators';
dispatch(actionCreators.add(payload))
複製代碼

仔細看看每次操做都須要dispatch來派發Action,咱們能夠考慮再封裝一次,把dispatch也隱藏起來。框架

const addTodo = payload => dispatch(actionCreators.add(payload))
複製代碼

這樣咱們每次處理addTodo就只須要調用addTodo函數便可。這樣的封裝操做有不少個,咱們能夠批量實現一下,咱們但願獲得下面這樣的結果異步

{
    addTodo: payload => dispatch(actionCreators.add(payload)),
    removeTodo: payload => dispatch(actionCreators.remove(payload)),
    toggleTodo: payload => dispatch(actionCreators.toggle(payload))
}
複製代碼

因而咱們編寫一個bindActionCreators函數來批量封裝獲得咱們想要的結果函數

function bindActionCreators(actionCreators, dispatch) {
  const ret = {}

  for(let key in actionCreators) {
    ret[key] = function(...args) {
      const actionCreator = actionCreators[key]
      const action = actionCreator(...args)
      dispatch(action)
    }
  }

  return ret
}
複製代碼

如今咱們能夠這樣實現一個addTodo的操做工具

const {
    add: addTodo, 
    remove: removeTodo,
    toggle: toggleTodo
} = bindActionCreators(actionCreators, dispatch)

addTodo(payload)
複製代碼

由於TodoList的邏輯很簡單,因此咱們這樣改造完沒有看到很明顯的優點,因此讓咱們改造一下項目,讓項目變得稍微複雜一點,咱們新添加一個incrementCount變量,每次新添加一個todo,incrementCount就會加一。

const [todos, setTodos] = useState([])
const [incrementCount, setIncrementCount] = useState(0)

const dispatch = (action) => {
    const { type, payload } = action
    swicth(type) {
        case 'set':
            //set的邏輯
            setIncrementCount(c => c + 1)
            break;
        case 'add':
            //add的邏輯
            setIncrementCount(c => c + 1)
            break;
        case 'remove':
            //remove的邏輯
            break;
        case 'toggle':
            //toggle的邏輯
            break;
    }   
}
複製代碼

如今咱們看代碼能夠發現多個Action有一樣的邏輯,須要重複編碼實現,這是由於咱們是從Action的維度來執行的數據更新邏輯,可是這些Action操做都是爲了更新數據,爲了更加清晰,咱們能夠考慮從數據的維度來整理數據的更新邏輯,咱們但願有這樣的一個reducer,它接收state和action而後返回更新後的state數據,每個數據有本身單獨的reducer,而後返回合併後的多個reducer,如今讓咱們用代碼來實現一下。

//reducers.js
const reducers = {
    todos(state, action) {
    const { type, payload } = action
    switch(type) {
        case 'set':
            return //set的邏輯
        case 'add':
            return //add的邏輯
        case 'remove':
            return //remove的邏輯
        case 'toggle':
            return //stoggle的邏輯
        }
        return state      
    },
  incrementCount(state, action) {
    const { type } = action
    switch(type) {
      case 'set':
        return state + 1
      case 'add':
        return state + 1
    }
    return state
  }
}

function combineReducers(reducers) {
  return function reducer(state, action) {
    const changed = {}

    for(let key in reducers) {
      changed[key] = reducers[key](state[key], action)
    }

    return {
      ...state,
      ...changed
    }
  }
}

export default combineReducers(reducers)
複製代碼

如今在TodoList.js中引入reducers.js,而後改寫dispatch函數

const dispatch = (action) => {
    const state = {
        todo,
        incrementCount
    }

    const setters = {
        todos: setTodos,
        incrementCount: setIncrementCount
    }
    
    const newState = reducer(state, action)
    
    for(let key in newState) {
        setters[key](newState[key])
    }
}
複製代碼

reducer的意義在於可以從數據字段的維度來處理action。

上面說了這麼多咱們都是在處理同步的Action,如今讓咱們思考一下如何處理異步的Action。最直接的想法就是咱們先處理異步的邏輯,異步結束後再派發一次Action,下面讓咱們用代碼來實現一下

//異步的Action
export const add = text => (dispatch, state) => {
    setTimeout(() => {
      const { todos } = state
      
      if(!todos.find(todo => todo.text === text)) {
        dispatch({
            type: 'add',
            payload: {
                id: Date.now(),
                text,
                complete: false
            }
        })
      }
    }, 3000)
}
複製代碼

如今咱們的dispatch只能處理對象,不能處理異步Action的函數,因此讓咱們改寫一下dispatch讓它能夠支持對函數的處理

const dispatch = (action) => {
    const state = {
        todo,
        incrementCount
    }

    const setters = {
        todos: setTodos,
        incrementCount: setIncrementCount
    }
    
    if('function' === typeof action) {
        action(dispatch, state)
        return
    }
    
    const newState = reducer(state, action)
    
    for(let key in newState) {
        setters[key](newState[key])
    }
}
複製代碼

這樣咱們就實現了一個異步的Action,咱們但願增長todo時先判斷原有的todo列表中是否包含新添加todo的內容,若是是再也不添加,若是不是再添加。這時咱們進行調試若是在3s以前刪掉了重複的Action,咱們會發現3s後這個重複的Action仍是被添加到了todo的列表中。這是由於add函數拿到的數據是3s前的數據,爲了不這種狀況的出現,咱們會考慮用函數動態的獲取state裏面的數據,例如

addTodo(dispatch, () => state)
複製代碼

可是state這個對象老是在異步action發起以前臨時構成的,若是在3s內作了一些操做,那麼數據其實已經發生改變,異步Action內獲取到的仍是舊的數據。在每次渲染週期state都會改變,因此咱們能夠在組件以外建立一個store來存儲全部的state

let store = {
    todo: [],
    incrementCount: 0
}
//TodoList組件內同步數據
useEffect(() => {
    Object.assign(store, {
        todos,
        incrementCount
    })
}, [todos, incrementCount])
複製代碼

如今讓咱們改寫dispatch

const dispatch = (action) => {
    const setters = {
        todos: setTodos,
        incrementCount: setIncrementCount
    }
    
    if('function' === typeof action) {
        action(dispatch, () => store)
        return
    }
    
    const newState = reducer(store, action)
    
    for(let key in newState) {
        setters[key](newState[key])
    }
}
複製代碼

改寫異步Action

//異步的Action
export const add = text => (dispatch, getState) => {
    setTimeout(() => {
      const { todos } = getState()
      
      if(!todos.find(todo => todo.text === text)) {
        dispatch({
            type: 'add',
            payload: {
                id: Date.now(),
                text,
                complete: false
            }
        })
      }
    }, 3000)
}
複製代碼

總結

咱們能夠用actionCreators來生成一次操做的Action,用dispatch來派發這個Action,用reducer來更新數據,用bindActionCreators封裝多個Action的派發操做,用combineReducers將多個reducer合併成一個。

實際上Redux也只有最基本的功能,它自己不具有對異步Action的處理,可是在Reudx的整個流程中,在Action被dispatch派發到達reducer以前能夠通過多箇中間件的處理,這些中間件能夠加強dispatch的功能,好比Redux-thunk中間件就可讓dispatch具有處理異步Action的能力。若是想要對 Redux Store 進行更深層次的加強定製,就須要使用 Store Enhancer,利用 Store Enhancer 能夠加強 Redux Store 的 各個 方面。

Action -> dispatch -> 各類中間件 -> reducer -> store
複製代碼

後記

我寫文章比較少,因此邏輯可能不是很清晰,若是有問題歡迎你們在評論區中提出,咱們一塊兒學習討論。本文是學習React勁爆新特性Hooks 重構去哪兒網火車票PWA這門課後,將老師講的內容加上一點點本身的理解寫成的。順便安利一下這門課,老師講的超級棒!!!再推薦一本書,程墨的《深刻淺出React和Redux》,裏面對Redux的原理也講解的十分清晰。

相關文章
相關標籤/搜索