React系列(三),直面Redux

304 阅读12分钟

写在前面

文章为了更清晰的让大家了解到redux的用法,所以会一步一步教大家redux整个过程,由浅入深,更适合新手,老司机可以直接超车~

温馨提示:项目基于TypeScript环境搭建,大部分都使用ES6语法编写示例代码,还不会的就把类型检查忽略,也可以去官网了解下噢,可以试着接触下

1.下载redux需要的依赖,前面只会用到redux,这里一块下了

$ npm install redux react-redux

2.依赖下载好之后,在项目src目录下创建一个文件夹,本着顾名思义的开发原则这里命名为redux,大家可以自由命名(如果项目不需要你维护的话😄)

img
img

3.在刚刚新建的redux文件夹里面新建一个store.ts文件,下面就是最简单的一个store原型了

//store.ts
import { createStore } from 'redux'const initState = {  name'yydbb',  age18}//reducer是一个纯函数,这里且将纯函数理解为入参相同,返回值就一定相同,具体定义请大家移步官方文档const reducer = (state = initState, action: any) => {  return state}const store = createStore(reducer)export default store;

4.我猜大家看到上面示例代码的第一反应肯定是:就这???是不是超简单?是的,就是这么简单,下面来康康具体用法

reducer是一个纯函数,它接收两个参数,第一个是全局初始化state,第二个是action,action是一个对象,里面必须包括type,还可以附带其他参数用来修改state,最后会返回一个新的state,在我们不懈努力之后,示例代码如下:

import { createStore } from 'redux'const initState = {  name'yydbb',  age18}//reducer是一个纯函数,这里且将纯函数理解为入参相同,返回值就一定相同,具体定义请大家移步官方文档const reducer = (state = initState, action: any) => {
  if (action.type === 'CHANGE_NAME') {    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样    myState.name = action.nameVal    return myState  }  return state}const store = createStore(reducer)export default store;

相比前一步的代码,康康我们增加了些什么

我们写了一个看不懂的东西,使用过typescript的小伙伴们就不用多说,这里解释一下,这里我们定义了一个接口,约束了reducer第二个参数action的数据类型,如果项目中其他开发要使用reducer,他就必须按照这个格式传参,不然咱就不给他通过,让他自己找bug去

interface Action{  
  typestring,  
  ageVal:number
}

接下来我们拓展了一下reducer函数的处理逻辑,相信这里面的代码大家理解起来都没问题(有问题的就回去学学JavaScript的基本语法)

const reducer = (state = initState, action: Action) => {
  if (action.type === 'CHANGE_NAME') {
    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样
    myState.name = action.nameVal
    return myState
  }
  if (action.type === 'CHANGE_AGE') {
    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样    
    myState.age--
    return myState
  }
  return state
}

其实到这一步,redux就可以用起来了,你没听错,让我们来试试吧

5.接下来我们就要在页面中使用一下刚刚写好的redux,期不期待😄

  • 在Login.tsx页面中引入redux
import React from "react";
import store from './../redux/store'
class Login extends React.PureComponent<anyany> {  state = {    age0,    name'enen'  }  componentDidMount() {    const { name, age } = store.getState()    this.setState({      age,      name    })  }  public render() {    return (      <div>        我是{this.state.name}我今年{this.state.age}      </div>    );  }}
  • store里面关键API就是getState(),这个方法拿到的就是我们store.ts文件夹里面定义的初始化initState的值,最后在页面上的效果为:
img
img
  • 现在我们的redux已经能够在页面上应用了,但是仅仅只是能用,不能修改,聪明的小伙伴已经开始琢磨了吧,接下来一起来康康怎么去修改它,在我们加班加点修改之后,Login.tsx页面变得不认识了
class Login extends React.PureComponent<anyany> {
  state = {
    age0,
    name'enen'
  }
  componentDidMount() {
    const { name, age } = store.getState()
    this.setState({
      age,
      name
    })
  }

  public render() {
    return (
      <div>
        我是{this.state.name}我今年{this.state.age}
        <button onClick={this.changeName}>修改名字</button>
        <button onClick={this.changeAge}>修改年龄</button>
      </div>

    );
  }
  changeName = () => {
    const action = {
      type'CHANGE_NAME',
      nameVal'开心'
    }
    store.dispatch(action)
    console.log(store.getState());
  }
  changeAge = () => {
    const action = {
      type'CHANGE_AGE'
    }
    store.dispatch(action)
    console.log(store.getState());
  }
}

贴上我们修改后的store.ts

import { createStore } from 'redux'

const initState = {
  name'yydbb',
  age18
}
interface Action {
  typestring,
  ageVal?: number,
  nameVal?: string
}

//reducer是一个纯函数,这里且将纯函数理解为入参相同,返回值就一定相同,具体定义请大家移步官方文档
const reducer = (state = initState, action: Action) => {
  if (action.type === 'CHANGE_NAME') {
    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样
    myState.name = action.nameVal
    return myState
  }
  if (action.type === 'CHANGE_AGE') {
    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样    
    myState.age--
    return myState
  }
  return state
}
const store = createStore(reducer)

export default store;

页面操作效果如下:

img
img

我们一共操作了五次,一次changeName,四次changeAge,如图大家看到打印的结果是我们想要的,但是界面展示的结果并不是我们想要的,这是为什么呢?我们不是改了好几次了吗,为什么界面不响应啊,Vuex比这个好多了,我提交一个commit界面马上就能看到更新后的值了,redux怎么可能没有,我们能想到的官方怎么可能想不到😄,还是在Login.tsx文件中,我们仅仅修改了一个地方,如下:

componentDidMount() {
    store.subscribe(() => {
      const { name, age } = store.getState()
      this.setState({
        age,
        name
      })
    })
  }

这里我们先不管为什么这样写先康康效果:

img
img

这么神奇?再回来康康我们修改的地方,你会发现subscribe这个神秘的单词,复制到百度翻译(没有打广告的意思,大家喜欢啥就用啥)去看看是啥意思,原来是订阅的意思,那这里我们就可以理解为页面获取redux数据的同时也订阅了它,这种好处就是之后如果数据发生改变了,页面就可以看到最新的数据了,缺点就是它相当于Vue里面的watch,如果不注销会一直在内存中消耗资源,所以subscribe这个函数设计的时候就想好了,它的返回值就是一个注销函数,执行就行了,当然暂时先不建议用它,附上用法:

let unsubscribe =
      store.subscribe(() => {
        const { name, age } = store.getState()
        this.setState({
          age,
          name
        })
      })
    // unsubscribe()

小结:

到这里我们已经对Redux最简单的用法有所了解了,下面我们将对store.ts进行拆分,做一个小小的优化,下面是我们优化过后的文件目录:

img
img

可以看到多了一个reducer.ts文件,一个actionType.ts文件,reducer.ts文件内容如下:

import { CHANGE_NAMECHANGE_AGE } from './actionType'
const initState = {
  name'yydbb',
  age18
}
interface Action {
  typestring,
  ageVal?: number,
  nameVal?: string
}
//reducer是一个纯函数,这里且将纯函数理解为入参相同,返回值就一定相同,具体定义请大家移步官方文档
const reducer = (state = initState, action: Action) => {
  if (action.type === CHANGE_NAME) {
    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样
    myState.name = action.nameVal
    return myState
  }
  if (action.type === CHANGE_AGE) {
    let myState = JSON.parse(JSON.stringify(state))//粗暴的深拷贝,真实项目中不建议大家这样
    myState.age--
    return myState
  }
  return state
}
export default reducer

actionType.ts文件内容如下:

export const CHANGE_NAME = 'CHANGE_NAME'
export const CHANGE_AGE = 'CHANGE_AGE'

actionType.ts主要目的是将type常量标准化,减少bug率,哪里用到这些常量,引用就行

拆分之后的store.ts文件内容如下:

import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

export default store;

是不是一下开心起来了,简直不敢想,四行代码?(这里补充一点一个项目中只能有一个store

6.到这里大家有没有发现哪里不对,没错还有一个比较明显的问题,就是如果想要在每个页面都用到store,那就得每个页面都要引用,这个时候前面下载的react-redux就要上场了,话不多说,动动小手~

下面是重新写好的index.tsx

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from 'react-redux'
import store from './redux/store'
import "./index.css";
import AppRouter from "./router/router";
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <AppRouter />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

两个地方:

  • 从react-redux中引入Provider组件
  • 引入store
  • 将Provider组件包裹页面路由把store作为参数传递给所有路由
  • 接下来就可以在每个页面使用store的数据且不用引入store

下面是重新写好的Login.tsx

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

class Login extends React.PureComponent<anyany> {
  state = {
    age0,
    name'enen'
  }
  componentDidMount() {
    const { name, age } = this.props
    this.setState({
      age,
      name
    })
  }
  componentDidUpdate() {
    console.log(this.props.namethis.props.age);
  }

  public render() {
    return (
      <div>
        我是{this.state.name}我今年{this.state.age}
        <button onClick={this.props.changeName}>修改名字</button>
        <button onClick={this.props.changeAge}>修改年龄</button>
      </div>
    );
  }
}


const mapStateToProps = (state: any) => {
  return {
    age: state.age,
    name: state.name
  }
}
const mapDispathToProps = (dispatch: any) => {
  return {
    changeAge() {
      const action = {
        type'CHANGE_AGE'
      }
      dispatch(action)
    },
    changeName() {
      const action = {
        type'CHANGE_NAME',
        nameVal'开心'
      }
      dispatch(action)
    }
  }
}
export default connect(mapStateToProps, mapDispathToProps)(Login)
  • 重点是connect这个API,它接收两个参数,如上图,顾名思义,将store中的state和reducer分别映射到props里面,现在我们把两个dispath操作都写到mapDispathToProps 里面来了,这使得我们页面逻辑变少,更加清晰了。对于这种用法大家一定要多写写,一时间理解不了,先学会用,后再深入了解
  • 我们在生命周期componentDidUpdate里面打印修改后的值,发现是实时的,但是页面没有实时更新
img
img

找到一个比较简单粗暴(更优解决方案笔者也在摸索)的解决方案,在componentDidUpdate生命周期里面再次给name和age赋值,可以达到预期效果,也可以直接使用props,这是实时的,这里赋值是为了后面可能会有别的操作

componentDidUpdate() {
    const { name, age } = this.props
    this.setState({
      age,
      name
    })
    console.log(this.props.name, this.props.age);
  }

效果如下:

img
img

大家可以发现,每个操作都打印了两次,那是因为每次修改页面的state,页面会触发componentDidUpdate生命周期,所以选择在这里处理监听以达到subscribe同样的效果

写在最后:

  • 相信看完这边文章,小伙伴们对redux用法应该能掌握的差不多了
  • redux还有很多插件,插件只是为了拓展功能,大家有兴趣的可以自己去琢磨,爱折腾才能不断进步哦~
  • 排版有些乱了不好意思,觉得还不错的大家点个赞~😄