JerrysBlog 2019-11-17
最近忙里偷闲学习了react。由于之前一直都是使用vue做项目,所以学习react的时候觉得既熟悉又陌生。
熟悉是因为它和vue拥有许多相似的概念,包括都推崇组件化、都拥有’props’的概念、核心都是视图层框架等等。虽然react不像vue拥有那么多丰富的API,但是在我看来,正因为react本身没有过度的封装,再加上react的社区非常成熟与活跃,才使得react的开发灵活多变,相比起来,我觉得react更适合大型项目的开发,react的函数式编程也更容易实现前端自动化测试。
尤大自己也说过vue从一开始的定位就是尽可能的降低前端开发的门槛,让更多的人能够更快地上手开发。所以学习起来,vue更加圆滑,而react相对陡峭。两者在我看来都是非常优秀的框架,没有高低之分,我们可以根据不同的开发情况选择不同的开发工具。
今天主要是想写一下如何在react中管理数据,所以在阅读这篇文章之前,我默认你已经可以自己搭建react项目,并可以看懂react的基本代码。如果你没有使用过react,但有过其他框架的使用经验,那么我认为这并不太影响你这篇文章的观看体验。
Redux=Reducer+Flux,Flux是Facebook推出的最原始的辅助React的数据层框架,但是它并不是那么的好用,所以有人把Flux做了一个升级,变成了Redux。
请看下面这张图
假设底部绿色的组件要和最顶层的组件通信,那么绿色的组件需要层层把消息转发给父级组件,直到传到最顶层的组件,如果我们项目中的组件非常之多,组件之间又经常需要共享传值的话,那么使用react这种父子通信的方式,整个项目的开发就会变得非常冗余,也不易维护。
前面说过,react是一个视图层框架(并不是什么问题都依靠react解决,react只解决数据和页面渲染——也就是搭建视图, 至于组件渲染交给别的数据层框架来做额外的支撑),所以我们需要一个数据层框架去协助react帮助数据管理,目前主流和react搭配的就是redux。
redux要求我们把数据都存放在一个名为store的公共存储区域,我们把数据都存放在store中。如果想通过绿色的组件改变数据传给其他组件,那么我们只需要操作store就可以了,接着其他灰色的组件会自动感知到变化,然后重新去store中取数据,这样我们取到的数据,就是刚刚绿色组件所更改的数据。也就是说,redux间接地帮我们实现了组件通信的功能,让我们的组件通信变得非常的轻松。
❗️但是我们要知道,redux不是只为react服务的,而是为JavaScript服务的状态容器,react-redux才是专门为react服务的状态管理插件,本篇文章主要讲解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也就不是一个纯函数了
在学习redux前,我希望你可以了解Mutabilit(可变性)和Immutability(不变性)这两个概念。
首先从字面上理解,「可变」意味着可以出现变化,可以变化,就意味着可能会出现一些问题或是bug。
「不可变」就代表某些数据是不可修变的,如果想要改变不可变的数据,那么只能去复制旧的数据,再产生新的数据来取代旧的数据,我们永远不要去修改旧的数据。
我这里不做过多的赘述,如果你对这块有兴趣,可以去自行查找一些文章了解,本文只需要你了解这个概念。
reactComponents: 每一个页面上的组件。
actionCreators:管理action的地方。
action:动作,它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store,通常是一个对象。
store:存储数据的公共区域,也可以理解为把action和reducers联系到一起的对象。
reducers:处理不同的action类型,告诉store该给组件什么样的数据,然后store再把这个数据给到对应的组件。
这里你或许会看的有点蒙,我下面用代码来解释一下redux的工作流程。
npm安装 | yarn安装 |
---|---|
npm install --save redux | yarn add redux |
我想实现一个todoList功能,当我点击提交按钮的时候,在input下面会增加我刚刚输入的内容。
其中,input和button是父组件,下面的ul是子组件。
效果如下
先在刚刚搭建好的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> ) } }
此时运行出来应该是这样
目录结构
此刻我们已经可以取到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里的数据已经被改变了,list增加了1条数据,inputValue也被清空了,这证明我们之前在reducer中编写的代码都生效了,但是都并没有渲染在页面上。现在页面上input的value值是空值,是因为一开始inputValue的值就是空,而不是我们后来清空的。这一切都因为我们并没有在组件中去监听更新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吧。
如果你对这篇文章有任何疑问或补充,都可以在评论区给我留言讨论。
如果你有兴趣,还可以来我的博客看我的最新更新,我平时还会总结一些日语的小知识,喜欢日语的小伙伴也可以和我一起沟通讨论。
顺便一提最近看了排球少年的动漫,虽然比较冷门但是真的是一部不可多得的好作品,无论你喜不喜欢排球我觉得你看了这部动漫后都会爱上它的,强烈安利一波。
大家晚安啦。