本文是一塊兒學習造輪子系列的第二篇,本篇咱們將從零開始寫一個小巧完整的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~)html
Redux是JavaScript狀態容器,提供可預測化的狀態管理。本文將會詳細介紹Redux五個核心方法
createStore,applyMiddleware,bindActionCreators,combineReducers,compose的實現原理,最後將本身封裝一個小巧完整的redux庫,隨後會介紹一下常常與Redux一塊兒結合使用的Redux經常使用中間件redux-logger,redux-thunk,redux-promise等中間件的實現原理。
前端
本文對於Redux是什麼及Redux幾個核心方法如何使用只會作簡單介紹,若是還沒用過Redux建議先學習基礎知識。
vue
推薦文章:
Redux 入門教程(一):基本用法
Redux 入門教程(二):中間件與異步操做
Redux 入門教程(三):React-Redux 的用法
react
本文全部代碼在github建有代碼倉庫,能夠點此查看本文代碼,也歡迎你們star~jquery
首先,咱們先來看一種使用Redux的基礎場景:webpack
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裏最核心的方法,下面咱們簡單實現一下這個方法。git
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對象,對象上有三個方法供外部使用 }
其實實現這個方法並不複雜github
通過以上三步,咱們便實現了一個簡單的createStore方法。web
咱們在開發稍微大一些的項目時reducer通常有多個,咱們會通常會創建一個reducers文件夾,裏面存儲項目中用到的全部reducer,而後使用一個combineReducers方法將全部reducer合併成一個傳給createStore方法。express
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; };
{userInfoReducer,bannerDataReducer}
,userInfoReducer裏state原本是這樣:{userId:1,name:"張三"}
,而bannerDataReducer裏的state原本是{pictureId:1,pictureUrl:"http://abc.com/1.jpg"}
{ userInfoReducer: { userId: 1, name: "張三" }, bannerDataReducer: { pictureId: 1, pictureUrl: "http://abc.com/1.jpg" } }
到此咱們實現了第二個方法combineReducers。
接下來介紹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,一個是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,咱們在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經常使用的三個中間件來實現一下
let logger = function({ dispatch, getState }) { return function(next) { return function(action) { console.log(getState()); next(action) console.log(getState()) } } }
這個咱們上面講applyMiddleware時已經講過了,再也不多說。
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中間件.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,因本文篇幅如今已經很長,因此這兩個輪子的實現將放到後續的一塊兒學習造輪子系列中,敬請關注~