redux以及react-redux簡單實現

寫在前頭

redux 簡介

  隨着 JavaScript 單頁應用開發日趨複雜,JavaScript 須要管理比任什麼時候候都要多的 state (狀態)。 這些 state 可能包括服務器響應、緩存數據、本地生成還沒有持久化到服務器的數據,也包括 UI 狀態,如激活的路由,被選中的標籤,是否顯示加載動效或者分頁器等等。css

  管理不斷變化的 state 很是困難。若是一個 model 的變化會引發另外一個 model 變化,那麼當 view 變化時,就可能引發對應 model 以及另外一個 model 的變化,依次地,可能會引發另外一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在何時,因爲什麼緣由,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者添加新功能就會變得舉步維艱。html

  若是這還不夠糟糕,考慮一些來自前端開發領域的新需求,如更新調優、服務端渲染、路由跳轉前請求數據等等。前端開發者正在經受史無前例的複雜性,難道就這麼放棄了嗎?固然不是。前端

  這裏的複雜性很大程度上來自於:咱們老是將兩個難以理清的概念混淆在一塊兒:變化和異步。 若是把兩者分開,能作的很好,但混到一塊兒,就變得一團糟。一些庫如 React 試圖在視圖層禁止異步和直接操做 DOM 來解決這個問題。美中不足的是,React 依舊把處理 state 中數據的問題留給了咱們本身。而 redux 就能夠來幫我管理這些狀態;react

demo 演示

![](./img/Peek 2018-08-20 15-03.gif)

demo 結構樹
├── config-overrides.js
├── .gitignore
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
├── README.md
└── src
    ├── App.js
    ├── Demo
    │   ├── actionCreate.js
    │   ├── Demo.jsx
    │   ├── react-redux.js
    │   ├── reducer.js
    │   ├── redux.js
    │   ├── style.css
    │   └── thunk.js
    └── index.js

1、 redux API createStore 的實現

  首先咱們先結合 reducer 以及 action 的知識簡單實現開頭展現的 demo, 並逐步揭曉 createStore 的神祕面紗;git

1.1 準備工做:
建立 reducer 並導出 reducer
// reducer.js
const initState = { user: 'qianyin', age: 18, sex: '男' };
export const reducer = (state=initState, action) => {
  switch(action.type){
    case 'USER_UPDATE':
      return {...state, ...action.payload};
    case 'AGE_GROW':
      return {...state, age: state.age + 1};
    case 'SEX_UPDATE':
      return {...state, ...action.payload};
    default:
      return state;
  }
}
建立 action 建立函數
// actionCreate.js
export const changeUser = (user) => {
  return {
    payload:{user},
    type: 'USER_UPDATE',
  };
}

export const changeAge = () => {
  return { type: 'AGE_GROW' };
}
經過 react 在頁面上預先繪製出基本的元素
/* style.css */
.btn{
  height: 31px;

}
.input{
  height: 25px;
}
// Demo.jsx
import React from 'react';
import './style.css';

export default class Demo extends React.Component{
  onChange = () => {}
  onClick = () => {}
  render(){
    return (
      <div>
        <p>user: xxx, age: xxx</p>
        user: 
        <input type="text" className="input" onChange={this.onChange}/>
        &nbsp;
        <button className="btn" onClick={this.onClick}>年齡增加</button>
      </div>
    );
  }
}
最終頁面將渲染以下:

圖片描述

1.2 demo 的初次實現代碼
  • 建立全局狀態 state;
  • 建立監聽隊列;
  • 針對監聽隊列,新增函數用於將指定監聽對象添加到隊列中;
  • 在函數 dispatch 中執行 reducer 將返回值做爲新的 state, 同時依次執行監聽對象;
  • 默認執行一次 dispatch 給定一個 type 相對惟一的 action, 目的是爲了匹配 reducer 的默認狀態值,從而實現對 redux state 的初始化;
  • 在組件 Demo 經過在函數 update 使用 this.setState 將全局 state 保存到 react state 中,並將函數 update 添加到監聽隊列中;從而使得當咱們一旦試圖經過 dispatch 修改全局狀態時,可以及時更新 react state 最終觸發 react 生命週期 render;
  • 在 react 生命週期 componentDidMount 中咱們除了將 update 添加到監聽隊列之外,還需手動執行一次 update 其主要目的就是爲了首次初始化 react state;
// Demo.jsx
import React from 'react';
import { changeAge, changeUser } from './actionCreate';
import { reducer } from './reducer';
import './style.css';

let state;
const listeners = [];
const subscribe = (listener) => {
  listeners.push(listener);
}
const dispatch = (action) => {
  state = reducer(state, action);
  console.log(state);
  listeners.forEach(v => v());
}
dispatch({type: '%$&HJKAJJHDJHJ'});

export default class Demo extends React.Component{
  state = {user: 'xxx', age: 'xxx'};
  componentDidMount(){
    subscribe(this.update);
    this.update();
  }

  update = () => {
    this.setState(state);
  }

  onChange = (e) => {
    dispatch(changeUser(e.target.value));
  }

  onClick = () => {
    dispatch(changeAge());
  }

  render(){
    return (
      <div>
        <p>user: {this.state.user}, age: {this.state.age}</p>
        user: 
        <input type="text" className="input" onChange={this.onChange}/>
        &nbsp;
        <button className="btn" onClick={this.onClick}>年齡增加</button>
      </div>
    );
  }
}
1.3 API createStore 的實現

  其實上文的代碼中對於 createStore 的實現原理已經基本描述清除,下面咱們只是單純的對代碼進行了簡單的封裝;固然爲了可以獲取到 state 咱們專門增長了一個函數 getState 來實現它;shell

createStore 函數實現
// redux.js

export const createStore = (reducer) => {
  // 聲明常量
  let state;
  const listeners = [];

  // 獲取狀態
  const getState = () => {
    return state;
  }

  // 添加監聽對象
  const subscribe = (listener) => {
    listeners.push(listener);
  }

  // [1]執行reducer修改狀態 [2]遍歷執行監聽對象
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(v => v());
  }

  // 初始化 state
  dispatch({type: '%$&HJKAJJHDJHJ'});
    
  // 暴露接口
  return {getState, subscribe, dispatch};
}
調用 createStore 並對 demo 進行修改
// Demo.jsx
import React from 'react';
import './style.css';
import { changeAge, changeUser } from './actionCreate';
import { reducer } from './reducer';
import { createStore } from './redux';

const store = createStore(reducer);

export default class Demo extends React.Component{
  state = {user: 'xxx', age: 'xxx'};
  componentDidMount(){
    store.subscribe(this.update);
    this.update();
  }

  update = () => {
    this.setState(store.getState());
  }

  onChange = (e) => {
    store.dispatch(changeUser(e.target.value));
  }

  onClick = () => {
    store.dispatch(changeAge());
  }

  render(){
    return (
      <div>
        <p>user: {this.state.user}, age: {this.state.age}</p>
        user: 
        <input type="text" className="input" onChange={this.onChange}/>
        &nbsp;
        <button className="btn" onClick={this.onClick}>年齡增加</button>
      </div>
    );
  }
}

2、 react-redux API Provider 的實現

  在 react 中大多數狀況下咱們須要將狀態傳遞給後代組件進行使用的,固然經過 props 是能夠實現狀態從父級到子級的傳遞,可是當狀態須要傳遞的層級比較深的狀況下再使用 props 就顯得無力了,那麼在 react-redux 中它是如何實現對 store 的傳遞的呢?json

2.1 react context 的引入
在 App.js 中建立 store 並經過 context 傳遞 store
// App.js
import React, { Component } from 'react';
import propTypes from 'prop-types';
import { createStore } from './Demo/redux';
import { reducer } from './Demo/reducer';
import Demo from './Demo/Demo';

// 建立 store
const store = createStore(reducer);

class App extends Component {
  // 聲明 childContextTypes 狀態屬性類型
  static childContextTypes = {
    store: propTypes.object
  };
  // 設置 childContext
  getChildContext(){
    return {store}
  }
  render() {
    return <Demo />;
  }
}
export default App;
在子組件 Demo 中經過 context 獲取 store 並對代碼進行簡單修改
// Demo.jsx
import React from 'react';
import propTypes from 'prop-types';
import './style.css';
import { changeAge, changeUser } from './actionCreate';

export default class Demo extends React.Component{
  // 設置 context 狀態值類型
  static contextTypes = {
    store: propTypes.object
  };

  constructor(props, context){
    super(props, context);
    // 獲取store
    this.store = context.store;
    this.state = {user: 'xxx', age: 'xxx'};
  }
  
  componentDidMount(){
    this.store.subscribe(this.update);
    this.update();
  }

  update = () => {
    this.setState(this.store.getState());
  }

  onChange = (e) => {
    this.store.dispatch(changeUser(e.target.value));
  }

  onClick = () => {
    this.store.dispatch(changeAge());
  }

  render(){
    return (
      <div>
        <p>user: {this.state.user}, age: {this.state.age}</p>
        user: 
        <input type="text" className="input" onChange={this.onChange}/>
        &nbsp;
        <button className="btn" onClick={this.onClick}>年齡增加</button>
      </div>
    );
  }
}
2.2 封裝代碼實現 Provider

  經過 react context 咱們實現了對 store 的傳遞,到這裏 Provider 的功能以及實現原理基本上應該算是清晰了,無非就是對組件進行包裹同時經過 react context 來傳遞共享 store;那麼接下來咱們經過對代碼的封裝來實現 Provider 組件;redux

Provider 組件:實現對 store 的傳遞
// react-redux.js
import React from 'react';
import propTypes from 'prop-types';

export class Provider extends React.Component{
  // 設置 childContext 狀態值類型
  static childContextTypes = {
    store: propTypes.object
  };

  // 設置 childContext
  getChildContext(){
    return {store: this.props.store}
  }
  
  render(){
    return this.props.children;
  }
}
重寫 App.js: 對 Provider 組件的調用
// App.js
import React, { Component } from 'react';
import { createStore } from './Demo/redux';
import { Provider } from './Demo/react-redux';
import { reducer } from './Demo/reducer';
import Demo from './Demo/Demo';

// 建立 store
const store = createStore(reducer);

class App extends Component {
  render() {
    // 調用接口 Provider
    return <Provider store={store}><Demo /></Provider>;
  }
}
export default App;

3、 react-redux API connect 高階組件的實現

  上文中在後代組件若是須要獲取 store 則須要手動經過獲取 react context 來調用 store 而且須要顯性的調用 store 內部的方法來進行一些操做;接下來咱們來實現這麼一個高階組件 connect,咱們只須要提供所需的 redux state 以及 action 建立函數,便可經過 props 獲取到相應的 redux state , 而且容許直接經過 props 調用action 建立函數來試圖修改 redux state ;緩存

建立高階組件 connect
// react-redux.js
export const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
  return class NewComponent extends React.Component{
    render(){
      return <Component />
    }
  }
}
獲取store
// react-redux.js
export const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
  return class NewComponent extends React.Component{
    // 設置 context 狀態值類型
    static contextType = {
      store: propTypes.object
    };
      // [1]獲取 store [2]設置空 react state
    constructor(props, context){
      super(props, context);
      this.store = context.store;
      this.state = {};
    }
    render(){
      return <Component />
    }
  }
}
添加監聽對象,並嘗試經過 props 將狀態傳遞給子組件
export const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
  return class NewComponent extends React.Component{
    static contextType = {
      store: propTypes.object
    };
    constructor(props, context){
      super(props, context);
      this.store = context.store;
      this.state = {};
    }
      // [1]添加監聽對象 [2]手動執行監聽對象,初始化 react state
    componentDidMount(){
      this.store.subscribe(this.update);
      this.update();
    }
  
    update = () => {
      // 獲取所有redux state 並添加到 react state
      const state = this.store.getState();
      this.setState(state);
    }
    render(){
      // 經過 props 將 react state 所有傳給子組件
      return <Component {...this.state} />
    }
  }
}
經過 mapStateToProps 獲取指定 redux state
// react-redux.js
export const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
  return class NewComponent extends React.Component{
    static contextType = {
      store: propTypes.object
    };
    constructor(props, context){
      super(props, context);
      this.store = context.store;
      this.state = {};
    }
    componentDidMount(){
      this.store.subscribe(this.update);
      this.update();
    }
  
    update = () => {
      // 執行 mapStateToProps 只獲取用戶指定需求的 state
      const state = this.store.getState();
      const filterState = mapStateToProps(state);
      this.setState(filterState);
    }
    render(){
      return <Component {...this.state} />
    }
  }
}
經過 mapDispatchToProps 獲取 action 建立函數: 使用 dispatch 包裹後返回
// react-redux.js
// react-redux.js
export const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
  return class NewComponent extends React.Component{
    static contextTypes = {
      store: propTypes.object
    };
    constructor(props, context){
      super(props, context);
      this.store = context.store;
      this.state = {};
    }
    componentDidMount(){
      this.store.subscribe(this.update);
      this.update();
    }
  
    update = () => {
      // 處理 state ===> 獲取用戶指定的 state
      const state = this.store.getState();
      const filterState = mapStateToProps(state);

      // 使用 dispatch 對 mapDispatchToProps 中的 action 建立函數進行包裹後返回
      const actionFun = {};
      for(let key in mapDispatchToProps){
        actionFun[key] = (...args) => {
          this.store.dispatch(mapDispatchToProps[key](...args));
        }
      }
  // 一種簡寫方式: 騷操做
  // const actionFun = Object.keys(mapDispatchToProps)
  //  .reduce((total, item) => {
  //    return { ...total, [item]: (...args) => {dispatch(mapDispatchToProps[item](...args));}
  //   } } ,{});

      this.setState({...filterState, ...actionFun});
    }
    render(){
      return <Component {...this.state} />
    }
  }
}
調用高階組件:修改 Demo.jsx
// Demo.jsx
import React from 'react';
import { changeAge, changeUser } from './actionCreate';
import { connect } from './react-redux';
import './style.css';

// 編寫 mapStateToProps 參數 redux state 返回所需的 redux state
const mapStateToProps = (state) => {
  return {user: state.user, age: state.age};
}

// 調用高階組件
@connect(mapStateToProps, {changeAge, changeUser})
export default class Demo extends React.Component{
  onChange = (e) => {
    this.props.changeUser(e.target.value);
  }
  onClick = () => {
    this.props.changeAge();
  }
  render(){
    return (
      <div>
        <p>user: {this.props.user}, age: {this.props.age}</p>
        user: 
        <input type="text" className="input" onChange={this.onChange}/>
        &nbsp;
        <button className="btn" onClick={this.onClick}>年齡增加</button>
      </div>
    );
  }
}

4、redux API bindactioncreators 的實現

  在上文咱們對 mapDispatchToProps 的處理過程就是 API bindactioncreators 的功能: 將給定 action 建立函數使用 dispatch 進行包裹後返回;服務器

封裝 bindactioncreators
// redux.js
export const bindactioncreators = (mapDispatchToProps, dispatch) => {
  const actionFun = {};
  // 遍歷 mapDispatchToProps 中每一個 action 建立函數 並使用 dispatch 包裹後返回
  for(let key in mapDispatchToProps){
    actionFun[key] = (...args) => {
      dispatch(mapDispatchToProps[key](...args));
    }
  }

  return actionFun;
  // 一種簡寫方式: 騷操做
  // return actionFun = Object.keys(mapDispatchToProps)
  //  .reduce((total, item) => {
  //    return { ...total, [item]: (...args) => {dispatch(mapDispatchToProps[item](...args));}
  //   } } ,{});
}
修改 connect :
// react-redux.js
import { bindactioncreators } from './redux';
....
export const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
  return class NewComponent extends React.Component{
    static contextTypes = {
      store: propTypes.object
    };
    constructor(props, context){
      super(props, context);
      this.store = context.store;
      this.state = {};
    }
    componentDidMount(){
      this.store.subscribe(this.update);
      this.update();
    }
  
    update = () => {
      const state = this.store.getState();
      const filterState = mapStateToProps(state);
      
      // 調用 API bindactioncreators 
      // 對 mapDispatchToProps 內每一個 action 建立函數使用 dispatch 進行包裹後返回
      const actionFun = bindactioncreators(mapDispatchToProps, this.store.dispatch);
      this.setState({...filterState, ...actionFun});
    }
    render(){
      return <Component {...this.state} />
    }
  }
}

5、redux API applyMiddleware 的實現

  到此簡化版的 react-redux 算是已經初步完成,可是假如咱們想要咱們的 age 值的增加是一個異步操做,好比:經過按鈕點擊後通過兩秒再修改 age ,而不是一點擊按鈕就當即修改值;這樣咱們又該怎麼實現呢?
  固然咱們能夠經過 setTimeout 兩秒後再執行 action 建立函數,好比這樣:

onClick = () => {
  setTimeout(()=>{
      // 兩秒後執行 action 建立函數
      this.props.changeAge();
  }, 2000);
}

  可是呢事實上咱們並不肯意像上面那麼整,咱們想要這麼一種效果:咱們只須要簡單的調用 action 建立函數便可實現異步操做,而不是須要進行額外的操做;這時咱們就須要爲咱們的 react-redux 編寫一箇中間件來實現這麼一個效果;

5.1 準備工做
新增action 建立函數

  在這以前咱們全部的 acton 建立函數都是直接返回一個 action 對象,下面咱們寫一個不同的 action 建立函數, 它返回的再也不是一個 action 對象而是一個函數,而且該函數接收兩個參數 dispatch 以及 getState, 在該函數內部咱們進行相應的異步操做,好比:修改 age 值;

// actionCreate.js
export const asyncChangeAge = () => {
  // 返回函數
  return (dispatch, getState) => {
    setTimeout(v=>{
      console.log('==>', getState());
      dispatch({type: 'AGE_GROW'});
    }, 1000);
  }
}
修改頁面:新增按鈕並綁定點擊事件,而且調用 asyncChangeAge 函數;
// Demo.jsx
// Demo.jsx
import React from 'react';
import './style.css';
// 導入 asyncChangeAge
import { changeAge, changeUser, asyncChangeAge } from './actionCreate';
import { connect } from './react-redux';

const mapStateToProps = (state) => {
  return {user: state.user, age: state.age};
}

// 添加 asyncChangeAge
@connect(mapStateToProps, {changeAge, changeUser, asyncChangeAge})
export default class Demo extends React.Component{
  onChange = (e) => {
    this.props.changeUser(e.target.value);
  }
  onClick = () => {
      this.props.changeAge();
  }
  // 點擊事件
  onClickAsync = () => {
    this.props.asyncChangeAge();
  }
  render(){
    return (
      <div>
        <p>user: {this.props.user}, age: {this.props.age}</p>
        user: 
        <input type="text" className="input" onChange={this.onChange}/>
        &nbsp;
        <button className="btn" onClick={this.onClick}>年齡增加</button>
        {/* 新增按鈕 */}
        <button className="btn" onClick={this.onClickAsync}>
            異步增加
        </button>
      </div>
    );
  }
}
頁面的變化其實很簡單就是新增了一個按鈕:

圖片描述

5.2 需求實現

  接下來咱們先什麼都不考慮先來實現咱們的需求,如今 action 建立函數 asyncChangeAge 由於返回的是一個對象,其 type 值爲 undefined 因此當咱們點擊按鈕時 reducer 將會一直匹配到默認狀況,返回的將是當前的狀態,接下來咱們先讓咱們的 action 建立函數 asyncChangeAge 生效,達到異步修改狀態的做用;

擴展 createStore

  既然 asyncChangeAge 返回的再也不是一個 action 對象,而是一個函數;那麼其實咱們要作的事情是很簡單的,咱們只須要針對 createStore 中的返回值 dispatch 進行一個簡單的擴展便可;經過判斷 dispatch 中的 action 參數是不是函數而進行不一樣的操做:

// redux.js
export const createStore = (reducer) => {
    ......
    // 在createStore 咱們對返回值 dispatch 進行了封裝
  const dispatchExtend = (action) => {
    if(typeof action === 'function'){
      // action 爲函數,執行函數
      action(dispatch, getState);
    } else {
      // action 爲非函數(對象)調用dispatch
      dispatch(action);
    }
  }
  return {getState, dispatch: dispatchExtend, subscribe};
}
5.3 抽離封裝

  上文咱們經過對 createStore 的返回值 dispatch 進行了擴展,實現了 redux-react 的異步操做,但問題是咱們將代碼寫死在 createStore 中了,redux-react 的異步操做應該是一個可選項而不該該是必選項;

從新擴展 createStore :

  新增參數 middleware (函數), 在函數 createStore 開始位置判斷 middleware 是否存在,存在則執行;

// redux.js
export const createStore = (reducer, middleware) => {
    // 判斷 middleware 是否存在,存在則執行
  if(middleware){
    return middleware(createStore)(reducer);
  }

  let state;
  const listeners = [];

  const getState = () => {
    return state;
  }

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(v => v());
  }

  const subscribe = (listener) => {
    listeners.push(listener);
  }

  dispatch({type: '%$&HJKAJJHDJHJ'});

  return {getState, dispatch, subscribe};
}
編寫函數 applyMiddleware ,在建立 store 時 做爲 createStore 第二參數
// App.js
const applyMiddleware = (createStore) => (redux)=> {
  // 在這裏進行建立 store
  const store = createStore(redux);
  // 返回store
  return {...store}
}

const store = createStore(reducer, applyMiddleware);
在 applyMiddleware 函數內擴展 dispatch

  上文 applyMiddleware 函數並其實沒作任何事情, 只是在 createStore 函數外面套了一層函數,那麼接下來咱們作點正事,來擴展一下咱們的 dispatch

// App.js
const applyMiddleware = (createStore) => (redux)=> {
  const store = createStore(redux);

  const midApi = {
    getState: store.getState,
    dispatch: (...args) => {dispatch(...args);}
  };

  const dispatch = (action) => {
    if( typeof action === 'function' ){
      action(midApi.dispatch, midApi.getState);
    } else {
      store.dispatch(action);
    }
  }

  return {
    ...store,
    dispatch
  };
}
如今咱們來看看效果:

5.4 擴展分離

  上文已經實現了咱們想要的效果了,咱們在 applyMiddleware 對 dispatch 進行了擴展;然而咱們是那麼容易知足的嘛,固然不是的!! applyMiddleware 中對 dispatch 的擴展咱們還能夠將其單獨提出來封裝成一個函數;

重寫 applyMiddleware ,再給 applyMiddleware 包裹一層函數: 將對 dispatch 的擴展抽離,封裝成方法;
// App.js
const applyMiddleware = (middleware) => (createStore) => (redux)=> {
  const store = createStore(redux);

  const midApi = {
    getState: store.getState,
    dispatch: (...args) => {dispatch(...args);}
  };

  const dispatch = middleware(midApi)(store.dispatch);

  return {
    ...store,
    dispatch
  };
}
thunk 中間件: 其實 thunk 纔是真正的中間件;applyMiddleware 只是用來綁定中間件的
// App.js 
const thunk = ({dispatch, getState}) => next => (action) => {
  if(typeof action === 'function'){
    action(dispatch, getState);
  } else {
    next(action);
  }
};
在調用 createStore 時綁定中間件 thunk
// App.jsx
const store = createStore(reducer, applyMiddleware(thunk));
5.5 代碼整理

  這一步只是將 applyMiddleware 函數移到 redux.js 文件中;同時將 thunk 函數寫到文件 thunk.jsx 中,而後在 App.js 中引用 applyMiddleware 以及 thunk 而已;

看下最終效果(效果和上一個錄屏是同樣樣的)

圖片描述

相關文章
相關標籤/搜索