攻坚redux

338 阅读12分钟

前言

redux,相信前端的小伙伴们对此都不陌生吧。本文将以通俗易懂的语言攻坚这个框架,一方面希望帮助大家更方便的掌握这项技术,同时也希望我能够对个框架有一个更深的理解,废话就不多说,开干。

redux

众所周知,react是一个视图层的UI框架,对此,官网的描述为:React – A JavaScript library for building user interfaces。在一些小型的项目中,react框架可以通过自身的state和props属性完成数据交互。但是在一些企业级的大型web项目中,各组件间的通信是很复杂的,因为react本身是单向数据传递的,所以仅仅依靠自身的属性来实现数据状态管理,就显得心有余而力不足了。专业的事情需要专业的人来办。redux,就是一个专业的数据层框架,使用它可以更高效的完成组件间的通信,从而更好的管理数据状态。下面就一起走进redux的世界,让它告诉我们神马叫做专业。

redux核心概念

redux有三个核心概念。store,reducer,action,下面一一解释。何为store呢,谷歌翻译解释为商店。

好吧,那么我们顺水乘舟,就以顾客在商店买东西为例子,来给大家讲解redux的执行流程。商品中有着各种各样的商品,而这些商品就对应中react项目中的数据,这些数据都集中在这个store商店中。那么我们如何在store店铺中购买商品呢,这个时候就需要派发action,简单翻译就是行动的意思,我们把这个行动类比顾客的行为,顾客可以有很多行为,比如购物或者退货,这都是可以的。顾客购买完商品,就需要收营员结账啦。那么收营员又是谁呢,相信大家都可以猜到了,就是最后的reducer。reducer会根据actio购买商品行为,进行结算,或者是顾客退货行为,给顾客进行退款。一次完整的redux购物就是这个样子,可能大家听完仍然一头雾水,没事,下面我们就用create-react-app创建一个项目,一步步来详细解释。新建完项目后,删除脚手架中一些多余的代码,基本目录如下。

├── node_modules
├── package.json
├── public
├── src
│   ├── App.css
│   ├── App.jsx
│   ├── index.css
│   └── index.js
└── yarn.lock

demo演示

首先,需要安装redux。

yarn add redux

下面就正式进入demo的环节。既然是去商店购物,所以第一步肯定是把这个商店建立,因此我们需要创建store。在src目录下创建store文件夹,并且新建index.js文件。

<!--文件目录:src/store/index.js-->
import { createStore } from "redux";
const store = createStore();
export default store;

这部分代码应该还是相对简单明了的,从redux库里面引入creatStore,然后就可以通过这个方法帮我们创建store。商店创建完毕后,第二个步骤就是将商品上架,这里的上架是指在视图页面上获取store中的数据,可以通过store的getState()方法来实现,来看下具体的代码。

<!--文件目录:src/App.jsx-->
constructor(props) {
    super(props);
    this.state = {
      value: "",
      goodsList: store.getState().goodsList
    };
  }

这样,就可以将store中的goodsList赋值给App组件的state,进而进行页面的渲染。好了,完成了上述的步骤,接下来是不是可以营业了呢?NO,当然不是,我们还需要营业员呢。好的,那么接下来就去雇佣营业员。同样的,在src目录下创建reducer文件夹,再新建index.js文件。下面是一个最简洁的reducer:

<!--文件目录:src/reducer/index.js-->
export default (state,action)=>{
    return state;
}

reducer是一个纯函数,接收两个参数,state和action。redux是符合函数式编程的,使用了纯函数。简单说下纯函数,就是相同的输入,必然得到相同的输出,没有任何副作用的影响。举个🌰,在js数组方法中,有两个方法,slcie和splice,第一个就是纯函数,第二是就是不纯的。来来来,解释一波。

let group = [1,2,3,4,5,6,7];
group.slice(1,3);  
group.slcie(1,3);  

大家可以自己测试一下这段代码,可以发现,两次调用slcie方法的输入都为(1,3),结果输出都是[2,3],满足纯函数的定义。

let group = [1,2,3,4,5,6,7];
group.splice(1,3);
group.splice(1,3);

换成splice方法后,两次输入都为(1,3),但是第一次输出为[2,3,4],第二次输出为[5,6,7],不满足纯函数的定义。使用纯函数可以避免副作用带来的影响,大家以后可以多多尝试纯函数。好了,话题切回来,回到我们的reducer上,一般情况下,我们需要给reduce的第一个参数state赋一个初始值。

const defaultState = {
    goodsList:[]
}
export default (state,action)=>{
    return state;
}

这样,reducer就创建好了。下面就可以去商店报道啦。在store中引入reducer,并将reducer作为createStore的参数。

文件目录:src/store/index.js
import { createStore } from "redux";
import reducer from "../reducer"
const store = createStore(reducer);
export default store;

营业员也雇佣好了,下面就欢迎大家光临购物啦。我们先来看下最终demo的效果。主要的功能为,点击购买,将商品添加进列表以及点击取消购买,将商品从列表中移除。

下面我们就一一来实现。这些功能都属于顾客行为,大家应该都可以猜到,这就是上面介绍的action。action的数据结构为js对象,里面有一个必选属性,type。store会把action派发给redcuer,reducer会根据action的type属性,进行不同的操作,然后将最新的数据状态返给store。对应我们购物的例子,顾客触发一个购物action,此时type属性为购买,并且在这个action对象中,我们还需要添加另一个goodsName属性,用来记录商品的名称,这样营业员就可以把这个商品成功添加进我们的购物清单啦。取消购买也是类似的流程,type的值为取消购买,同样需要传递商品的名称, 然后营业员才可以把指定的商品从列表中移除。下面来看下具体代码,

<!--文件目录:src/App.jsx-->
import React, { Component } from "react";
import GoodsItem from "./goodsItem";
import store from "./store";
import { ADD_GOODS } from "./constant";

import "./App.css";
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: "",
      goodsList: store.getState().goodsList
    };
  }

  /**
   * 输入商品的名称
   */
  handleChange = e => {
    this.setState({
      value: e.target.value
    });
  };

  /**
   * @description: 购买商品
   * @param {string} 购买的商品名称
   */
  addGoods = () => {
    this.setState({
      value: ""
    });
    store.dispatch({
      type: ADD_GOODS,
      value: this.state.value
    });
  };

  render() {
    const { goodsList = [], value } = this.state;
    return (
      <div className="App">
        <h3>购物清单</h3>
        <div className="header_wrapper">
          <input
            value={value}
            placeholder="请输入商品名称"
            onChange={this.handleChange}
          />
          <button
            onClick={() => {
              this.addGoods();
            }}
          >
            购买
          </button>
        </div>
        <div>
          {goodsList.length > 0
            ? goodsList.map((goodsItem, index) => {
                return (
                  <GoodsItem
                    key={index}
                    goodsName={goodsItem.goodsName}
                    store={store}
                  />
                );
              })
            : null}
        </div>
      </div>
    );
  }
}

export default App;

点击购买按钮后,通过addGoods方法触发一个action。然后需要通过store的dispatch方法将这个action传递给reducer。

store.dispatch({ type: ADD_GOODS, value: this.state.value });

reducer接收到参数,将商品加入进购物列表。

import { ADD_GOODS, DELETE_GOODS } from "../constant";
const defaultState = {
  goodsList: []
};
export default (state = defaultState, action) => {
  switch (action.type) {
    case ADD_GOODS:
      return {
        ...state,
        goodsList: [
          ...state.goodsList,
          {
            goodsName: action.value
          }
        ]
      };
    case DELETE_GOODS:
      return {
        ...state,
        goodsList: state.goodsList.filter((item, index) => {
          return item.goodsName !== action.value;
        })
      };
    default:
      return state;
  }
};

reducer根据action的type属性,做出不同的处理。虽说是处理,但是不能直接去改变state的状态,因为state是不可变的,具有immutable属性,state的具体特性大家可以参考redux官网,这里就不展开叙述啦。我们可以通过解构的方式创建一个新的state,并对这个state进行相应处理,再将最新的状态返回给store。完成上述操作后,页面视图是没有发生变化的。store的状态并没有传递到页面,而需要通过store的subscribe()方法订阅数据状态的变化。只要这样,页面视图才会随着store中数据的变化而同步更新,。

<!--文件目录:src/App.js-->
<!--在App组件的constructor方法中,添加store.subscribe()方法-->
store.subscribe(() => {
      this.setState({
        goodsList: store.getState().goodsList
      });
    });

走到这一步,一个简单的购物demo就这样实现了。可能逻辑可能还是有点混乱,下面再将主要的知识点梳理一下。主要涉及三大概念,store、action、reducer。redux提供createStore()方法创建store。store用到了两个核心的api,getState()和dispatch。action是一个js对象,并且有一个type必选属性。reducer是一个纯函数,两个参数分别为state和action。执行流程为下图所示,是一个完成的闭环。

react-redux

看完了redux的执行流程,大部分人可能都有一个感受,复杂,借助汤家凤老师的一句话来说就是一点都不清爽。还好,业界大佬早就针对react这个特定的框架,对redux进行进一步升级,react-redux随着产生了,这样一切都会变得井然有序。 首先,我们需要在项目中安装这个神奇的库。

yarn add react-redux

Provider

react-redux提供了一个Provider组件,只有被Provider组件包裹的组件才可以访问到store中的数据。

<!--文件目录:src/index.js-->
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App.jsx";
import "./index.css";
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

根组件App被Provier组件包裹,这样项目中的所有组件都获取到store中的数据。

connect

connect是联系的意思。没错,这个方法的作用就是将store和当前的组件相关联。 参考react-redux官网,

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

connect方法有4个可选参数,一般项目中只会用到前两个参数,并且这两个参数都为函数,具体我们根据代码来解释。

<!--文件目录:src/App.jsx-->
const mapStateToProps = state => {
  return {
    goodsList: state.goodsList
  };
};

const mapDispatchToProps = dispatch => ({
  addGoods: goodsName => dispatch({ type: ADD_GOODS, value: goodsName })
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

上述代码中,connect方法的第一个参数mapStateToProps,该函数接收一个state参数,从state中获取store中的数据。然后在App组件中,就可以通过this.props.goodsList渲染购物列表。mapDispatchToProps方法的接收dispatch作为第一个参数,这个dispatch即为redux中的store.dispatch()方法,用来派发action。 使用react-reudx的Provider组件和connect方法,我们可以比较方便的去获取store中的数据,从而不再需要使用store.dispatch()去订阅store数据的变化。一旦store中的数据发生变化,页面视图也会随之改变。下面看下App.jsx的完整代码。

import React, { Component } from "react";
import { connect } from "react-redux";
import GoodsItem from "./goodsItem";
import store from "./store";
import { ADD_GOODS } from "./constant";

import "./App.css";
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: ""
    };
  }

  /**
   * 输入商品的名称
   */
  handleChange = e => {
    this.setState({
      value: e.target.value
    });
  };

  /**
   * @description: 购买商品
   * @param {string} 购买的商品名称
   */
  addGoods = () => {
    this.props.addGoods(this.state.value);
    this.setState({
      value: ""
    });
  };

  render() {
    const { value } = this.state;
    const goodsList = this.props.goodsList;
    return (
      <div className="App">
        <h3>购物清单</h3>
        <div className="header_wrapper">
          <input
            value={value}
            placeholder="请输入商品名称"
            onChange={this.handleChange}
          />
          <button
            onClick={() => {
              this.addGoods();
            }}
          >
            购买
          </button>
        </div>
        <div>
          {goodsList.length > 0
            ? goodsList.map((goodsItem, index) => {
                return (
                  <GoodsItem
                    key={index}
                    goodsName={goodsItem.goodsName}
                    store={store}
                  />
                );
              })
            : null}
        </div>
      </div>
    );
  }
}
const mapStateToProps = state => {
  return {
    goodsList: state.goodsList
  };
};

const mapDispatchToProps = dispatch => ({
  addGoods: goodsName => dispatch({ type: ADD_GOODS, value: goodsName })
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

goodsItem完整代码

import React, { Component } from "react";
import { connect } from "react-redux";
import { DELETE_GOODS } from "./constant";

class GoodsItem extends Component {
  cancel = () => {
    this.props.cancel(this.props.goodsName);
  };
  render() {
    const { goodsName } = this.props;
    return (
      <div className="contentItem">
        <div>{goodsName}</div>
        <div onClick={this.cancel}>取消购买</div>
      </div>
    );
  }
}

const mapStateToProps = () => {
  return {};
};
const mapDispatchToProps = dispatch => ({
  addGoods: goodsName => dispatch({ type: DELETE_GOODS, value: goodsName })
});
export default connect(mapStateToProps, mapDispatchToProps)(GoodsItem);

Redux DevTools

为了更好的观察redux的数据状态,我们可以使用chrome的一款插件,Redux DevTools 。要正常使用这款插件,我们还需要稍微修改下我们的项目代码。更多细节可以参考官网文档

<!--文件目录:src/store/index.js-->
import { createStore } from "redux";
import reducer from "../reducer";
const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;

给createStore增加一个参数,这个参数的也比较好理解,如果检测到当前当前浏览器有安装Redux DevTools,就启用该插件,否则按正常代码执行。

window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()

重新启动我们的项目。打开开发者工具,我们可以发现多了一个redux。

我们分别将牛奶和草莓添加进商品列表,演示下该插件的效果。

可以看到,左侧的inspector监测栏,监测到了两次action,并且这两次action的相关信息也在右侧栏展示出来。右侧栏选择State,也可以查看当前项目中store的状态。
store中有一个goodsList数组,并且里面有两条数据,就是我们刚加入的牛奶和草莓。 当然啦,这个插件的功能远不如此,大家可以阅读文档捣鼓一下。在项目调试过程中,这个插件的用处还是很大的。

结语

redux的知识点还是比较复杂的,而且是符合函数式编程的,如果不经常使用,有些概念难免会有些遗忘。写文章的时候,其实是花了很多的时间,因为我也在不停的理思路,想以一种最通俗易懂的语言把redux讲明白,让没怎么使用过redux的前端人能快速上手这个框架。因为我自己的项目中其实一直是在用mobx,所以,撰写本文,也让我对redux有了更多的认识。文章中难免有一些不严谨或者错误的地方,希望大家可以积极指出告诉我。有关redux其实还有很多可以说,我也会积极整理总结,在以后的文章中和大家分享,加油!

参考