前端笔记之React(五)Redux深入浅出

一、Redux整体感知

ReduxJavaScript状态管理容器,提供了可被预测状态的状态管理容器。来自于Flux思想,Facebook基于Flux思想,在2015年推出Redux库。

中文网站:http://www.redux.org.cn/

官方githttps://github.com/reduxjs/redux

 

首先要引redux.js包,这个包提供了Redux对象,这个对象可以调用Redux.createStore()方法。


    

创建一个叫做reducer的函数,reducer中“维持”一个量,叫state,初始值是{"v" : 0}

 

这个函数你必须知道两点:

1)它是一个纯函数,在函数内部不改变{"v" : 0}的,不改变state对象的,只是返回了新的state

const reducer = (state = {"v" : 0} , action)=>{
    if(action.type == "ADD"){
        return {"v" : state.v + 1};
    }else if(action.type == "MINUS"){
        return {"v" : state.v - 1};
    }
    return state;
}

 

2)这个函数提供了可被预测的功能。

reducer中的state,就像被关进了保险箱。任何对这个state的变化,只能是通过dispatch一个action来改变的。换句话说,只有dispatch一个action才能改变这个保险箱中的数据

reducer函数中,用if语句来表示对state可能发生变化的罗列,只有罗列在楼层里面的改变,才会发生:

if(action.type==""){
    return 新的state
}else if(action.type ==""){
    return 新的state
}else if(action.type ==""){
    return 新的state
}
示例代码

创建store仓库,这个仓库创建时需要提供reducer,所以可以认为store就是reducerreducer就是store

const store = Redux.createStore(reducer);

reducer是一个纯函数,而store提供了三个方法:

 store.subscribe() 注册到视图

 store.getState() 得到数据

 store.dispatch() 发送action

 

学习reduxreact结合,到时候就不用注册到视图。

然后创建监听:

document.getElementById("btn1").onclick = function(){
     store.dispatch({"type":"ADD"})
}
document.getElementById("btn2").onclick = function(){
     store.dispatch({"type":"MINUS"})
}
document.getElementById("btn3").onclick = function(){
     store.dispatch({"type":"CHENG"})
}
示例代码

点击按钮,storedispatch出一个action。所谓的action就是一个JSON,这个JSON必须有type属性,值为大写字母。这个action没有任何意义,比如{"type":"ADD"},但reducer认识这个action,可以产生变化!

 

案例2,添加载荷:


    

唯一可以改变state的方式dispatch一个action

 

案例3:添加数组


    

 

案例4,加深练习:


    


二、ReduxReact进行结合开发

React开发的时候使用Redux可预测状态容器,要装两个新的依赖:

 redux:提供createStorecombineReducersbindActionCreators等功能

 react-redux:只提供两个东西,组件、connect()函数。

 

安装两个依赖:

npm install --save redux react-redux

 

创建reducers文件夹,创建index.js,这个文件暴露一个纯函数:

reducers/index.js

export default (state = { v : 10}, action) => {
    return state;
}

 

main.js入口文件:

import React from "react";
import ReactDOM from "react-dom";
import {createStore} from "redux";
import App from "./App.js";
import reducer from "./reducers";  //reducer函数

//创建 Redux store 来存放应用的状态。
const store = createStore(reducer);

ReactDOM.render(
    ,
    document.getElementById("app")
);

有两个问题:

如何让组件能够访问到store中的数据?

如果让store中的数据改变的时候,能够自动更新组件的视图

 

react-redux解决了这两个问题。

main.js中创建组件,它提供的是一个顶层容器的作用,实现store的上下文传递,是让store能够“注入”到所有组件

react-redux提供Provider组件,可以让容器组件拿到state

Provider在根组件外面包一层,这样一来,App所有的子组件就默认都拿到了state了。

原理是React组件的context属性,就是将store这个对象放到上下文(context)中

import React from "react";
import ReactDOM from "react-dom";
import {createStore} from "redux";
import {Provider} from "react-redux";
import App from "./App.js";
import reducer from "./reducers";

//创建store仓库
const store = createStore(reducer);

ReactDOM.render(
    
        
    ,
    document.getElementById('app')
);

组件中要获得全局store的值,需要用connect函数,connect提供连接React组件与Redux store的作用。

connect([mapStateToProps], [mapDispatchToProps])

connect()的第一个参数:mapStateToProps这个函数的第一个参数就是Reduxstore,会自动将store的数据作为props绑定到组件上。

connect()的第二个参数:mapDispatchToProps它的功能是,将action作为props绑定到组件上

 

通俗理解,使用connect可以把statedispatch绑定到React组件,使得组件可以访问到redux的数据。

常看到下面这种写法:

export default connect()(App)

App.js

import React from "react";
import {connect} from "react-redux";
class App extends React.Component {
    constructor() {
        super();
     
    }
    render(){
        return 

{this.props.v}

} } export default connect( //这个函数return的对象的值,将自动成为组件的props。 (state) => { return { v : state.v } } )(App);

一旦connect()(App); 连接某个组件,此时这个组件就会:当全局store发送改变时,如同自己的props发生改变一样,从而进行视图更新。

 

App.js写两个按钮,可以加1,减1

import React from "react";
import {connect} from "react-redux";
class App extends React.Component {
    constructor() {
        super();
    }
    render(){
        return 

{this.props.v}

} } export default connect( (state) => { return { v : state.v } }, (dispatch) => { return { add(){ dispatch({"type" : "ADD"}); }, minus(){ dispatch({"type" : "MINUS"}); } } } )(App);

reducers/index.js提供可预测状态

export default (state = {"v" : 10} , action) => {
    if(action.type == "ADD"){
        return { "v" : state.v + 1 };
    }else if(action.type == "MINUS"){
        return { "v" : state.v - 1 };
    }
    return state;
}

 

学生管理系统小案例:

reducers/index.js

const initObj = {
    "arr" : [
        {"id" : 1, "name" : "小明" , "age" : 12},
        {"id" : 2, "name" : "小红" , "age" : 13},
        {"id" : 3, "name" : "小刚" , "age" : 14}  
    ]
}
export default (state = initObj, {type, age, name, id})=>{
    if(type == "ADD_STUDENT"){
        return {
            ...state , 
            "arr" : [
                ...state.arr ,
                {
                    "id" : state.arr.reduce((a,b)=>{
                        return b.id > a ? b.id : a;
                    },0) + 1,
                    name, 
                    age
                }
            ]
        }
    }else if(type == "DEL_STUDENT"){
        return {
            ...state,
            "arr" : state.arr.filter(item=>item.id != id)
        }
    }
    return state;
}

 

App.js

import React from "react";
import {connect} from "react-redux";
class App extends React.Component {
    constructor() {
        super();
    }
    //增加学生
    add(){
        var name = this.refs.name.value;
        var age = this.refs.age.value;
        this.props.addStudent(name, age)
    }
    render(){
        return 

ref="name"/>

ref="age"/>

{ this.props.arr.map((item,index)=>{ return

key={item.id}> {item.id} {item.name} {item.age}

}) }
} } export default connect( //(state) => { // return { arr : state.arr } //}, //简化写法 ({arr})=>{ return { arr } }, (dispatch)=>{ return { addStudent(name, age){ dispatch({"type" : "ADD_STUDENT", name, age}) }, delStudent(id){ dispatch({"type" : "DEL_STUDENT" , id}) } } } )(App);

原理解析

首先connect之所以会成功,是因为Provider组件:

在原应用组件上包裹一层,使原来整个应用成为Provider的子组件

接收Reduxstore作为props,通过context对象传递给子孙组件上的connect

 

connect做了些什么?

它真正连接 Redux React,它包在我们的容器组件的外一层,它接收Provider提供的 store 里面的state dispatch,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件。

 

总结:

connect()(App),第一个()中接受两个参数,分别是:mapStateToPropsmapDispatchToProps

这两个参数都是函数,第一个参数函数return的对象的键名将自动和props进行绑定,第二个参数函数return的对象的键名,也将和props进行绑定。

第一个参数return的对象,是从state中获得值

第二个参数return的对象,是要改变state的值

 

如果有兴趣,可以看一下connect函数的API文档:

https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options

不管应用程序有多大,store只有一个,它就像天神一样“照耀”所有组件,但默认情况下所有组件是不能得到store的数据的,哪个组件要拿数据,就要connect一下,另外App最大组件确实包裹着所有组件,但不代表App组件连接了就代表其他组件也连接了。

 

前端笔记之React(五)Redux深入浅出_第1张图片


三、Redux编程-TodoList

http://www.todolist.cn/

reducers/index.js中根据项目情况创建reducer

const initObj = {
    "todos": [
       {"id" : 1, "title" : "吃饭",  "done" : false},
       {"id" : 2, "title" : "睡觉",  "done" : false},
       {"id" : 3, "title" : "打豆豆","done" : false}
    ]
}

export default (state = initObj, action) => {
    return state;
}

 

分别创建TodoHd.jsTodoBd.jsTodoFt.js三个组件:

import React from "react";
export default class TodoHd extends React.Component {
    constructor() {
        super();
    }

    render() {
        return 

我是TodoHd组件

} }
示例代码

 

 

 

 

App.js引入组件

import React from "react";
import {connect} from "react-redux";

import TodoHd from "./components/TodoHd.js";
import TodoBd from "./components/TodoBd.js";
import TodoFt from "./components/TodoFt.js";

class App extends React.Component {
    constructor() {
        super();
    }
    
    render() {
        return 
} } export default connect()(App)  
import React from 'react';
import {connect} from "react-redux";
import TodoItem from "./TodoItem.js";
class TodoBd extends React.Component {
    constructor(props){
        super(props);
    }
    render() {
        return (
            
//{JSON.stringify(this.props.todos)} { this.props.todos.map(item=>{ //return
{item.title}
return }) }
); } }   //connect目的是问“天”要数据,要通天。 //“通天”是比喻,就是说要问store要数据 export default connect( (state)=>{ return { todos : state.todos } } )(TodoBd);

 

TodoItem.js

import React from 'react';
export default class TodoItem extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            
className="todoItem"> checked={this.props.item.done}/> {this.props.item.title}
); } }

 

TodoHd.js增加待办事项

import React from 'react';
import {connect} from "react-redux";
class TodoHd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            
); } } //这个组件要通天的目的不是要数据,而是改变数据 export default connect( null , (dispatch)=>{ return { addTodo(title){ dispatch({"type":"ADDTODO", title}) } } } )(TodoHd);

 

reducers/index.js写可预测状态  

const initObj = {
    "todos" : [
        ...
    ],
}
export default (state = initObj, action)=>{
    if(action.type == "ADDTODO"){
        return {
            ...state, 
            "todos" : [
                ...state.todos, 
                {
                    "id" : state.todos.reduce((a,b)=>{
                        return b.id > a ? b.id : a;
                    },0) + 1,
                    "title" : action.title, 
                    "done" : false
                }
            ]
        }
    }
    return state;
}

TodoFt.js

import React from "react";
import {connect} from "react-redux";
class TodoFt extends React.Component {
    constructor() {
        super();
    }

    render() {
        return 
当前:共{this.props.todos.length}条信息-- 已做{this.props.todos.filter(item => item.done).length}条-- 未做{this.props.todos.filter(item => !item.done).length}条
} } export default connect( (state)=>{ return { todos:state.todos } } )(TodoFt)
示例代码

因为TodoItem这个组件是不通天的,所以TodoItem是不能自己独立dispatchstore的。

此时就需要TodoBd帮助,因为TodoBd是通天。

 

这是套路:所有被循环语句map出来的组件,一律不通天,数据父亲给,改变store的能力父亲给。

TodoBd.js组件引入了TodoItem.js组件,因为TodoItem组件是被map出来的,所以信息要传给每一个TodoItem,而不是让TodoItem自己通天拿数据。

TodoBd.js

import React from 'react';
import {connect} from "react-redux";
import TodoItem from "./TodoItem.js";

class TodoBd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        //根据全局的show属性来决定当前todos数组
        if(this.props.show == "ALL"){
            var todos = this.props.todos;
        }else if(this.props.show == "ONLYDONE"){
            var todos = this.props.todos.filter(item=>item.done);
        }else if(this.props.show == "ONLYUNDONE"){
            var todos = this.props.todos.filter(item=>!item.done);
        }

        return (
            
{ todos.map(item=>{ return <TodoItem key={item.id} item={item} delTodo={this.props.delTodo.bind(this)} changeTodo={this.props.changeTodo.bind(this)} > }) }
); } } export default connect( (state)=>{ return { todos : state.todos , show : state.show } }, (dispatch)=>{ return { delTodo(id){ dispatch({"type" : "DELTODO", id}); }, changeTodo(id , k , v){ dispatch({"type" : "CHANGETODO", id, k, v}); } } } )(TodoBd);

 

TodoItem.js

import React from 'react';
export default class TodoItem extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            "onEdit" : false
        }
    }
    render() {
     const {id, title, done} = this.props.item;
        return (
            
<input type="checkbox" checked={done} onChange={(e)=>{               this.props.changeTodo(id, "done", e.target.checked)             }} /> { this.state.onEdit ? <input type="text" defaultValue={title} onBlur={(e)=>{ this.props.changeTodo(id,"title", e.target.value) this.setState({"onEdit" : false}) }} /> : onDoubleClick={()=>{this.setState({"onEdit":true})}}>             {title}            } onClick={()=>{this.props.delTodo(id)}}>删除
); } }

 

index.js

const initObj = {
    "todos" : [
        ...
    ],
    "show" : "ALL"       //ALL、ONLYDONE、ONLYUNDONE
}
export default (state = initObj, action) => {
    if(action.type == "ADDTODO"){
        ...
    }else if(action.type == "DELTODO"){
        return {
            ...state, 
            "todos" : state.todos.filter(item => item.id != action.id)
        }
    }else if(action.type == "CHANGETODO"){
        return {
            ...state, 
            "todos" : state.todos.map(item => {
                //如果遍历到的item项和传入的aciton的id项不一样,此时返回原item
                if(item.id != action.id) return item;
                //否则返回修改之后的item
                return {
                    ...item ,
                    [action.k] : action.v
                }
            })
        }
    }else if(action.type == "CHANGESHOW"){
        return {
            ...state, 
            "show" : action.show
        }
    }
    return state;
}

 

TodoFt.js

import React from 'react';
import {connect} from "react-redux";
import classnames from "classnames";

class TodoFt extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            

当前共{this.props.todos.length}条信息 做完{this.props.todos.filter(item=>item.done).length}条 未做{this.props.todos.filter(item=>!item.done).length}条

); } } export default connect( (state) => { return { todos : state.todos , show : state.show } }, (dispatch) => { return { changeShow(show){ dispatch({"type" : "CHANGESHOW" , show}) } } } )(TodoFt);

四、logger插件

redux-logger用来辅助开发。

npm install --save redux-logger

改变main.js:

import React from "react";
import ReactDOM from "react-dom";
import {createStore , applyMiddleware} from "redux";
import {Provider} from "react-redux";
import logger from "redux-logger";

import App from "./App.js";
//引入reducer
import reducer from "./reducers/index.js";

//创建store
const store = createStore(reducer , applyMiddleware(logger));

ReactDOM.render(
    
        
    
    ,
    document.getElementById("app-container")
);

前端笔记之React(五)Redux深入浅出_第2张图片

也可以使用redux-devtools这个插件。

npm install --save-dev redux-devtools
npm install --save-dev redux-devtools-log-monitor
npm install --save-dev redux-devtools-dock-monitor
npm install --save-dev redux-devtools-chart-monitor

文档:

https://github.com/reduxjs/redux-devtools

https://github.com/reduxjs/redux-devtools/blob/master/docs/Walkthrough.md

 


五、combineReducersbindActionCreators

一个网页的应用程序可能是多个reducer,合并为一个reducer,比如countertodoreducer

 

Redux提供的combineReducers方法,用于reducer的拆分,只要定义各个子reducer函数,然后用这个方法,将它们合成一个大的reducer

Redux提供的bindActionCreators方法,用于通过dispatchaction包裹起来,这条可以通过bindActionCreators创建的方法,直接调用dispatch(action)“隐式调用”。


5.1 combineReducers

reducers/counter.js就是一个普通的纯函数:

export default (state = {"v" : 10},action)=>{
    return state;
}

reducers/todo.js提供的数据:

const initObj = {
    "todos": [
        { "id": 1, "title": "吃饭", "done": false },
        { "id": 2, "title": "睡觉", "done": false },
        { "id": 3, "title": "打豆豆", "done": true }
    ] 
};

export default (state = initObj, action) => {
    return state
}
示例代码

 

 

 

 

reducers/index.js要用redux提供的combineReducers来进行智能合并

import { combineReducers } from "redux";
import counter from "./counter.js";
import todos from "./todos.js";

//暴露合并的reducer
export default combineReducers({
    counter,
    todos
})

 

main.js

import React from "react";
import ReactDOM from "react-dom";
import {createStore} from "redux";
import {Provider} from "react-redux";

import App from "./containers/App.js";
//引入reducer
import reducer from "./reducers";

// 创建 Redux store 来存放应用的状态。
const store = createStore(reducer);

ReactDOM.render(
    
        
    ,
    document.getElementById("app")
);

 

containers/App.js组件使用数据

import React from 'react';
import {connect} from "react-redux";
class App extends React.Component {
    constructor(){
        super();
    }
    render() {
        return (
            

{this.props.v}

); } } export default connect( ({counter}) => ({ v : counter.v }) )(App);

 

components/TodoList/index.js组件

import React from "react";
import { connect } from "react-redux";
class TodoList extends React.Component {
    constructor(){
        super();
    }
    render() {
        return (
            

我是TodoList

{ this.props.todos.map(item=>{ return

{item.title}

}) }
) } }; export default connect( ({todos: {todos}})=>{ return { todos } } )(TodoList)

 

containers/App.js引入组件:

import React from 'react';
import TodoList from "../components/todolist/index.js";
import Counter from "../components/counter/index.js";
 
export default class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            
); } }

5.2 bindActionCreators

bindActionCreators会将actiondispatch绑定并返回一个对象,这个对象会作为props的一部分传入组件中。

bindActionCreators主要作用:一般情况下,可以通过Providerstore通过Reactconnext属性向下传递,bindActionCreators的唯一用处就是需要传递action creater到子组件,并且该子组件并没有接收到父组件上传递的storedispatch

官方的文件夹结构:https://github.com/reduxjs/redux/tree/master/examples/todomvc/src

 

actions/counterActions.js新建actions文件夹存放type

// 我们把return一个action的函数叫做“action creator”
// 所以这个文件向外暴露了几个动作
export const add = () => ({ type: "ADD" })
export const minus = () => ({ type: "MINUS" })
export const cheng = () => ({ type: "CHENG" })
export const chu = () => ({ type: "CHU" })

 

counter/index.js计数器组件

import React from 'react';
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import * as counterActions from "../../actions/counterActions.js";

class Counter extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            

Counter : {this.props.v}

); } } export default connect( ({counter}) => ({ v : counter.v }), (dispatch) => ({ //这里的dispatch,等同于store中的store.dispatch,用于组合action counterActions : bindActionCreators(counterActions , dispatch) }) )(Counter);

 

app/reducers/counter.js

import {ADD, MINUS, CHENG, CHU} from "../constants/COUNTER.js";

export default (state = {"v" : 0} , action) => {
    if(action.type == "ADD"){
        return {
            ...state , 
            "v" : state.v + 1
        }
    }else if(action.type == "MINUS"){
        return {
            ...state , 
            "v" : state.v - 1
        }
    }else if(action.type == "CHENG"){
        return {
            ...state , 
            "v" : state.v * 2
        }
    }else if(action.type == "CHU"){
        return {
            ...state , 
            "v" : state.v / 2
        }
    }
    return state;
}
示例代码

 

 

 

 

todolist/index.js

import React from 'react';
import TodoHd from "./TodoHd.js";
import TodoBd from "./TodoBd.js";

export default class TodoList extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
              

TodoList

); } }

 

TodoHd.js

import React from 'react';
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import * as todoActions from "../../actions/todoActions.js";

class TodoHd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            
); } } export default connect( null , (dispatch) => ({ todoActions : bindActionCreators(todoActions , dispatch) }) )(TodoHd);

 

TodoBd.js

import React from 'react';
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import * as todoActions from "../../actions/todoActions.js";

class TodoBd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            
{ this.props.todos.map(item=>{ return

{item.title}

}) }
); } } export default connect( ({todo}) => ({ todos : todo.todos }) , (dispatch) => ({ todoActions : bindActionCreators(todoActions , dispatch) }) )(TodoBd);

 

为了防止actiontype命名冲突,此时要单独存放在const文件夹中:

app\constants\COUNTER.js

export const ADD = "ADD_COUNTER";
export const MINUS = "MINUS_COUNTER";
export const CHENG = "CHENG_COUNTER";
export const CHU = "CHU_COUNTER";

app\constants\TODO.js

export const ADD = "ADD_TODO";
export const DEL = "DEL_TODO";

然后就可以在以下文件中,引入以上常量,然后使用大写的常量替换type字符串

l actions中的counterActions.jstodoActions.js

l reducers中的todo.jscounter.js

 

actions/TodoActions.js

import {ADD , DEL} from "../constants/TODO.js";
export const add = (title) => ({"type" : ADD, title});
export const del = (id) => ({"type" : DEL, id});

actions/counterActions.js

import {ADD , MINUS , CHENG , CHU} from "../constants/COUNTER.js";
export const add = () => ({"type" : ADD});
export const minus = () => ({"type" : MINUS});
export const cheng = () => ({"type" : CHENG});
export const chu = (n) => ({"type" : CHU , n});

 

reducers/todo.js

import {ADD , DEL} from "../constants/TODO.js";
const initObj = {
    "todos" : [
        {"id" : 1 , "title" : "吃饭" , "done" : false},
        {"id" : 2 , "title" : "睡觉" , "done" : false},
        {"id" : 3 , "title" : "打豆豆" , "done" : false}
    ]
}
export default (state = initObj , action) => {
    if(action.type == ADD){
        return {
            ...state ,
            "todos" : [
                ...state.todos ,
                {
                    "id" : state.todos.reduce((a,b)=>{return b.id > a ? b.id : a},0) + 1,
                    "title": action.title,
                    "done" : action.done
                }
            ]
        }
    }else if(action.type == DEL){
        return {
            ...state ,
            "todos" : state.todos.filter(item => item.id != action.id)
        }
    }
    return state;
}

 

reducers/counter.js

import {ADD , MINUS , CHENG , CHU} from "../constants/COUNTER.js";

export default (state = {"v" : 0} , action) => {
    if(action.type == ADD){
        return {
            ...state , 
            "v" : state.v + 1
        }
    }else if(action.type == MINUS){
        return {
            ...state , 
            "v" : state.v - 1
        }
    }else if(action.type == CHENG){
        return {
            ...state , 
            "v" : state.v * 2
        }
    }else if(action.type == CHU){
        return {
            ...state , 
            "v" : state.v / action.n
        }
    }
    return state;
}

 

你可能感兴趣的:(前端笔记之React(五)Redux深入浅出)