首先咱們要弄清楚 reduxjs
的思想、做用是什麼,這樣咱們才能開始下一步的構思。在我看來 reduxjs
核心就是一種單一數據源的概念,數據存儲在一個函數的 state
變量中,只能經過固定的方法去修改和獲取 dispatch()、getState()
。react
在 SPA 應用中,reduxjs
被普遍使用。對數據進行統一管理、實現數據共享,一般組件和組件之間、頁面和頁面之間能夠數據共享。在 react
開發中,我常常將共用的數據和異步請求數據存放在 state
中。經過 props
的形式存在,只要在一個組件中對數據源進行了修改,其餘共享的組件都會及時獲得更新和渲染UI界面。redux
如今咱們知道了關於 redux
的關鍵思想和用途,接下來咱們一步一步實現它。我會按照下面這個列表的順序給你們詳細說明:數組
function createStore(reducer, initState) {
// 聲明一個初始化用的 action
const INIT_ACTION = undefined;
// 綁定監聽事件的集合
const listeners = [];
// 這就是咱們一直說的那個【數據源】
// 參數 initState 能夠有,也能夠沒有。通常狀況下不須要傳遞
let state = initState ? initState : {};
function dispatch(action) {
// action 必須是一個純對象,不能是其餘的類型
if (Object.prototype.toString.call(action) === '[object Object]') {
throw new Error('Actions must be plain objects');
}
// 注意:這裏是最終仍是經過調用 reducer 方法
state = reducer(state, action);
// 遍歷 listeners
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
}
// 獲取 state 數據
function getState() {
return state;
}
// 綁定監聽事件
function subscription(listener) {
listeners.push(listener);
// 取消監聽,將事件從 listeners 中移除
return function() {
const idx = listeners.indexOf(listener);
if (idx >= 0) {
listeners.splice(idx, 1);
}
}
}
// 這是啥意思了,其實這是在調用 createStore() 時,就初始化了一個 state
dispatch(INIT_ACTION);
// 經過對象,將這些內部函數傳遞到外部。不要懷疑,這就是一個典型的閉包
return {
dispatch,
getState,
subscription,
};
}
複製代碼
從 createStore
方法中咱們能夠看出來,其實他就是 js模塊
。利用了局部變量和閉包的特性,將 state
隱藏起來,只能經過閉包的形式進行訪問和修改。閉包
首先 reduce 它是一個函數,咱們能夠本身定義。咱們能夠把咱們的項目想像成以下的一個場景,修改用戶的信息:app
function userName(state = {}, action = {}) {
switch (action.type) {
case 'name':
return { ...state, name: action.data };
case 'age':
return { ...state, age: action.data };
case 'sex':
return { ...state, sex: action.data };
// 必須設置 default,直接返回 state
default:
return state;
}
}
複製代碼
若是咱們的項目中只須要這一種交互場景,那麼定義 userName() 就夠了。這個時候 咱們把 userName 傳遞給 createStore異步
const { getState } = createStore(userName);
// 返回的是一個 {}
console.log(getState());
複製代碼
上面的代碼在執行 createStore(userName)
時,內部執行一次 dispatch(INIT_ACTION)
,從而在 dispatch 方法內部調用了 userName({}, undefined)
。因此打印的結果是一個空對象。函數
若是交互場景比較多的時候呢,一個 reducer
確定不夠用啊,那麼這個時候咱們可能會定義多個相似 userName
這個的 reducer
函數,因此咱們還須要定義一個工具函數 combineReducers
,將多個 reducer
函數組合成一個 reducer
函數。工具
function combineReducers(reducers) {
const keys = Object.keys(reducers);
const finallyKeys = [];
for (let i = 0; i < keys.length; i++) {
if (typeof reducers[keys[i]] !== 'function') throw Error('reducer must be a function');
finallyKeys.push(keys[i]);
}
// 看,最後返回的仍是一個 function
return function(state = {}, action) {
let hasChange = false;
const newState = {};
// 遍歷全部的 reducer 函數
finallyKeys.forEach(key => {
// 獲取這個 reducer 函數對應的 state。注意它多是一個 undefined
// 沒錯,在 createStore() 中執行 dispatch(INIT_ACTION),這個時候 prevState_key 可能就是一個 unudefined
const prevState_key = state[key];
const reducer = reducers[key];
// 調用該 reducer,返回一個新的 state
const nextState_key = reducer(prevState_key, action);
// 注意這裏,若是 reducer 函數返回的是一個 undefined。那麼這裏就會報錯了
// 因此咱們在定義 reducer 函數時,應該有一個限制:若是沒有匹配到 action 的 type 。應該默認返回 previous state。
if (typeOf nextState_key === 'undefined') {
throw Error('to ignore an action, you must explicitly return the previous state');
}
// 當 reducer 執行完成時,會在 newState 上添加一個新屬性,屬性值就是 nextState_key
// 其實,從這個地方咱們就應該能夠猜想到,最終獲得的 state【數據源】,它的結果應該和咱們傳入的 reducers 結構是同樣的
newState[key] = nextState_key;
hasChange = hasChange || nextState_key !== prevState_key;
});
return hasChange ? newState : state;
}
}
複製代碼
結合以前的 createStore
,咱們看看下面的 demo:ui
function menu(state = {}, action = {}) {
switch (action.type) {
case 'home':
return { ...state, home: action.data };
case 'list':
return { ...state, list: action.data };
case 'detail':
return { ...state, detail: action.data };
default:
return state;
}
}
const reducer = combineReducers({ userName, menu });
const { getState } = createStore(userName);
// 返回的是一個 { userName: {}, menu: {} }
// 這裏和咱們傳遞給 combineReducers() 中的參數的結構是一致的。
console.log(getState());
複製代碼
上面的 reducer
是 userName, menu
的一個組合體,因此每次調用 dispatch(action)
時,都會遍歷全部的 reducers
。還有一個很重要的地方就是,每一個 reducer
函數在沒有匹配到 action.type
時,必須把 reducer()
的參數 state
做爲返回值,不然就報錯。spa
reduxjs
還有一個很是厲害的功能,就是能夠利用中間件,作不少事情。好比說,咱們比較經常使用的 redux-thunk、redux-logger
等。
// 這裏先不考慮參數爲空的狀況
function compose() {
const middleware = [...arguments];
// 這裏利用了redux 高階函數
// 第一次執行時,將 middleware 中的第一個和第二個元素賦值給 a、b。而後將返回的結果函數 fn 賦值給 a。
// 第二次執行時,a 就是上一次的執行結果,這個時候將 middleware 中的第三個元素賦值給 b。而後將返回的結果函數 fn 賦值給 a。
// 第三次,第四次。依次類推。。。
return middleware.reduce(function(a, b) {
return function fn () {
return a(b.apply(null, arguments));
}
});
}
function applyMiddlyWare(createStore) {
return function(reducer) {
// 接收中間件做爲參數
return function(...middlewares) {
const { dispatch, getState, subscription } = createStore(reducer);
// 將 dispatch 賦值給變量 _dispatch
let _dispatch = dispatch;
const disp = (...args) => {
_dispatch(...args);
}
// 將上面定義 disp 內部函數,傳遞給每個中間件函數
// 因此上面的 disp 就構成了一個閉包
const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));
// 這裏又對變量 _dispatch 進行了賦值。這裏理解可能有點繞,後面再詳細介紹
// 注意這裏是一個科裏化函數的調用, 參數 dispatch 是原始,沒有進過改造的
_dispatch = compose(...chain)(dispatch);
return {
dispatch: _dispatch,
getState,
subscription,
}
}
}
}
複製代碼
到這裏爲止,reduxjs 就基本實現了。可是咱們的探討尚未結束,繼續往下看
從上面的代碼咱們能夠看出來,applyMiddlyWare
函數其實就是對 createStore
的一層封裝,最終輸出的 dispatch
是通過中間件改造過的。如今咱們來看看這個 dispatch
究竟是什麼,它和咱們傳入的中間件有什麼關係???
const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));
_dispatch = compose(...chain)(dispatch);
複製代碼
上面的兩行代碼,先遍歷執行中間件,再將變量 chain
傳遞給 compose
函數。因此咱們應該能夠猜想到,表達式 middleware({ dispatch: disp, getState })
應該返回一個函數,否則 compose
中的 reduce
就沒有辦法執行了。
這裏還要考慮到中間件執行的策略,全部的中間件必須串聯起來,挨個往下執行。因此中間件應該還應該接收另外一箇中間件做爲參數。因此如今咱們能夠大體的猜想到一箇中間件應該是這樣的:
function middleware({ dispatch, getState }) {
return function (nextMiddleware) {
return function () {
// 這裏應該先執行一些任務,而後再去執行下一個中間件
...
nextMiddleware();
}
}
}
複製代碼
這個時候其實中間件的模型還不夠完整,少了一些東西。少了什麼了,就是 action
呀!applyMiddlyWare
函數經過中間件對 dispatch
進行改造。因此仍是要接收 action
才能對 state
進行修改。因此這下咱們清楚了
function middleware({ dispatch, getState }) {
return function (nextMiddleware) {
return function (action) {
// 在調用 nextMiddleware 以前能夠進行一些操做
console.log(1111);
// 必須將 action 傳遞給下一個中間件
const result = nextMiddleware(action);
// 在調用 nextMiddleware 以後能夠進行一些操做
console.log(222);
return result;
}
}
}
複製代碼
如今咱們清楚了中間件的模型了,能夠來專門研究一下 applyMiddlyWare
函數返回的 dispatch
是啥玩意了
function compose() {
const middleware = [...arguments];
return middleware.reduce(function(a, b) {
return function fn () {
return a(b.apply(null, arguments));
}
});
}
function one(next) {
console.log('one');
return function one_(action) {
console.log('這是中間件one,你能夠在這裏作不少事情', action);
return next(action)
}
}
function two(next) {
console.log('two');
return function two_(action) {
console.log('這是中間件two,你能夠在next調用以前作一些事情', action);
const result = next(action);
console.log('這是中間件two,也能夠在next調用以後作一些事情', action);
return result;
}
}
function three(next) {
console.log('three');
return function three_(action) {
console.log('這是中間件three,你能夠在這裏作不少事情', action);
return next(action)
}
}
// 能夠把它看成 createStore 函數返回的 dispatch 方法
function dispatch(action) {
console.log(action);
}
// 我這麼寫,你們應該能夠理解哈。由於 compose 函數接收到的實際上是 middleware({ dispatch, getState }) 返回的結果
// 因此這裏的 one, two, three 能夠理解爲是 middleware({ dispatch, getState }) 返回的結果
// 這裏只是作一個簡單的 demo,用不到 dispatch, getState。
var disp = compose(one, two, three)(dispatch);
複製代碼
咱們把 compose(one, two, three)(dispatch)
這段代碼用咱們本身的代碼實現一下,大體就是下面這樣的效果:
var fn = (function(one, two, three) {
var first = function() {
return one(two.apply(null, arguments));
};
var next = function() {
return first(three.apply(null, arguments));
};
return next
})(one, two, three);
var disp = fn(dispatch);
複製代碼
當調用 fn(dispatch)
時,three.apply(null, dispatch)
開始執行,返回一個 three_
函數。繼續往下執行。
first(three_)
開始執行,而後執行 two.apply(null, three_)
,two
執行完成,返回一個 two_
函數。繼續往下執行。
one(two_)
開始執行,並返回一個 one_
函數,這個函數最終做爲 fn(dispatch)
執行的最終結果,並賦值給變量 disp
。
disp(action)
執行時,先調用 one_(action)
而後是 two_(action)
最後是 three_(action)
。注意最後一箇中間件接收的參數不是中間件參數了,而是原始的 dispatch
方法。因此會在最後一箇中間件中執行 dispatch(action)
,從而調用 rducer 函數修改數據源【state】。
執行 disp({data: 1200, type: 'username'})
這段代碼,看下打印的結果是啥
這下咱們就很是清楚了,原來通過 applyMiddlyWare
改造後輸出的 dispatch
方法,在調用時,會挨個執行每個傳入 applyMiddlyWare
函數的中間件,並在最後一箇中間件中調用原始的 dispatch()
方法。
// 中間件1
function thunk ({dispatch, getState}) {
return function (next) {
return function(action) {
if (typeof action === 'function') {
action({dispatch, getState});
} else {
return next(action);
}
}
}
}
// 中間件2
function dialog ({dispatch, getState}) {
return function (next) {
return function(action) {
console.log('prevstate:', getState());
const result = next(action);
console.log('nextstate:', getState());
return result;
}
}
}
複製代碼
// 模擬用戶http請求
function getUserName(name) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'name', data: name})
}, 0);
}
}
function getUserAge(age) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'age', data: age})
}, 0);
}
}
function getUserSex(sex) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'sex', data: sex})
}, 0);
}
}
function getHome(value) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'home', data: value})
}, 0);
}
}
function getList(value) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'list', data: value})
}, 0);
}
}
function getDetail(value) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'detail', data: value})
}, 0);
}
}
複製代碼
// userName, menu 直接複製前面的代碼
var reducer = combineReducers({ userName, menu });
var { dispatch, getState, subscription } = applyMiddlyWare(store)(reducer)(thunk, dialog);
console.log(getState(), 'initState');
const name_button = document.querySelector('.name');
const age_button = document.querySelector('.age');
const sex_button = document.querySelector('.sex');
const home_button = document.querySelector('.home');
const list_button = document.querySelector('.list');
const detail_button = document.querySelector('.detail');
const addListener = document.querySelector('.addListener');
const removeListener = document.querySelector('.removeListener');
name_button.onclick = function() {
dispatch(getUserName('shenxuxiang'))
};
age_button.onclick = function() {
dispatch(getUserAge('29'))
};
sex_button.onclick = function() {
dispatch(getUserSex('man'))
};
home_button.onclick = function() {
dispatch(getHome('home_page'))
};
list_button.onclick = function() {
dispatch(getList('list_page'))
};
detail_button.onclick = function() {
dispatch(getDetail('detail_page'))
};
let removeListen;
addListener.onclick = function() {
removeListen = subscription(function() {
console.log('咱們添加了一個事件監聽器', getState())
})
};
removeListener.onclick = function() {
removeListen && removeListen();
};
複製代碼