手寫傻瓜式 React 全家桶之 Reduxjavascript
手寫傻瓜式 React 全家桶之 React-Reduxjava
本文代碼react
A Predictable State Container for JS Apps(JS應用的可預測狀態容器)git
可預測:實際上指的是純函數(每一個相同的輸入,都會有固定輸出,而且沒有反作用)這個是由 reducer 來保證的,同時方便了測試github
狀態容器: 在 web 頁面中,每一個 DOM 元素都有本身的狀態,好比彈出框有顯示與隱藏兩種狀態,列表當前處於第幾頁,每頁顯示多少條就是這個列表的狀態,存儲這些狀態的對象就是狀態容器。web
雖然說 Redux 是 React 全家桶的一員,但實際上 Redux 與 React 沒有必然聯繫,它能夠應用在任何其餘庫上,只是跟 React 搭配比較火。 redux
接下來分別講解下:設計模式
就是 React 組件,也就是 UI 層markdown
管理數據的倉庫,對外暴露些 APIapp
let store = createStore(reducers);
複製代碼
有如下職責:
getState()
方法獲取 state;dispatch(action)
方法更新 state;subscribe(listener)
註冊監聽器;subscribe(listener)
返回的函數註銷監聽器。action 就是個動做,在組件裏經過 dispatch(action)
來觸發 store 裏的數據更改
action 只是個指令,告訴 store 要進行怎樣的更改,真正更改數據的是 reducer。
默認 React 傳遞數據只能自上而下傳遞,而下層組件要向上層組件傳遞數據時,須要上層組件傳遞修改數據的方法到下層組件,當項目愈來愈大時,這種傳遞方式會很雜亂。
而引用了 Redux,因爲 Store 是獨立於組件,使得數據管理獨立於組件,解決了組件間傳遞數據困難的問題
定義個 store 容器文件,根據 reducer 生成 store
import { createStore } from "redux";
const counterReducer = (state = 0, { type, payload = 1 }) => {
switch (type) {
case "ADD":
return state + payload;
case "MINUS":
return state - payload;
default:
return state;
}
};
export default createStore(counterReducer);
複製代碼
在組件中
import React, { Component } from "react";
import store from "../store";
export default class Redux extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
add = () => {
store.dispatch({ type: "ADD", payload: 1 });
};
minus = () => {
store.dispatch({ type: "MINUS", payload: 1 });
};
render() {
return (
<div className="border"> <h3>加減器</h3> <button onClick={this.add}>add</button> <span style={{ marginLeft: "10px", marginRight: "10px" }}> {store.getState()} </span> <button onClick={this.minus}>minus</button> </div>
);
}
}
複製代碼
經過上面的講解也能夠看到,其實主要的就是 createStore 函數,該函數會暴露 getState,dispatch,subScribe 三個函數 因此先搭下架子,建立 createStore.js
文件
export default function createStore(reducer) {
let currentState;
// 獲取 store 的 state
function getState() {}
// 更改 store
function dispatch() {}
// 訂閱 store 更改
function subscribe() {}
return {
getState,
dispatch,
subscribe,
};
}
複製代碼
接着完善下各方法
返回當前的 state
function getState() {
return currentState
}
複製代碼
接收 action,並更新 store,經過誰更新的呢: reducer
// 更改 store
function dispatch(action) {
// 將當前的 state 以及 action 傳入 reducer 函數
// 返回新的 state 存儲在 currentState
currentState = reducer(currentState, action);
}
複製代碼
做用: 訂閱 state 的更改
如何作: 採用觀察者模式,組件方監聽 subscribe ,並傳入回調函數,在 subscribe 裏註冊回調,並在 dispatch 方法裏觸發回調
let curerntListeners = [];
// 訂閱 state 更改
function subscribe(listener) {
curerntListeners.push(listener);
return () => {
const index = curerntListeners.indexOf(listener);
curerntListeners.splice(index, 1);
};
}
複製代碼
dispatch 方法在更新數據以後,要執行訂閱事件。
// 更改store
function dispatch(action) {
// store裏面數據就更新了
currentState = reducer(currentState, action);
// 執行訂閱事件
curerntListeners.forEach(listener => listener());
}
複製代碼
將上面計數器裏的 redux 改爲引用手寫的 redux,會發現頁面沒有最初值
因此在 createStore 里加上 dispatch({ type: "kkk" });
給 state 賦初始值,要注意傳入的這個 type 要進入 reducer 函數的 default 條件
完整代碼以下:
export default function createStore(reducer) {
let currentState;
let curerntListeners = [];
// 獲取 store 的 state
function getState() {
return currentState;
}
// 更改 store
function dispatch(action) {
// 將當前的 state 以及 action 傳入 reducer 函數
// 返回新的 state 存儲在 currentState
currentState = reducer(currentState, action);
// 執行訂閱事件
curerntListeners.forEach((listener) => listener());
}
// 訂閱 state 更改
function subscribe(listener) {
curerntListeners.push(listener);
return () => {
const index = curerntListeners.indexOf(listener);
curerntListeners.splice(index, 1);
};
}
dispatch({ type: "kkk" });
return {
getState,
dispatch,
subscribe,
};
}
複製代碼
你們也能夠自行查看下 Redux 裏的 createStore 源碼
說到 Redux,那就不得不提中間件,由於自己 Redux 能作的東西頗有限,好比須要 redux-thunk 來達到異步調用,redux-logger 記錄日誌等。 中間件就是個函數,在組件發出 Action 和執行 Reducer 這兩步之間,添加其餘功能,至關於增強 dispatch。
開發中間件是有模板代碼的
export default store => next => action => {}
複製代碼
好比模擬寫個 logger 中間件
function logger(store) {
return (next) => {
return (action) => {
console.log("====================================");
console.log(action.type + "執行了!");
console.log("prev state", store.getState());
next(action);
console.log("next state", store.getState());
console.log("====================================");
};
};
}
export default logger;
複製代碼
// 在createStore的時候將applyMiddleware做爲第二個參數傳進去
const store = createStore(
reducer,
applyMiddleware(logger)
)
複製代碼
能夠看出是經過 createStore 的第二個參數來實現的,這個參數官方稱爲 enhancer。 enhancer 是個參數爲 createStore 的函數,並返回個新的 createStore 函數
function enhancer (createStore) {
return function (reducer) {
var store = createStore(reducer);
var dispatch = store.dispatch;
function _dispatch (action) {
if (typeof action === 'function') {
return action(dispatch)
}
dispatch(action);
}
return {
...store,
dispatch: _dispatch
}
}
}
複製代碼
實現上 createStore 總共是有三個參數,除了第一個 reducer 參數是必傳的以外,第二個 state 初始值,以及第三個 enhancer 都是可選的
下面咱們在手寫的 createStore 里加入 enhancer 參數, 以及手寫下 applyMiddleware 函數
function createStore(reducer,enhancer) {
// 判斷是否存在 enhancer
// 若是存在而且是個函數, 則將 createStore 傳遞給它, 不是函數則拋出錯誤
// 它會返回個新的 createStore
// 傳入 reducer ,執行新的 createStore,返回 store
// 返回該 store
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('enhancer必須是函數')
}
return enhancer(createStore)(reducer)
}
// 沒有 enhancer 走原先的邏輯
// 省略
}
複製代碼
按上面的分析,applyMiddleware 函數,會接收中間件函數,並返回個 enhancer,因此基本結構爲
export default function applyMiddleware(...middlewares) {
// applyMiddleware 的返回值應該是一個 enhancer
// enhancer 是個接收 createStore 爲參數的函數
return function (createStore) {
// enhancer 要返回一個新的 createStore
return function newCreateStore(reducer) {};
};
}
複製代碼
看下 logger 中間件的結構,
function logger(store) {
return (next) => {
return (action) => {
console.log("====================================");
console.log(action.type + "執行了!");
console.log("prev state", store.getState());
next(action);
console.log("next state", store.getState());
console.log("====================================");
};
};
}
export default logger;
複製代碼
完善下 applyMiddleware
export default function applyMiddleware(middleware) {
// applyMiddleware 的返回值應該是一個 enhancer
// enhancer 是個接收 createStore 爲參數的函數
return function (createStore) {
// enhancer 要返回一個新的 createStore
return function newCreateStore(reducer) {
// 建立 store
let store = createStore(reducer);
let dispatch = store.dispatch;
// dispatch 屬性必定要寫成這種形式,不能直接將 store.dispatch 傳入
// 由於有多箇中間件時, dispatch 的值是要獲取上一個中間件增強後的 dispatch
// 這種傳遞方式有效,是因爲 dispatch 是引用類型
const midApi = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};
// 傳入 store 執行中間件的第一層函數
const chain = middleware(midApi);
// 將原始的 dispatch 函數做爲 next 參數傳給 chain,調用中間件的第二層函數
// 返回增強的 dispatch 覆蓋原先的 dispatch
dispatch = chain(dispatch);
return {
...store,
dispatch,
};
};
};
}
複製代碼
測試: 是能夠正常打印出日誌的
上面已經在 applyMiddleware 函數裏處理只有一箇中間件的狀況,那多個的場景呢?
首先咱們再模擬寫個 redux-thunk 中間件
默認 redux 只支持同步,而且參數只能是對象形式,redux-thunk 要實現的是你傳入一個函數時,我就直接執行該函數,異步操做代碼寫在傳遞過來的函數裏,若是傳遞過來的是個對象,則調用下一個中間件
function thunk({ getState, dispatch }) {
return next => {
return action => {
// 若是是個函數,則直接執行,並傳入 dispatch 與 getState
if (typeof action == 'function') {
return action(dispatch, getState)
}
next(action)
}
}
}
複製代碼
如今要去依次執行各個中間件,要如何依次執行呢?就得采用柯里化,首先寫個 compose 函數
function compose(...funs) {
// 沒有傳遞函數時,則返回參數透傳函數
if (funs.length === 0) {
return (arg) => arg
}
// 傳遞一個函數時,則直接返回該函數,省去了遍歷
if (funs.length === 1) {
return funs[0]
}
// 傳遞多個時,則採用 reduce,進行合併
// 好比執行 compose(f1,f2,f3) 則會返回 (...args) => f1(f2(f3(...args)))
return funs.reduce((a, b) => {
return (...args) => {
return a(b(...args))
}
})
}
複製代碼
applyMiddleware 函數支持多箇中間件:
export default function applyMiddleware(...middlewares) {
// applyMiddleware 的返回值應該是一個 enhancer
// enhancer 是個接收 createStore 爲參數的函數
return function (createStore) {
// enhancer 要返回一個新的 createStore
return function newCreateStore(reducer) {
// 建立 store
let store = createStore(reducer);
let dispatch = store.dispatch;
// dispatch 屬性必定要寫成這種形式,不能直接將 store.dispatch 傳入
// 由於有多箇中間件時, dispatch 的值是要獲取上一個中間件增強後的 dispatch
// 這種傳遞方式有效,是因爲 dispatch 是引用類型
const midApi = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};
// 調用中間件的第一層函數 傳遞閹割版的 store 對象
const middlewareChain = middlewares.map((middle) => middle(midApi));
// 用 compose 獲得一個組合了全部中間件的函數
const middleCompose = compose(...middlewareChain);
// 將原始的 dispatch 函數做爲參數逐個調用中間件的第二層函數
// 返回增強的 dispatch 覆蓋原先的 dispatch
dispatch = middleCompose(dispatch);
return {
...store,
dispatch,
};
};
};
}
複製代碼
驗證:
import { createStore } from "../kredux";
import logger from "../kredux/middlewares/logger";
import thunk from "../kredux/middlewares/thunk";
import applyMiddleware from "../kredux/applyMiddleware";
const counterReducer = (state = 0, { type, payload = 1 }) => {
switch (type) {
case "ADD":
return state + payload;
case "MINUS":
return state - payload;
default:
return state;
}
};
export default createStore(counterReducer, applyMiddleware(thunk, logger));
複製代碼
並將 add 函數更改爲異步觸發 dispatch
add = () => {
// store.dispatch({ type: "ADD", payload: 1 });
store.dispatch(function (dispatch) {
setTimeout(() => {
dispatch({ type: "ADD", payload: 2 });
}, 1000);
});
};
複製代碼
當業務邏輯複雜時,不可能都寫在一個 reducer 裏,這時就得使用 combineReducers 將幾個 reducer 組合起來。
再添加個 userReducer:
const userReducer = (state = { ...initialUser }, { type, payload }) => {
switch (type) {
case "SET":
return { ...state, ...payload };
default:
return state;
}
};
複製代碼
引入 combineReducers ,該函數接收個對象,key 爲標識,value 爲每一個 reducer
export default createStore(
combineReducers({ count: counterReducer, user: userReducer }),
applyMiddleware(thunk, logger)
);
複製代碼
根據上面的分析,手寫個 combineReducers ,它要返回個 reducer 函數,reducer 函數天然是要接收 state 跟 action 並返回新的 state
export default function combineReducers(reducers) {
return function reducer(state = {}, action) {
let nextState = {};
// 遍歷全部的 reducers,並依次觸發返回新的 state
for (let key in reducers) {
nextState[key] = reducers[key](state[key], action);
}
return nextState;
};
}
複製代碼
export default store => next => action => {}
的函數