Redux其实很简单(原理篇)
在上一篇文章中,我們通過(guò)一個(gè)示例頁(yè)面,了解到Redux的使用方法以及各個(gè)功能模塊的作用。如果還不清楚Redux如何使用,可以先看看Redux其實(shí)很簡(jiǎn)單(示例篇),然后再來(lái)看本文,理解起來(lái)會(huì)更加輕松。
那么在這一篇文章中,筆者將帶大家編寫(xiě)一個(gè)完整的Redux,深度剖析Redux的方方面面,讀完本篇文章后,大家對(duì)Redux會(huì)有一個(gè)深刻的認(rèn)識(shí)。
核心API
這套代碼是筆者閱讀完Redux源碼,理解其設(shè)計(jì)思路后,自行總結(jié)編寫(xiě)的一套代碼,API的設(shè)計(jì)遵循與原始一致的原則,省略掉了一些不必要的API。
createStore
這個(gè)方法是Redux核心中的核心,它將所有其他的功能連接在一起,暴露操作的API供開(kāi)發(fā)者調(diào)用。
在初始化時(shí),createStore會(huì)主動(dòng)觸發(fā)一次dispach,它的action.type是系統(tǒng)內(nèi)置的INIT,所以在reducer中不會(huì)匹配到任何開(kāi)發(fā)者自定義的action.type,它走的是switch中default的邏輯,目的是為了得到初始化的狀態(tài)。
當(dāng)然我們也可以手動(dòng)指定initialState,筆者在這里做了一層判斷,當(dāng)initialState沒(méi)有定義時(shí),我們才會(huì)dispatch,而在源碼中是都會(huì)執(zhí)行一次dispatch,筆者認(rèn)為沒(méi)有必要,這是一次多余的操作。因?yàn)檫@個(gè)時(shí)候,監(jiān)聽(tīng)流中沒(méi)有注冊(cè)函數(shù),走了一遍reducer中的default邏輯,得到新的state和initialState是一樣的。
第三個(gè)參數(shù)enhancer只有在使用中間件時(shí)才會(huì)用到,通常情況下我們搭配applyMiddleware來(lái)使用,它可以增強(qiáng)dispatch的功能,如常用的logger和thunk,都是增強(qiáng)了dispatch的功能。
同時(shí)createStore會(huì)返回一些操作API,包括:
- getState:獲取當(dāng)前的state值
- dispatch:觸發(fā)reducer并執(zhí)行l(wèi)isteners中的每一個(gè)方法
- subscribe:將方法注冊(cè)到listeners中,通過(guò)dispatch來(lái)觸發(fā)
applyMiddleware
這個(gè)方法通過(guò)中間件來(lái)增強(qiáng)dispatch的功能。
在寫(xiě)代碼前,我們先來(lái)了解一下函數(shù)的合成,這對(duì)后續(xù)理解applyMiddleware的原理大有裨益。
函數(shù)的合成
如果一個(gè)值要經(jīng)過(guò)多個(gè)函數(shù),才能變成另外一個(gè)值,就可以把所有中間步驟合并成一個(gè)函數(shù),這叫做函數(shù)的合成(compose)
舉個(gè)例子
function add (a) {return function (b) {return a + b} }// 得到合成后的方法 let add6 = compose(add(1), add(2), add(3))add6(10) // 16
下面我們通過(guò)一個(gè)非常巧妙的方法來(lái)寫(xiě)一個(gè)函數(shù)的合成(compose)。
export function compose (...funcs) {if (funcs.length === 0) {return arg => arg}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((a, b) => (...args) => a(b(...args))) }
上述代碼巧妙的地方在于:通過(guò)數(shù)組的reduce方法,將兩個(gè)方法合成一個(gè)方法,然后用這個(gè)合成的方法再去和下一個(gè)方法合成,直到結(jié)束,這樣我們就得到了一個(gè)所有方法的合成函數(shù)。
有了這個(gè)基礎(chǔ),applyMiddleware就會(huì)變得非常簡(jiǎn)單。
function middleware (store) {return function f1 (dispatch) {return function f2 (action) {// do somethingdispatch(action)// do something}} }
可以看出,chains是函數(shù)f1的數(shù)組,通過(guò)compose將所欲f1合并成一個(gè)函數(shù),暫且稱(chēng)之為F1,然后我們將原始dispatch傳入F1,經(jīng)過(guò)f2函數(shù)一層一層地改造后,我們得到了一個(gè)新的dispatch方法,這個(gè)過(guò)程和Koa的中間件模型(洋蔥模型)原理是一樣的。
為了方便大家理解,我們?cè)賮?lái)舉個(gè)例子,有以下兩個(gè)中間件
function middleware1 (store) {return function f1 (dispatch) {return function f2 (action) {console.log(1)dispatch(action)console.log(1)}} }function middleware2 (store) {return function f1 (dispatch) {return function f2 (action) {console.log(2)dispatch(action)console.log(2)}} }// applyMiddleware(middleware1, middleware2)
大家猜一猜以上的log輸出順序是怎樣的?
好了,答案揭曉:1, 2, (原始dispatch), 2, 1。
為什么會(huì)這樣呢?因?yàn)閙iddleware2接收的dispatch是最原始的,而middleware1接收的dispatch是經(jīng)過(guò)middleware1改造后的,我把它們寫(xiě)成如下的樣子,大家應(yīng)該就清楚了。
三個(gè)或三個(gè)以上的中間件,其原理也是如此。
至此,最復(fù)雜最難理解的中間件已經(jīng)講解完畢。
combineReducers
由于Redux是單一狀態(tài)流管理的模式,因此如果有多個(gè)reducer,我們需要合并一下,這塊的邏輯比較簡(jiǎn)單,直接上代碼。
combineReucers將單個(gè)reducer塞到一個(gè)對(duì)象中,每個(gè)reducer對(duì)應(yīng)一個(gè)唯一鍵值,單個(gè)reducer狀態(tài)改變時(shí),對(duì)應(yīng)鍵值的值也會(huì)改變,然后返回整個(gè)state。
bindActionCreators
這個(gè)方法就是將我們的action和dispatch連接起來(lái)。
function bindActionCreator (actionCreator, dispatch) {return function () {dispatch(actionCreator.apply(this, arguments))} }export default function bindActionCreators (actionCreators, dispatch) {if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, dispatch)}const boundActionCreators = {}Object.keys(actionCreators).forEach(key => {let actionCreator = actionCreators[key]if (typeof actionCreator === 'function') {boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)}})return boundActionCreators }
它返回一個(gè)方法集合,直接調(diào)用來(lái)觸發(fā)dispatch。
中間件
在自己動(dòng)手編寫(xiě)中間件時(shí),你一定會(huì)驚奇的發(fā)現(xiàn),原來(lái)這么好用的中間件代碼竟然只有寥寥數(shù)行,卻可以實(shí)現(xiàn)這么強(qiáng)大的功能。
logger
function getFormatTime () {const date = new Date()return date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + ' ' + date.getMilliseconds() }export default function logger ({ getState }) {return next => action => {/* eslint-disable no-console */console.group(`%caction %c${action.type} %c${getFormatTime()}`, 'color: gray; font-weight: lighter;', 'inherit', 'color: gray; font-weight: lighter;')// console.time('time')console.log(`%cprev state`, 'color: #9E9E9E; font-weight: bold;', getState())console.log(`%caction `, 'color: #03A9F4; font-weight: bold;', action)next(action)console.log(`%cnext state`, 'color: #4CAF50; font-weight: bold;', getState())// console.timeEnd('time')console.groupEnd()} }
thunk
export default function thunk ({ getState }) {return next => action => {if (typeof action === 'function') {action(next, getState)} else {next(action)}} }
這里要注意的一點(diǎn)是,中間件是有執(zhí)行順序的。像在這里,第一個(gè)參數(shù)是thunk,然后才是logger,因?yàn)榧偃鏻ogger在前,那么這個(gè)時(shí)候action可能是一個(gè)包含異步操作的方法,不能正常輸出action的信息。
心得體會(huì)
到了這里,關(guān)于Redux的方方面面都已經(jīng)講完了,希望大家看完能夠有所收獲。
但是筆者其實(shí)還有一個(gè)擔(dān)憂(yōu):每一次dispatch都會(huì)重新渲染整個(gè)視圖,雖然React是在虛擬DOM上進(jìn)行diff,然后定向渲染需要更新的真實(shí)DOM,但是我們知道,一般使用Redux的場(chǎng)景都是中大型應(yīng)用,管理龐大的狀態(tài)數(shù)據(jù),這個(gè)時(shí)候整個(gè)虛擬DOM進(jìn)行diff可能會(huì)產(chǎn)生比較明顯的性能損耗(diff過(guò)程實(shí)際上是對(duì)象和對(duì)象的各個(gè)字段挨個(gè)比較,如果數(shù)據(jù)達(dá)到一定量級(jí),雖然沒(méi)有操作真實(shí)DOM,也可能產(chǎn)生可觀的性能損耗,在小型應(yīng)用中,由于數(shù)據(jù)較少,因此diff的性能損耗可以忽略不計(jì))。
原文發(fā)布時(shí)間為:2018年06月28日
原文作者:連城
本文來(lái)源: 掘金?如需轉(zhuǎn)載請(qǐng)聯(lián)系原作者
總結(jié)
以上是生活随笔為你收集整理的Redux其实很简单(原理篇)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: linux下通过命令来下载视频
- 下一篇: Coding and Paper Let