手寫傻瓜式 React 全家桶之 Redux

文章系列

手寫傻瓜式 React 全家桶之 Reduxjavascript

手寫傻瓜式 React 全家桶之 React-Reduxjava

本文代碼react

1、什麼是 Redux

A Predictable State Container for JS Apps(JS應用的可預測狀態容器)git

可預測:實際上指的是純函數(每一個相同的輸入,都會有固定輸出,而且沒有反作用)這個是由 reducer 來保證的,同時方便了測試github

狀態容器: 在 web 頁面中,每一個 DOM 元素都有本身的狀態,好比彈出框有顯示與隱藏兩種狀態,列表當前處於第幾頁,每頁顯示多少條就是這個列表的狀態,存儲這些狀態的對象就是狀態容器。web

雖然說 Redux 是 React 全家桶的一員,但實際上 Redux 與 React 沒有必然聯繫,它能夠應用在任何其餘庫上,只是跟 React 搭配比較火。 redux Flowredux

Redux 的核心概念

  1. Store: 存儲狀態的容器, JS 對象
  2. Actions: 對象,描述對狀態進行怎樣的操做
  3. Reducers: 函數,操做狀態並返回新的狀態
  4. React Component: 組件,或者稱之爲視圖

Redux 工做流程

  1. 組件經過 dispatch 方法觸發 Action
  2. Store 接收 Action 並將 Action 分發給 Reducer
  3. Reducer 根據 Action 類型對狀態進行更改並將更改後的狀態返回給 Store
  4. 組件訂閱了 Store 中的狀態,Store 中的狀態更新會同步到組件

接下來分別講解下:設計模式

React Components

就是 React 組件,也就是 UI 層markdown

Store

管理數據的倉庫,對外暴露些 APIapp

let store = createStore(reducers);
複製代碼

有如下職責:

  • 維持應用的 state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 經過 subscribe(listener) 註冊監聽器;
  • 經過 subscribe(listener) 返回的函數註銷監聽器。

Action

action 就是個動做,在組件裏經過 dispatch(action) 來觸發 store 裏的數據更改

Reducer

action 只是個指令,告訴 store 要進行怎樣的更改,真正更改數據的是 reducer。

2、爲何要使用 Redux

默認 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>
    );
  }
}

複製代碼
  • 經過 getState 顯示 state
  • 點擊 add 或 minus 時觸發 dispatch 並傳遞指令(action)
  • 並在 componentDidMount 監聽 state 更改,有更改則就 forceUpdate 強制渲染
  • componentWillUnmount 清除監聽

加減器

3、開始手寫

經過上面的講解也能夠看到,其實主要的就是 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,
  };
}
複製代碼

接着完善下各方法

getState

返回當前的 state

function getState() {
    return currentState
}
複製代碼

dispatch

接收 action,並更新 store,經過誰更新的呢: reducer

// 更改 store
  function dispatch(action) {
    // 將當前的 state 以及 action 傳入 reducer 函數
    // 返回新的 state 存儲在 currentState
    currentState = reducer(currentState, action);
  }
複製代碼

subscribe

做用: 訂閱 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 源碼

4、Redux 中間件

說到 Redux,那就不得不提中間件,由於自己 Redux 能作的東西頗有限,好比須要 redux-thunk 來達到異步調用,redux-logger 記錄日誌等。 中間件就是個函數,在組件發出 Action 和執行 Reducer 這兩步之間,添加其餘功能,至關於增強 dispatch。

image.png

開發 Redux 中間件

開發中間件是有模板代碼的

export default store => next => action => {}
複製代碼
  1. 一箇中間件接收 store 做爲參數,返回一個函數
  2. 返回的這個函數接收 next(老的 dispatch 函數) 做爲參數,返回一個新的函數
  3. 返回的新的函數就是增強的 dispatch 函數,在這個函數裏能夠拿到上面兩層傳遞進來的 store 以及 next

好比模擬寫個 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 函數

createStore 里加上 enhancer

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

按上面的分析,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);
    });
  };
複製代碼

中間件

5、手寫 combineReducers

當業務邏輯複雜時,不可能都寫在一個 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;
  };
}

複製代碼

6、總結

  1. Redux 自己就只是個可預測性狀態容器,狀態的更改都是經過發出 Action 指令,Action 指令會分發給 Reducer ,再由 Reducer 返回新的狀態值,組件進行監聽,但狀態值更改了,就從新渲染。
  2. Redux 是典型的觀察者模式,subscribe 時註冊回調事件,dispatch action 時執行回調
  3. Redux 重點在於 createStore 函數,該函數會返回三個方法,其中 getState 用於返回當前的 state,subscribe 用於訂閱 state 的更改,dispatch 更新 store,並執行回調事件
  4. 默認的 Redux 只支持傳入對象,以及只能執行同步,要知足更多的場景就須要採用中間件
  5. 中間件是個模板爲export default store => next => action => {} 的函數
  6. 註冊中間件,就要使用 createStore 的 enhancer
  7. Redux 的中間件是對 dispatch 進行增強,這就是典型的裝飾者模式,能夠看出 Redux 裏處處是設計模式
相關文章
相關標籤/搜索