Redux(一):基本概念

2020-06-01 14:29:35 浏览数 (1)

背景

React是一个单向数据流的view层框架,单向数据流、组件化、生命周期是其特点。在React组件关系中,组件状态由自己管理,父子组件通过props传递;兄弟组件那么就需要一个共同的父组件作中转;如果涉及层级比较深的话一层一层传递会非常麻烦。所以大量状态共享是React单独难以解决的问题。

随着单页面应用的日益复杂,JavaScript需要维护更多的状态,这些状态除了包含服务端返回的数据还有:缓冲数据、未同步到服务端的持久化数据、UI状态等。如果能将这些状态从单个组件剥离出来统一管理,将会更好的维护、拓展Web应用。

Redux就是JavaScript应用这样一个可预测化的状态管理容器。Redux本身和React其实并没有任何关系,只是二者共性的函数式编程配合起来会比较方便,当然实际React项目中还要用到react-redux做桥接。

三大原则

一、单一数据源

应用的state保存在一个JavaScript对象树中,并且这个对象树只能存在于唯一的一个store中。

代码语言:javascript复制
import {createStore } from 'redux';
const store = createStore(reducer);

二、state是只读的

唯一改变state的方法就是触发action,action是一个描述state如何改变的普通对象,必须包含type属性。

代码语言:javascript复制
store.dispatch({
    type: 'COMPLETE_TODO',
    index: 1
});
store.dispatch({
    type: 'SET_VISIBILITY_FILTER',
    filter: 'SHOW_COMPLETED'
});

三、使用纯函数来执行修改

dispatch一个action以后,如何根据这个普通对象来修改state树,那么就需要编写对应的函数,这个函数称之为reducers。reducers必须是纯函数,所谓纯函数可以简单理解为:只要输入相同那么输出就相同,同样的输入只会输出同一个结果。

随着应用规模的增长,所要维护的state树会变的很大,这样就需要把reducers拆分成多个reducer,每个reducer来维护状态树的一部分。

代码语言:javascript复制
function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }}
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true
          })
        }
        return todo
      })
    default:
      return state
  }
}

快速上手

0、开始之前

这里以react来演示,当然还是得记住那句话,”redux和react没有任何关系”。

现在设计这么一个状态树:

代码语言:javascript复制
{
    visibilityFilter:"SHOW_ALL",
    todos:[{
        text:"consider use redux",
        completed:true
    },{
        text:"keep all state in a single tree",
        completed:true
    }]
}

这个对象包含2个属性:visibilityFilter、todos。visibilityFilter表示过滤类型,值是一个字符串;todos表示待办事项,值是一个数组。

可以为todos新增或删除项目,也可以改变某个项目的完成情况——completed。

1、安装redux、react、react-dom

代码语言:javascript复制
npm install redux react react-dom --save

示例对应版本: – reudx:4.0.1 – react:16.6.3 – react-dom:16.6.3

2、编写一个reducer

创建一个reducers.js,编写以下代码:

代码语言:javascript复制
const initState = {
    visibilityFilter:"SHOW_ALL",
    todos:[]
};
function reducers(state=initState,action){
    return state;
}
 
export {reducers}

reducer接收2个参数:state、action。现在函数内部什么都没有做,仅仅是返回state,后续再增加相关逻辑判断。

3、创建一个store

在redux中应该只有一个store,单一数据源,这一点很重要。redux向外暴露了一个createStore方法用来创建store。

所以,创建一个store.js,编写以下代码:

代码语言:javascript复制
import {createStore} from "redux";
import {reducers} from "./reducers";
 
const store = createStore(reducers);
 
export default store;

4、创建一个react组件

现在实现这么个功能:一个input框用来输入待办事项,点击提交按钮将数据加到todos中,初始状态completed为false,点击完成将对应的这一条改为true。同时增加一个下拉框select,用来筛选todos。

创建一个app.js,编写以下代码:

代码语言:javascript复制
import React from "react";
import ReactDOM from "react-dom";
 
class App extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            filterList:[
                {label:"全部",value:"SHOW_ALL"},
                {label:"已完成",value:"SHOW_COMPLETED"},
                {label:"未完成",value:"SHOW_UNCOMPLETED"},
            ],
            visibilityFilter:"SHOW_ALL",
            todoValue:"",
            todos:[]
        }
    }
    render(){
        const {filterList,visibilityFilter,todoValue,todos} = this.state;
        return(
            <div>
                <div>
                    <label>过滤类型:</label>
                    <select value={visibilityFilter} onChange={this.filterChange}>
                        {
                            filterList.map(item=>(
                                <option key={item.value} value={item.value}>{item.label}</option>
                            ))
                        }
                    </select>
                </div>
                <div>
                    <input placeholder={"请输入待办事项"} value={todoValue} onChange={this.todoChange} />
                    <button onClick={this.submitTodo} type={"button"}>提交</button>
                </div>
                <div>
                    <table border={"border"}>
                        <thead>
                            <tr>
                                <th>事项</th>
                                <th>是否完成</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody>
                            {
                                this.getFilterTodos(todos,visibilityFilter).map((item,index)=>(
                                    <tr key={index}>
                                        <td>{item.text}</td>
                                        <td>{item.completed.toString()}</td>
                                        <td>
                                            {
                                                !item.completed &&
                                                <button onClick={()=>{this.completeTodo(index)}} type={"button"}>完成</button>
                                            }
                                        </td>
                                    </tr>
                                ))
                            }
                        </tbody>
                    </table>
                </div>
            </div>
         )
    }
    //监听过滤条件
    filterChange=(e)=>{
        const visibilityFilter= e.target.value;
        this.setState({
           visibilityFilter
        });
    };
    //监听input
    todoChange=(e)=>{
        const todoValue = e.target.value;
        this.setState({
            todoValue
        });
    };
    //提交事项
    submitTodo=()=>{
        const {todoValue,todos} = this.state;
        this.setState({
            todos:[].concat(todos).concat({
                text:todoValue,
                completed:false
            }),
            todoValue:""
        });
    };
    //完成事项
    completeTodo=(i)=>{
        const {todos} = this.state;
        const newTodos = todos.map((item,index)=>{
            if(index !== i){
                return item;
            }
            return Object.assign({},item,{completed:true})
        });
        this.setState({todos:newTodos});
    };
    //获取筛选后的todos
    getFilterTodos=(todos,visibilityFilter)=>{
        switch (visibilityFilter) {
            case "SHOW_ALL":
                return todos;
            case "SHOW_COMPLETED":
                return todos.filter(item=>item.completed);
            case "SHOW_UNCOMPLETED":
                return todos.filter(item=>!item.completed);
            default:
                return todos;
        }
    };
}
 
ReactDOM.render(<App/>,document.querySelector("#root"));

这个组件是纯state维护状态的版本,现在将todos和visibilityFilter拆分到store中:

代码语言:javascript复制
import React from "react";
import ReactDOM from "react-dom";
import store from "./store/store";
 
class App extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      filterList:[
        {label:"全部",value:"SHOW_ALL"},
        {label:"已完成",value:"SHOW_COMPLETED"},
        {label:"未完成",value:"SHOW_UNCOMPLETED"},
      ],
      visibilityFilter:store.getState().visibilityFilter,
      todoValue:"",
      todos:store.getState().todos
    }
  }
  render(){
    const {filterList,visibilityFilter,todoValue,todos} = this.state;
    return(
      <div>
        <div>
          <label>过滤类型:</label>
          <select value={visibilityFilter} onChange={this.filterChange}>
            {
              filterList.map(item=>(
                <option key={item.value} value={item.value}>{item.label}</option>
              ))
            }
          </select>
        </div>
        <div>
          <input placeholder={"请输入待办事项"} value={todoValue} onChange={this.todoChange} />
          <button onClick={this.submitTodo} type={"button"}>提交</button>
        </div>
        <div>
          <table border={"border"}>
            <thead>
            <tr>
              <th>事项</th>
              <th>是否完成</th>
              <th>操作</th>
            </tr>
            </thead>
            <tbody>
            {
              this.getFilterTodos(todos,visibilityFilter).map((item,index)=>(
                <tr key={index}>
                  <td>{item.text}</td>
                  <td>{item.completed.toString()}</td>
                  <td>
                    {
                      !item.completed &&
                      <button onClick={()=>{this.completeTodo(index)}} type={"button"}>完成</button>
                    }
                  </td>
                </tr>
              ))
            }
            </tbody>
          </table>
        </div>
      </div>
    )
  }
  componentDidMount(){
    store.subscribe(()=>{
      this.setState({
        visibilityFilter:store.getState().visibilityFilter,
        todos:store.getState().todos
      });
    });
  }
  //监听过滤条件
  filterChange=(e)=>{
    store.dispatch({
      type:"VISIBILITY_FILTER_SET",
      value:e.target.value
    });
  };
  //监听input
  todoChange=(e)=>{
    const todoValue = e.target.value;
    this.setState({
      todoValue
    });
  };
  //提交事项
  submitTodo=()=>{
    const {todoValue} = this.state;
    store.dispatch({
      type:"TODOS_ADD",
      todo:{
        text:todoValue,
        completed:false
      }
    });
    this.setState({
      todoValue:""
    });
  };
  //完成事项
  completeTodo=(index)=>{
    store.dispatch({
      type:"TODOS_COMPLETED",
      todo:{
        index,
        completed:true
      }
    });
  };
  //获取筛选后的todos
  getFilterTodos=(todos,visibilityFilter)=>{
    switch (visibilityFilter) {
      case "SHOW_ALL":
        return todos;
      case "SHOW_COMPLETED":
        return todos.filter(item=>item.completed);
      case "SHOW_UNCOMPLETED":
        return todos.filter(item=>!item.completed);
      default:
        return todos;
    }
  };
}
 
ReactDOM.render(<App/>,document.querySelector("#root"));

store的dispatch()方法用来派发一个action,action是一个普通对象,必须包含type属性,这个属性用来标识执行对应的reducer。

store.subscribe()方法用来监听store里state的变化,所以我们在subscribe的回调里重新获取store的state,以此来更新我们组件的state。

这里共三种action,分别为:VISIBILITY_FILTER_SET(设置过滤类型)、TODOS_ADD(新增事项)、TODOS_COMPLETED(完成事项)。所以我们的reducer需要对这三种情况做判断。

5、修改reducer

代码语言:javascript复制
const initState = {
  visibilityFilter:"SHOW_ALL",
  todos:[]
};
function reducers(state=initState,action){
  switch (action.type){
    case "VISIBILITY_FILTER_SET":
      return Object.assign({},state,{visibilityFilter:action.value});
    case "TODOS_ADD":
      return Object.assign({},state,{todos:state.todos.concat(action.todo)});
    case "TODOS_COMPLETED":
      return Object.assign(
        {},
        state,
        {
          todos:state.todos.map((item,index)=>{
            if(index !== action.todo.index){
              return item;
            }
            return Object.assign({},item,{completed:action.todo.completed})
          })
        }
      );
    default:
      return state;
  }
}
export {reducers}

这里使用switch语句,根据不同的action.type执行不同的操作,返回的都是修改后的state树。

例子中,无论是对象还是数组,并没有直接去修改属性会增加元素,返回的都是一个新的对象或数组,这一点很重要,因为在js中对象是按地址引用的,直接修改属性或push一个元素,引用地址并没有发生变化,这会导致出现一些难以控制的情况。所以,在redux中不应该使用如:push、pop、slice等方法。对于数组可以用concat、拓展运算符、map等;对于对象可以用Object.assign()、拓展运算符等。

总结:

可以看到Redux使用的是派发/监听的设计模式,每次派发action,reducer运算结束后会执行在subscribe注册的回调函数。试想一个问题,如果我的组件之前注册了一个subscribe,然后组件销毁了,当组件又重新渲染的时候便会再次注册subscribe,那么这时派发一个action后,会怎么样?

事实证明,会执行2次,但由于第一次的组件销毁了,所以在一个已经销毁的组件上执行setState()方法必然是不合理的,此时react会抛出一个警告:

Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

意思就是:不能在一个已经卸载的组件上执行更新state的操作,这会导致内存泄漏, 应该在componentWillUnmount生命周期中取消所有订阅和异步任务。

redux本身并没有取消订阅的方法,所以实际react redux项目中,还要用到桥接二者的工具——react-redux。

0 人点赞