本文是一起學習造輪子系列的第二篇,本篇我們將從零開始寫一個小巧完整的Redux,本系列文章將會選取一些前端比較經典的輪子進行源碼分析,並且從零開始逐步實現,本系列將會學習Promises/A+,Redux,react-redux,vue,dom-diff,webpack,babel,kao,express,async/await,jquery,Lodash,requirejs,lib-flexible等前端經典輪子的實現方式,每一章源碼都托管在github上,歡迎關注~
相關系列文章:
一起學習造輪子(一):從零開始寫一個符合Promises/A+規范的promise
一起學習造輪子(二):從零開始寫一個Redux
一起學習造輪子(三):從零開始寫一個React-Redux
本系列github倉庫:
一起學習造輪子系列github(歡迎star~)
前言
Redux是JavaScript狀態容器,提供可預測化的狀態管理。本文將會詳細介紹Redux五個核心方法
createStore,applyMiddleware,bindActionCreators,combineReducers,compose的實現原理,最后將自己封裝一個小巧完整的redux庫,隨后會介紹一下經常與Redux一起結合使用的Redux常用中間件redux-logger,redux-thunk,redux-promise等中間件的實現原理。
本文對於Redux是什么及Redux幾個核心方法如何使用只會做簡單介紹,如果還沒用過Redux建議先學習基礎知識。
推薦文章:
Redux 入門教程(一):基本用法
Redux 入門教程(二):中間件與異步操作
Redux 入門教程(三):React-Redux 的用法
本文所有代碼在github建有代碼倉庫,可以點此查看本文代碼,也歡迎大家star~
開始
createStore
首先,我們先來看一種使用Redux的基礎場景:
function reducer(state, action) {}
const store = createStore(reducer) //用reducer生成了store
store.subscribe(() => renderApp(store.getState())) //注冊state變化的回調
renderApp(store.getState()) //初始化頁面
store.dispatch(xxxaction) //發出action
上面代碼是一個用到Redux的基礎場景,首先定義了一個reducer,然后用這個reducer生成了store,在store上注冊當state發生變化后要執行的回調函數,然后使用初始state先渲染一下頁面,當頁面有操作時,store.dispatch發出一個action,action和舊的state經過reducer計算生成新的state,此時state變化,觸發回調函數使用新的state重新渲染頁面,這個簡單的場景囊括了整個redux工作流,
如圖所示:
這個場景主要用到Redux里面的createStore方法,這是Redux里最核心的方法,下面我們簡單實現一下這個方法。
function createStore(reducer) {
let state = null //用來存儲全局狀態
let listeners = [] //用來存儲狀態發生變化的回調函數數組
const subscribe = (listener) => { //用來注冊回調函數
listeners.push(listener)
}
const getState = () => state //用來獲取最新的全局狀態
const dispatch = (action) => { //用來接收一個action,並利用reducer,根據舊的state和action計算出最新的state,然后遍歷回調函數數組,執行回調.
state = reducer(state, action) //生成新state
listeners.forEach((listener) => listener()) //執行回調
}
dispatch({}) //初始化全局狀態
return { getState, dispatch, subscribe } //返回store對象,對象上有三個方法供外部使用
}
其實實現這個方法並不復雜
- 首先,定義2個變量,一個是state,一個是listeners,state用來存放全局狀態,listeners用來存儲狀態發生變化的回調函數數組。
- 然后定義三個方法subscribe,getState,dispatch。subscribe用於注冊回調函數,getState用來獲取最新的state狀態,dispatch用來接收一個action,並利用reducer,根據舊的state和action計算出最新的state,然后遍歷回調函數數組,執行回調。
- 當調用createStore時,會先執行dispatch({})利用reducer生成一個初始state,然后返回一個store對象,對象上掛載着getState, dispatch, subscribe這三個方法供外部調用
經過以上三步,我們便實現了一個簡單的createStore方法。
combineReducers
我們在開發稍微大一些的項目時reducer一般有多個,我們會一般會建立一個reducers文件夾,里面存儲項目中用到的所有reducer,然后使用一個combineReducers方法將所有reducer合並成一個傳給createStore方法。
import userInfoReducer from './userinfo.js'
import bannerDataReducer from './banner.js'
import recordReducer from './record.js'
import clientInfoReducer from './clicentInfo.js'
const rootReducer = combineReducers({
userInfoReducer,
bannerDataReducer,
recordReducer,
clientInfoReducer
})
const store = createStore(rootReducer)
接下來,我們就一起來實現combineReducers這個方法:
const combineReducers = reducers => (state = {}, action) => {
let currentState = {};
for (let key in reducers) {
currentState[key] = reducers[key](state[key], action);
}
return currentState;
};
- 首先combineReducers這個函數接收一個reducer集合,返回一個合並后的reducer函數,所以返回的函數傳參仍然和平常的reducer一樣,接收state和action,返回新的state。
- 然后聲明一個currentState對象,用來存儲全局狀態,接着遍歷reducers數組,使用reducer函數生成對應的state對象掛載到currentState上。
比如說reducers里傳入了2個reducer{userInfoReducer,bannerDataReducer}
,userInfoReducer里state本來是這樣:{userId:1,name:"張三"}
,而bannerDataReducer里的state本來是{pictureId:1,pictureUrl:"http://abc.com/1.jpg"}
合並以后的currentState變為
{
userInfoReducer: {
userId: 1,
name: "張三"
},
bannerDataReducer: {
pictureId: 1,
pictureUrl: "http://abc.com/1.jpg"
}
}
到此我們實現了第二個方法combineReducers。
bindActionCreators
接下來介紹bindActionCreators這個方法,這是redux提供的一個輔助方法,能夠讓我們以方法的形式來調用action。同時,自動dispatch對應的action。它接收2個參數,第一個參數是接收一個action creator,第二個參數接收一個 dispatch 函數,由 Store 實例提供。
比如說我們有一個TodoActionCreators
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
};
}
我們之前需要這樣使用:
import * as TodoActionCreators from './TodoActionCreators';
let addReadAction = TodoActionCreators.addTodo('看書');
dispatch(addReadAction);
let addEatAction = TodoActionCreators.addTodo('吃飯');
dispatch(addEatAction);
let removeEatAction = TodoActionCreators.removeTodo('看書');
dispatch(removeEatAction);
現在只需要這樣:
import * as TodoActionCreators from './TodoActionCreators';
let TodoAction = bindActionCreators(TodoActionCreators, dispatch);
TodoAction.addTodo('看書')
TodoAction.addTodo('吃飯')
TodoAction.removeTodo('看書')
好了,說完了如何使用,我們來實現一下這個方法
function bindActionCreator(actions, dispatch) {
let newActions = {};
for (let key in actions) {
newActions[key] = () => dispatch(actions[key].apply(null, arguments));
}
return newActions;
}
方法實現也不難,就是遍歷ActionCreators里面的所有action,每個都使用一個函數進行包裹dispatch行為並將這些函數掛載到一個對象上對外暴露,當我們在外部的調用這個函數的時候,就會自動的dispatch對應的action,這個方法的實現其實也是利用了閉包的特性。
這個方法在使用react-redux里面經常見到,等講react-redux實現原理時會再說一下。
compose
最后,還剩兩個方法,一個是compose,一個是applyMiddleware,這兩個都是使用redux中間件時要用到的方法,先來說說compose這個方法,這是一個redux里的輔助方法,其作用是把一系列的函數,組裝生成一個新的函數,並且從后到前依次執行,后面函數的執行結果作為前一個函數執行的參數。
比如說我們有這樣幾個函數:
function add1(str) {
return str + 1
}
function add2(str) {
return str + 2
}
function add3(str) {
return str + 3
}
我們想依次執行函數,並把執行結果傳到下一層就要像下面一樣一層套一層的去寫:
let newstr = add3(add2(add1("abc"))) //"abc123"
這只是3個,如果數量多了或者數量不固定處理起來就很麻煩,但是我們用compose寫起來就很優雅:
let newaddfun = compose(add3, add2, add1);
let newstr = newaddfun("abc") //"abc123"
那compose內部是如何實現的呢?
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
其實核心代碼就一句,這句代碼使用了reduce方法巧妙地將一系列函數轉為了add3(add2(add1(...args)))
這種形式,我們使用上面的例子一步一步地拆分看一下,當調用compose(add3, add2, add1)
,funcs是add3, add2, add1,第一次進入時a是add3,b是add2,展開就是這樣子:(add3, add2)=>(...args)=>add3(add2(...args))
,傳入了add3, add2,返回一個這樣的函數(...args)=>add3(add2(...args))
,然后reduce繼續進行,第二次進入時a是上一步返回的函數(...args)=>add3(add2(...args))
,b是add1,於是執行到a(b(...args)))
時,b(...args)
作為a函數的參數傳入,變成了這種形式:(...args)=>add3(add2(add1(...args)))
,是不是很巧妙。
applyMiddleware
最后我們來看最后一個方法applyMiddleware,我們在redux項目中,使用中間件時一般這樣寫:
import thunk from 'redux-thunk'
import logger from 'redux-logger'
const middleware = [thunk, logger]
const store = createStore(rootReducer, applyMiddleware(...middleware))
上面我們用到了thunk和logger這兩個中間件,在createStore創建倉庫時傳入一個新的參數applyMiddleware(...middleware),在此告訴redux我們要使用的中間件,所以我們要先改造一下createStore方法,讓其支持中間件參數的傳入。
function createStore(reducer, enhancer) {
//如果傳入了中間件函數,使用中間件增強createStore方法
if (typeof enhancer === 'function') {
return enhancer(createStore)(reducer)
}
let state = null
const listeners = []
const subscribe = (listener) => {
listeners.push(listener)
}
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({})
return { getState, dispatch, subscribe }
}
然后接下來以redux-logger中間件為例來分析一下redux中間件的實現方式。
首先我們可以先思考一下,如果我們不用logger中間件,想實現logger的功能該怎樣做呢?
let store = createStore(reducer);
let dispatch = store.dispatch;
store.dispatch = function (action) {
console.log(store.getState());
dispatch(action);
console.log(store.getState())
};
我們可以在原始dispatch方法外面包裝一層函數,讓發起真正的dispatch之前和之后都打印一下日志,調用時調用包裝后的這個dispatch函數,其實redux中間件原理的思路就是這樣的:將store的dispatch進行替換,換成一個功能增強了但是仍然具有dispach功能的新函數。
那applyMiddleware方法里是如何改造dispatch來增強功能的呢?首先我們來看個簡單版本,假如我們只有一個中間件,如何實現applyMiddleware方法呢?
function applyMiddleware(middleware) {
return function a1(createStore) {
return function a2(reducer) {
//取出原始dispatch方法
const store = createStore(reducer)
let dispatch = store.dispatch
//包裝dispatch
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
let mid = middleware(middlewareAPI)
dispatch = mid(store.dispatch)
//使用包裝后的dispatch覆蓋store.dispatch返回新的store對象
return {
...store,
dispatch
}
}
}
}
//中間件
let logger = function({ dispatch, getState }) {
return function l1(next) {
return function l2(action) {
console.log(getState());
next(action)
console.log(getState())
}
}
}
//reducer函數
function reducer(state, action) {
if (!state) state = {
count: 0
}
console.log(action)
switch (action.type) {
case 'add':
let obj = {...state,
count: ++state.count
}
return obj;
case 'sub':
return {...state,
count: --state.count
}
default:
return state
}
}
const store = createStore(reducer, applyMiddleware(logger))
-
首先我們定義了的applyMiddleware方法,它接收一個中間件作為參數。然后定義了一個logger中間件函數,它接收dispatch和getState方法以供內部使用。這兩個函數Redux源碼里都是使用高階函數實現的,在這里與源碼保持一致也使用高階函數實現,但是為了方便理解,使用具名的function函數代替匿名箭頭函數可以看得更清晰。
-
當我們執行
const store = createStore(reducer,applyMiddleware(logger))
時,首先applyMiddleware(logger)
執行,將logger存在閉包里,然后返回了一個接收createStore方法的函數a1,將a1這個函數作為第二個參數傳入createStore方法,因為傳入了第二個參數,所以createstore里面其實會執行這一段代碼:
if (typeof enhancer === 'function') {
return enhancer(createStore)(reducer)
}
當執行return enhancer(createStore)(reducer)
,其實執行的是a1(createStore)(reducer)
,當執行a1(createStore)
時返回a2,最后return的是a2(reducer)
的執行結果。
-
然后,我們看看a2內部都做了些什么,我給這個函數定義了三個階段,首先為取出原始dispatch階段,這一階段執行
createStore(reducer)
方法,並拿出原始的dispatch方法。 -
接着,我們到了第二個階段包裝原始dispatch,首先我們定義了middlewareAPI用來給中間件函數使用,這里的getState直接使用了store.getState,而dispatch使用函數包了一層,
(action)=>dispatch(action)
,為什么呢,因為我們最終要給中間件使用的dispatch方法,一定是經過各種中間件包裝后的dispatch方法,而不是原方法,所以我們這里將dispatch方法設置為一個變量。然后將middlewareAPI傳入middleware執行,返回一個函數mid(也就是logger里面的l1),這個函數接收一個next方法作為參數,然后當我們執行dispatch = mid(store.dispatch)
時,將store.dispatch作為next方法傳入,並把返回的函數l2作為新的dispatch,我們可以看到新的dispatch方法其實里面做了和我們上面自己直接改造store.dispatch做了同樣的事情:
function l2(action) {
console.log(getState());
next(action)
console.log(getState())
}
都是接收一個action,先打印日志,然后執行原始的dispatch方法去發一個action,然后再打印日志。
-
最后到了第三個階段:使用包裝后的dispatch覆蓋store.dispatch方法后返回新的store對象。
-
到此,當我們在外面執行store.dispatch({type:add})時,實際上執行的是包裝后的dispatch方法,所以logger中間件就生效了,如圖所示真正發起dispatch的前后都打印出了最新狀態:
現在我們在上一版applyMiddleware的基礎上再改造,使其支持多個中間件:
import compose from './compose';
function applyMiddleware(...middlewares) {
return function a1(createStore) {
return function a2(reducer) {
const store = createStore(reducer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
}
let loggerone = function({ dispatch, getState }) {
return function loggerOneOut(next) {
return function loggerOneIn(action) {
console.log("loggerone:", getState());
next(action)
console.log("loggerone:", getState())
}
}
}
let loggertwo = function({ dispatch, getState }) {
return function loggerTwoOut(next) {
return function loggerTwoIn(action) {
console.log("loggertwo:", getState());
next(action)
console.log("loggertwo:", getState())
}
}
}
const store = createStore(reducer, applyMiddleware([loggertwo, loggerone]))
-
首先當調用applyMiddleware方法時,由傳入一個中間件變為傳入一個中間件數組。
-
然后我們在applyMiddleware方法中維護一個chain數組,這個數組用於存儲中間件鏈。
-
當執行到
chain = middlewares.map(middleware => middleware(middlewareAPI))
時,chain里面存放的是[loggerTwoOut,loggerOneOut]
。 -
然后下一步我們改造dispatch時用到了我們之前講過的compose方法,
dispatch=compose(...chain)(store.dispatch)
其實相當於是執行了dispatch =loggerTwoOut(loggerOneOut(store.dispatch))
,然后這一句loggerTwoOut(loggerOneOut(store.dispatch))
再次拆開看一下是如何執行的,當執行loggerOneOut(store.dispatch)
,返回loggerOneIn函數,並將store.dispatch方法作為loggerOneIn里面的next方法。現在函數變成了這樣:loggerTwoOut(loggerOneIn)
,當執行這一句時,返回loggerTwoIn函數,並將loggerOneIn作為loggerTwoIn方法里的next方法。最后給dispatch賦值:dispatch =loggerTwoIn
。 -
在外部我們調用
store.dispatch({type:add})
時,實際執行的是loggerTwoIn({type:add})
,所以會先執行console.log("loggertwo:", getState())
,然后執行next(action)
時執行的其實是loggerOneIn(action)
,進入到loggerOneIn內部,所以會執行console.log("loggerone:",getState())
;然后執行next(action)
,這里的其實執行的是原始的store.dispatch方法,所以會真正的把action提交,提交完后繼續執行,執行console.log("loggerone:",getState())
,然后loggerOneIn執行完畢,執行權交還到上一層loggerTwoIn,loggerTwoIn繼續執行,執行console.log("loggertwo:", getState())
,結束。
畫一張圖形象的表示下執行流程:
到此,applymiddleware方法就講完了,我們來看下redux官方源碼的實現:
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
我們實現的applyMiddleware方法對比官方除了沒有對前后端同構時預取數據preloadedState做支持外,其余功能都完整實現了。
到此我們把redux里所有方法都實現了一遍,當然我們實現的只是每個方法最核心最常用的部分,並沒有將redux源碼逐字逐句去翻譯。因為個人認為對於源碼的學習應該抓住主線,學習源碼中的核心代碼及閃光點,如果對redux其他功能感興趣的,可以自行看官方源碼學習。
常用中間件 redux-logger,redux-thunk,redux-promise
接下來,我們將redux常用的三個中間件來實現一下
redux-logger
let logger = function({ dispatch, getState }) {
return function(next) {
return function(action) {
console.log(getState());
next(action)
console.log(getState())
}
}
}
這個我們上面講applyMiddleware時已經講過了,不再多說。
redux-thunk
redux-thunk在我們平常使用時主要用來處理異步提交action情況,引入了redux-thunk后我們可以異步提交action
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
store.dispatch(fetchPosts('reactjs'))
我們可以看到fetchPosts('reactjs')返回的是一個函數,而redux里的dispatch方法不能接受一個函數,Redux官方源碼中明確說了,action必須是一個純粹的對象,處理異步action時需要使用中間件,
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
......
}
那redux-thunk到底做了什么使dispatch可以傳入函數呢?
let thunk = function({ getState, dispatch }) {
return function(next) {
return function(action) {
if (typeof action == 'function') {
action(dispatch, getState);
} else {
next(action);
}
}
}
}
thunk中間件在內部進行判斷,如果傳入了一個函數,就去執行它,不是函數就不管交給下一個中間件,以上面的fetchPosts為例,當執行store.dispatch(fetchPosts('reactjs'))
時,給dispatch傳入了一個函數:
postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
thunk中間件發現是個函數,於是執行它,先發出一個Action(requestPosts(postTitle)),然后進行異步操作。拿到結果后,先將結果轉成 JSON 格式,然后再發出一個Action(receivePosts(postTitle,json))。這兩個Action都是普通對象,所以當dispatch時會走else {next(action);}這個分支,繼續執行.這樣就解決了dispatch不能接受函數的問題。
redux-promise
最后講一個redux-promise中間件.dispatch目前可以支持傳入函數了,利用redux-promise我們再讓它支持傳入promise對象,平時我們在用這個中間件時,一般有兩種用法:
寫法一,返回值是一個 Promise 對象。
const fetchPosts =
(dispatch, postTitle) => new Promise(function(resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
寫法二,Action 對象的payload屬性是一個Promise對象。這需要從redux里引入createAction方法,並且寫法也要變成下面這樣。
import { createAction } from 'redux-actions';
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 發出同步 Action
dispatch(requestPosts(selectedPost));
// 發出異步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
}
讓我們來實現一下redux-promise中間件:
let promise = function({ getState, dispatch }) {
return function(next) {
return function(action) {
if (action.then) {
action.then(dispatch);
} else if (action.payload && action.payload.then) {
action.payload.then(payload => dispatch({...action, payload }), payload => dispatch({...action, payload }));
} else {
next(action);
}
}
}
}
我們實現redux-thunk時是判斷如果傳入function就執行這個function,否則next(action)繼續執行;redux-promise同理,當action或action的payload上面有then方法時,我們認為它是promise對象,就讓dispatch到promise的then里面再執行,直到dispatch提交的action沒有then方法,我們認為它不是promise了,可以執行next(action)交給下一個中間件執行了。
最后
本篇介紹了Redux五個方法createStore,applyMiddleware,bindActionCreators,combineReducers,compose的實現原理,並自己封裝了一個小巧完整的Redux庫,同時簡單介紹了Redux里常用的3個中間件redux-logger,redux-thunk,redux-promise的實現原理,本文所有代碼在github建有代碼倉庫,可以點擊查看本文源碼。
與Redux相關的比較經典的輪子還有React-Redux和redux-saga,因本文篇幅現在已經很長,所以這兩個輪子的實現將放到后續的一起學習造輪子系列中,敬請關注~