美文网首页
一步步带你入门Redux管理数据

一步步带你入门Redux管理数据

作者: 是胡桃呀 | 来源:发表于2019-05-21 01:59 被阅读0次

    闲聊

     
    最近忙里偷闲学习了react。由于之前一直都是使用vue做项目,所以学习react的时候觉得既熟悉又陌生。
     
    熟悉是因为它和vue拥有许多相似的概念,包括都推崇组件化、都拥有’props’的概念、核心都是视图层框架等等。虽然react不像vue拥有那么多丰富的API,但是在我看来,正因为react本身没有过度的封装,再加上react的社区非常成熟与活跃,才使得react的开发灵活多变,相比起来,我觉得react更适合大型项目的开发,react的函数式编程也更容易实现前端自动化测试。
     
    尤大自己也说过vue从一开始的定位就是尽可能的降低前端开发的门槛,让更多的人能够更快地上手开发。所以学习起来,vue更加圆滑,而react相对陡峭。两者在我看来都是非常优秀的框架,没有高低之分,我们可以根据不同的开发情况选择不同的开发工具。

    前言

    今天主要是想写一下如何在react中管理数据,所以在阅读这篇文章之前,我默认你已经可以自己搭建react项目,并可以看懂react的基本代码。如果你没有使用过react,但有过其他框架的使用经验,那么我认为这并不太影响你这篇文章的观看体验。
     

    Redux

    Redux=Reducer+Flux,Flux是Facebook推出的最原始的辅助React的数据层框架,但是它并不是那么的好用,所以有人把Flux做了一个升级,变成了Redux。

    为什么要使用redux

    请看下面这张图


    组件通信

    假设底部绿色的组件要和最顶层的组件通信,那么绿色的组件需要层层把消息转发给父级组件,直到传到最顶层的组件,如果我们项目中的组件非常之多,组件之间又经常需要共享传值的话,那么使用react这种父子通信的方式,整个项目的开发就会变得非常冗余,也不易维护
     
    前面说过,react是一个视图层框架(并不是什么问题都依靠react解决,react只解决数据和页面渲染——也就是搭建视图, 至于组件渲染交给别的数据层框架来做额外的支撑),所以我们需要一个数据层框架去协助react帮助数据管理,目前主流和react搭配的就是redux
     
    redux要求我们把数据都存放在一个名为store的公共存储区域,我们把数据都存放在store中。如果想通过绿色的组件改变数据传给其他组件,那么我们只需要操作store就可以了,接着其他灰色的组件会自动感知到变化,然后重新去store中取数据,这样我们取到的数据,就是刚刚绿色组件所更改的数据。也就是说,redux间接地帮我们实现了组件通信的功能,让我们的组件通信变得非常的轻松。
     

    ❗️但是我们要知道,redux不是只为react服务的,而是为JavaScript服务的状态容器,react-redux才是专门为react服务的状态管理插件,本篇文章主要讲解redux。

    redux 三大原则

    1.单一数据源
    store是唯一的。
    整个应用的数据被储存在一棵object tree(对象树)中,并且这个 object tree 只存在于唯一一个 store 中。
     
    2.state是只读的
    唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
     
    3.使用纯函数执行修改
    为了描述 action 如何改变 state tree ,你需要编写 reducers。
    reducer必须是纯函数: 纯函数是指给定固定的输入, 就一定会有固定的输出, 且不会有任何副作用; 一旦一个函数有一个settimeout或者ajax或者new Date相关内容的时候, 它就不是一个纯函数, 所以reducer里不可以有异步的操作。
    ❗️副作用: 例如对参数的修改就是副作用, 这个时候reducer也就不是一个纯函数了

    Mutabilit(可变性) & Immutability(不变性)

    在学习redux前,我希望你可以了解Mutabilit(可变性)和Immutability(不变性)这两个概念。
     
    首先从字面上理解,「可变」意味着可以出现变化,可以变化,就意味着可能会出现一些问题或是bug。
     
    「不可变」就代表某些数据是不可修变的,如果想要改变不可变的数据,那么只能去复制旧的数据,再产生新的数据来取代旧的数据,我们永远不要去修改旧的数据。
     
    我这里不做过多的赘述,如果你对这块有兴趣,可以去自行查找一些文章了解,本文只需要你了解这个概念。

    redux的工作流程

    redux工作流程
    reactComponents: 每一个页面上的组件。
    actionCreators:管理action的地方。
    action:动作,它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store,通常是一个对象。
    store:存储数据的公共区域,也可以理解为把action和reducers联系到一起的对象。
    reducers:处理不同的action类型,告诉store该给组件什么样的数据,然后store再把这个数据给到对应的组件。
     
    这里你或许会看的有点蒙,我下面用代码来解释一下redux的工作流程。

    安装redux

    npm安装 yarn安装
    npm install --save redux yarn add redux

    redux代码讲解

    我想实现一个todoList功能,当我点击提交按钮的时候,在input下面会增加我刚刚输入的内容。
    其中,input和button是父组件,下面的ul是子组件。
    效果如下

    todoList

    基础结构-取值

    先在刚刚搭建好的react项目中的src文件下建立一个store文件夹(你也可以建在任何的组件文件夹下),在store里分别创建一个index.js和reducer.js。
     
    reducer.js

    // 定义初始数据defaultState,如果不给state设置一个初始数据,那么最初state就是一个undefined。
    // 这里我已经为todoList写入了一个字符串inputValue和数组list。
    const defaultState = {
      inputValue: '',
      list: ['默认数据1', '默认数据2']
    };
    export default (state = defaultState, action) => {
      // state指的是上一次存储的数据, action是组件传过来的内容
      return state;
    }
    

     
    index.js

    // 从redux引入createStore方法
    import { createStore } from 'redux'; 
    // 从刚刚创建的reducer.js引入reducer
    import reducer from './reducer';
    // 定义一个名为store的redux存储区,我们把reducer作为参数传入createStore方法来构造这个存储区,store里的数据只可以通过reducer来修改。
    const store = createStore(reducer);
    
    // 导出store
    export default store;
    

     
    创建子组件List.js

    import React from 'react';
    
    const List = (props) => {
        return (
          <div>
            <ul>
              {
                props.list.map((item, index) => {
                  return <li key={index}>{item}</li>
                })
              }
            </ul>
          </div>
        );
    }
    
    export default List;
    

    ❗️此处的List组件是一个无状态组件,没有任何的逻辑操作,所有逻辑操作交由父组件执行。
     
    接着修改你的App.js (我这里把App.js作为父组件)

    import React, { Component } from 'react'
    import store from './store'
    import List from './List'
    
    export default class App extends Component {
      constructor(props) {
        super(props);
        // 用store的getState()方法取出store的数据,再赋值给this.state
        this.state = store.getState();
      }
      render() {
        return (
          <div>
            <input type="text"/>
            <button>提交</button>
            <List list={this.state.list}></List>
          </div>
        )
      }
    }
    

    此时运行出来应该是这样


    todoList

    目录结构


    目录

    修改store

    此刻我们已经可以取到store里的数据了,那么我们现在想在点击提交的时候,list里新增一条数据,并且实时地响应出来,应该怎么做呢。
     
    修改App.js

    import React, { Component } from 'react'
    import store from './store';
    import List from './List'
    export default class App extends Component {
      constructor(props) {
        super(props);
        this.state = store.getState();
        // 修改事件的this指向,否则this指向undefined
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleClick = this.handleClick.bind(this);
      }
      render() {
        return (
          <div>
            <input
              type="text"
              onChange={this.handleInputChange}
              value={this.state.inputValue}
            />
            <button onClick={this.handleClick}>提交</button>
            <List list={this.state.list}></List>
          </div>
        )
      }
      handleInputChange(e) {
        // 1) 创建action
        const action = {
          type: 'change_input_value',
          value: e.target.value
        }
        // 2) 传给store
        store.dispatch(action);
        // 3) store如果接收到了action, 会自动把之前的数据和action传给reducer (这步store帮我们做了)
      }
      handleClick() {
        const action = {
          type: 'add_todo_item',
        }
        store.dispatch(action);
      }
    }
    

     
    然后修改我们的reducer

    const defaultState = {
      inputValue: '',
      list: ['默认数据1', '默认数据2']
    };
    // 4) reducer拿到之前的数据和当前操作的信息后对数据进行处理,然后返回新的数据给store
    export default (state = defaultState, action) => {
      const newState = JSON.parse(JSON.stringify(state)); //深拷贝,因为reducer可以接收state, 但绝不能修改state 所以要拷贝state
      switch (action.type) {
        case 'change_input_value':
          newState.inputValue = action.value;
          return newState; //return给了store
        case 'add_todo_item':
          newState.list.push(newState.inputValue);
          // 添加成功后清空inputValue
          newState.inputValue = '';
          return newState;
        default:
          break;
      }
      return state;
    }
    

    此时我们会发现在input框里输入数据页面是没有反应的,点击提交,页面上也没有发生任何变化,别急,我们先来打印一下store,这也是我们学redux时经常容易犯的错误。
     
    我们在handleClick方法的最后,用store.getState()方法来打印一下store的值
    ❗️注意是最后,store.dispatch(action)的后面

    console.log(store.getState());
    
    打印store

    我们发现store里的数据已经被改变了,list增加了1条数据,inputValue也被清空了,这证明我们之前在reducer中编写的代码都生效了,但是都并没有渲染在页面上。现在页面上input的value值是空值,是因为一开始inputValue的值就是空,而不是我们后来清空的。这一切都因为我们并没有在组件中去监听更新store里的数据,我们应该在页面中监听store,当store发生变化时,实时更新我们的数据。

    监听store

    App.js最终代码

    import React, { Component } from 'react'
    import store from './store';
    import List from './List'
    export default class App extends Component {
      constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleClick = this.handleClick.bind(this);
        // 5) 监听store的变化
        // 订阅store, 只要store发生改变, subscribe里的函数就会被自动执行
        this.handleStoreChange = this.handleStoreChange.bind(this);
        store.subscribe(this.handleStoreChange);
      }
      render() {
        return (
          <div>
            <input
              type="text"
              onChange={this.handleInputChange}
              value={this.state.inputValue}
            />
            <button onClick={this.handleClick}>提交</button>
            <List list={this.state.list}></List>
          </div>
        )
      }
    
      handleInputChange(e) {
        const action = {
          type: 'change_input_value',
          value: e.target.value
        }
        store.dispatch(action);
      }
    
      handleClick() {
        const action = {
          type: 'add_todo_item',
        }
        store.dispatch(action);
      }
    
      handleStoreChange() {
        // 6) 当感知到store变化的时候, 调用store.getState()方法从store中重新取一次数据, 然后调用setState替换掉当前组件中的数据, 这样就会同步数据了
        this.setState(store.getState());
      }
    }
    

    ❗️我们上面说过,不要直接更改state的值,所以我们每次修改时都创建了一个新的state,返回的也是全新的state。
    不过,大量重复的代码就是问题的源泉,我们在编写代码时,理应去减少出现bug的可能性。所以,当我们日常开发时,我推荐使用immutable.js或一些其他的第三方库——我们在最初就把state生成immutable对象, 这样可以百分百保证state不会被改变。

    总结

    拿刚刚的例子来说,我们首先把input的值和store中的inputValue关联到了一起,如果你想修改input框的value值,就必须通过修改store中的inputValue实现。我们用onChange事件监听了input,在每次修改input中的值的时候,我们都创建了一个action,并把这个action派发给了store。
     
    store接收到了这个action,会自动把这个action传给reducer。reducer拿到这个action,开始对比action的type值,并进行相应的数据操作,之后返回了一个新的数据给store。我们在组件内监听了store的变化,所以当reducer把值返回给了store,store更新了自己的数据,我们的组件就会监听到刚刚store的变化,随之更换组件内store的数据。
     
    input输入流程:App→store→reducer→store→App检测到store发生变化,更新数据,渲染页面
     
    点击提交流程:App→store→reducer→store→App.js检测到store发生变化,更新数据→父组件App重新渲染触发子组件List更新渲染
     

    优化

    写到这里,如果你只想了解该怎么使用redux,那么至此之前的代码应该已经足够让你上手去使用redux了。但是其实上面的代码中还有很多可以优化的地方,我没有直接把优化过后的代码写出来是怕不易于初学者阅读学习,容易看晕。
     
    比如说我们应该利用actionTypes统一常量, 预防因拼写引发的bug,以及将action的创建放到actionCreators中统一进行管理。这样做的优点除了提高代码的可维护性,还可以方便自动化测试。
     
    在实际开发中,redux也应遵照组件化开发,建议每个组件都应该拥有自己的store文件夹,src目录下的store应仅仅作为各个组件内store的集合。
     
    在子组件List上,我们使用数组的index作为key值并不是一个好的做法。事实上我认为不到万不得已的情况不要使用index作为key值。因为列表每一项的顺序都可能会发生变化(比如说我们如果删除list中的某一项时,list的顺序就发生了变化,list中每一项的index值都发生了改变),react又是通过diff算法去渲染页面的,diff算法通过key值去对比虚拟dom,如果key值全部发生改变,那虚拟dom便会全部更新,这明显会降低我们的性能,所以说使用数组的index作为key值是下下策,有兴趣的话可以去看看这篇文章深度解析使用索引作为 key 的负面影响
     
    因为diff算法(虚拟dom从顶层 层层比对)的原因,所以在父组件内只要一改变inputValue的值,子组件就会重新渲染,即使我们并没有修改list数据。这同样会降低我们的性能,试想一下,如果你拥有非常多的子组件,父组件输入任何一个字符都会导致所有子组件的重新渲染,这会消耗多少的性能呢?
    为了解决这种多余性能的消耗,我们应该在子组件内利用react内置的生命周期函数shouldComponentUpdate去阻止子组件跟随父组件去执行无谓的render函数,这样就可以避免虚拟dom的比对,提升性能。
     

    这篇文章到这里就全部结束了,本来想在一篇里把redux和react-redux都写出来,但是怕太长了,所以下次找时间再写react-redux吧。
     
    如果你对这篇文章有任何疑问或补充,都可以在评论区给我留言讨论。
    如果你有兴趣,还可以来我的博客看我的最新更新,我平时还会总结一些日语的小知识,喜欢日语的小伙伴也可以和我一起沟通讨论。
     
    顺便一提最近看了排球少年的动漫,虽然比较冷门但是真的是一部不可多得的好作品,无论你喜不喜欢排球我觉得你看了这部动漫后都会爱上它的,强烈安利一波。
     
    大家晚安啦。

    乌野高校排球部

    相关文章

      网友评论

          本文标题:一步步带你入门Redux管理数据

          本文链接:https://www.haomeiwen.com/subject/wpmrzqtx.html