Redux学习手册

1,559 阅读10分钟

前言

我们在学习react技术栈中,不可避免的会去接触学习redux,然而redux确是一个难点,它不同于之前学习原生js或者是react.js那样,直接写业务逻辑就能解决问题。而是更多的体现在一种架构思想上,什么时候用redux,为什么要用redux,以及如何去用redux。本文将用案例加说明的方式引导学习。

·需求基础

react.js+es6 你需要有这两个基础,如果没有,可以学习redux用法和思想,因为下文案例将采用react来开发。

1.什么是redux?

redux是一个状态管理库,它提供一种可控管理状态的方法,它能够帮你管理代码中的那些看起来很糟糕的状态,状态只是一些变量,那么这些变量自然有无数种方法去改变,你可以将这些变量看做你的孩子,你可以想象一下当你的孩子过多,几十个?上百个?而何况这些孩子还有可能朝着各种歪瓜裂枣的方向变化着。。。

redux正是基于这种情况,给状态限制在了一个框架中,任何不“正确的方式”将无法去修改,并且所有允许修改的方法,都可见且便于管理。

2.什么时候用redux?

如果你觉得不用redux很累,我想你会主动用的。

3.redux详解

在使用 Redux 之前,你必须要谨记它的三大原则:单一数据源、state 是只读的和使用纯函数执行修改。

3.1单一数据源

整个应用的 state 都被储存在一棵树中,并且这棵状态树只存在于唯一一个 store 中。

这使得来自服务端的 state 可以轻易地注入到客户端中;并且,由于是单一的 state 树,代码调试、以及“撤销/重做”这类功能的实现也变得轻而易举。

3.2只读的 state

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

这就表示无论是用户操作或是请求数据都不能直接修改 state,相反它们只能通过触发 action 来变更当前应用状态。其次,action 就是普通对象,因此它们可以被日志打印、序列化、储存,以及用于调试或测试的后期回放。

3.3使用纯函数执行修改

函数在有相同的输入值时,产生相同的输出

函数中不包含任何会产生副作用的语句

在这里,reducer 要做到只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,只进行单纯执行计算。

3.4redux结构

请看我随手画的草图。

我们通过action发起修改state的请求,action通过自身的type属性找到reducer里对应的方法,reducer则经过一系列操作返回一个新的state对象,通过以旧换新将旧的state取代。

(1)state(状态/数据)

state就是我们定义的状态。 我们只需要了解两点

1 .state存在于store中,并且state与store是一对一的关系,即单一的数据源state存在唯一的store中。

2 .state只能通过发起action去修改。

(2)action(修改state的唯一方法)

//添加
export function addtodo(todo) {
  return{
      type:"ADDTODO",
      todo
  }
}

这是一个简单的进行添加的action

1.action是一个对象,用于描述修改state的一些行为。

2.内部的第一个属性type,用于指定进行何种操作,对应的操作存在于reducer中,它的作用类似于指路牌。

2.第二个属性,todo是一个参数,它可以传入任何值,类似于形参,我们将会把它传入到reducer中去使用,用于动态去修改state。

(3)reducer (执行action的地方)

reducer里集成了我们将要对state所做的各种操作,这里不要将它于action混淆,action只是指引了对state的操作,而reducer则是操作的具体实现。

export default function active(state="all",action) {
  switch (action.type){
      case "UPDATEACTIVE":
         return action.todo
      default:
          return state;
  }
}

这是一个最简单的reducer结构,里面甚至只有一个action方法。reducer中有两个参数,除此之外,我们还需要了解reducer的一些特性。

1.第一个参数,代表着我们我们所定义的状态,在这里我们将state引入,并不是用于我们将要对state进行操作,reducer中的纯函数的特性也不允许我们去对state进行可能修改的操作。我们引入state是为了获取state的值,以便于我们组成新的值。

2.第二个参数,action,这就是我们传入的action,我们获取action.type决定执行何种方法,获取action.todo(todo的命名不固定)决定将如何去修改state。

3.永远不要去试图在reducer中修改state,这是不可取,也是不规范的。(如果直接修改,那要redux干嘛?) 所有直接修改state,或者可能对state造成影响的操作都被禁止,(我们所做的仅仅是克隆一个跟state长得比较像的数据进行返回取代state)

4.reducer并不一定是对整个state结构进行操作,如果state的结构树比较深,我们可以将reducer拆分,分别对部分state进行操作,最后只需要使用Redux 提供的 combineReducers() 工具类进行合并即可

import {combineReducers} from "redux"
import todos from "./todos"
import active from "./active"

const reducer = combineReducers({
    todos,
    active
})
export default reducer;

(4)store

Store 用来存放整个应用的 state,并将 action 和 reducer 联系起来。它主要有以下几个职能:

存储整个应用的 state,它提供了以下三个api接口

1. getState() 方法获取 state

2.dispatch(action) 方法更新 state

3.subscribe(listener) 来注册、取消监听器

import {createStore,applyMiddleware} from "redux"
import reducer from "../reducers/App"

const state = {
   todos:[
       {content:"redux学习",flag:false},
   ],
   active:"all"
}
const store=createStore(reducer,state);
export default store;

4.具体案例(选看)

这是一个简单的案例,虽然简单,但内部具体逻辑功能有将近十几个,十分适合新人学习。

具体案例可以去todo官网查看---------> todo

我们新建一个基于react的项目 (使用 npm i create-react-app myapp 生成)

然后我们修改项目结构为如下。

具体的html,css代码,以及react的组件划分就不详解了,不是本文重点。

另外在react中使用redux需要安装这两个依赖库。redux和react-redux

1.编写state

const state = {
    todos:[
        {content:"redux学习",flag:false},
    ],
    active:"all"
}

可以看到,我们的state对象中有两条属性,第一个todos就是我们的内容数据,其中有两个属性,第一个content是集体任务内容,第二个flag是定义任务的完成状态的,false为未完成。

第二个暂时不用(用来定义显示何种内容的)

2.编写action

src/action/todo.js

//添加任务
export function addtodo(todo) {
    return{
        type:"ADDTODO",
        todo
    }
}
//删除任务
export function deletetodo(todo) {
    return{
        type:"DELETETODO",
        todo
    }
}
//更新内容
export function updatecontent(todo) {
    return{
        type:"UPDATECONTENT",
        todo
    }
}
export function clear(todo) {
    return{
        type:"CLEAR",
        todo
    }
}
export function updateflag(todo) {
    return{
        type:"UPDATEFLAG",
        todo
    }
}
export function updateallflag(todo) {
    return{
        type:"UPDATEALLFLAG",
        todo
    }
}
export function updateactive(todo) {
    return{
        type:"UPDATEACTIVE",
        todo
    }
}

前三个我加了注释的action是本次案例讲解的三个方法,由于我事先已经将其余功能写完,为了不报错,就先不删除了,只需要关注前三个功能即可。

2.编写reducer

export default function todos(state=[],action) {
    switch (action.type){
        //添加。
        case "ADDTODO":
            return [...state,{content:action.todo,flag:false}]
        //删除todo
        case "DELETETODO":
            var index=state.findIndex((item,index)=>index==action.todo)
            var arr=[]
            var j=0
            for (var i=0;i<state.length;i++){
                if (i!=index){
                    arr[j]=state[i]
                    j++
                }
                else {
                }
            }
            return arr
        //修改content
        case "UPDATECONTENT":
            var arr=[...state]
            var index=state.findIndex((item,index)=>index==action.todo.keyid)
            arr[index].content=action.todo.content
            return arr
        //统计未完成的任务(此任务不需要修改状态,所以不用写方法)
        case "CLEAR":
            var arr=[]
            var j=0
            for (var i=0;i<state.length;i++){
                if (state[i].flag==false){
                    arr[j]=state[i]
                    j++
                }
            }
            return arr
        //单选更新状态
        case "UPDATEFLAG":
            var index=state.findIndex((item,index)=>index==action.todo)
            var newflag=state[index].flag==true?false:true
            var arr=[...state]
            arr[index].flag=newflag
            return arr
        //全选更新
        case "UPDATEALLFLAG":
            var arr=[...state]
            for (var i=0;i<arr.length;i++){
               arr[i].flag=action.todo
            }
            return arr
       
        default:
            console.log('进错对象啦')
            return state;
    }
}

我将reducer进行了拆分这个代码是对todos内容的操作,todos是一个数组结构,所以我们只需要返回一个同样的数组结构即可。

2.1添加todos

我们已知在reducer中无法使用push等修改参数的方法。那我们如何在一个数字中添加数据呢?

这里我们就需要用到es6中的扩展运算符了 (在reducer中扩展运算符是老顾客)

既然[...state]=state 那么[...state,{content:action.todo,flag:false}]就相当于在state中添加了一条数据。我们直接返回即可。

我们来看看在addtodo组件中调用这个方法

 add(e){
        let content=e.target.value.trim();
        if(content){
            if(e.keyCode==13)
            {
                if(content){
                    this.props.add.addtodo(content)
                    e.target.value=""
                }
            }

        }
    }

可知当我们按下回车是,会将input中的内容作为参数调用此方法。当然,我这儿贴出的只是核心代码,集体组件的使用可以自己去操作。 我们来看看效果

好像还不错,你也可以继续添加,例如内容为空无法添加等,按esc放弃添加等。

2.1删除todos

删除基本逻辑是点击删除,调用删除的action,还要传入我们需要删除的内容,(注意传入的参数最好不要是具体的内容,因为同样的任务,数据很有可能是一样的,那么当我们删除,就有可能删错,最好传入要删除内容的数组下标)

deletetodo(){
      setTimeout(()=>this.props.comments.deletetodo(this.props.keyid),1000)
  }
  
deletetodo(){
      this.props.comments.deletetodo(this.props.keyid)
  }

这两个代码是没有区别的,使用定时器只是为了普及一个知识,我们在redux中如果不使用中间件Middleware来加强store的话,是无法使用异步操作的,但是我们在react中是可以使用异步操作的,我们在react中调用store中的同步操作,一样可以实现异步action操作。

还是直接来看看效果吧

2.1编辑todos

1.编辑功能第一个功能就是单机内容出现编辑框,并且编辑框内容为原来的内容,失去焦点编辑框消失,内容框显现。 2.修改功能,修改内容为失去焦点时编辑框的内容。

第一个功能既可以使用dom操作来完成,也可以使用新状态(此状态为todo组件私有),为了简单化,就采用dom来实现。 第二个功能则是调用更新内容的action

 case "UPDATECONTENT":
           var arr=[...state]
           var index=state.findIndex((item,index)=>index==action.todo.keyid)
           arr[index].content=action.todo.content
           return arr

我们来回顾之前写的更新内容的reduce, 可以看到,我们传入的action.todo,必须包含两个参数,一个是要修改的数据下标,第二个就是新的内容。

我们直接上todo的失去焦点代码

 onblur(e){
      if (e.target.value.trim()==""){
          this.props.comments.deletetodo(this.props.keyid)
      }
      else {
          this.props.comments.updatecontent({ content:e.target.value.trim(),keyid:this.props.keyid})
      }
      var label=document.getElementsByClassName("label")
      var edit=document.getElementsByClassName('edit')
      for(var i=0;i<label.length;i++){
          if (e.target==edit[i]){
              label[i].style.display="block"
          }
      }
      e.target.style.display="none"
  }

写了那么多,只有这一句是核心代码,其余都是逻辑处理this.props.comments.updatecontent({ content:e.target.value.trim(),keyid:this.props.keyid})

老规矩,看看效果。

到这三个功能都讲解完了

总结

本文,在前面讲解了redux的基本用法,后面则以一个案例的形式应用了redux。

另外,案例虽然使用了react-redux,我在文中却并没有讲解react-redux的用法,因为redux只是一种架构思想,它可以用在任何地方,并不局限于react。如果你刚好学习react,那么你有必要去学习 redux和react-redux。如果是vue,那么你得去学vuex了。