Redux的使用详解(一)

249 阅读10分钟

Redux的核心理念

  • Store
    • 存储数据
// 初始化的数据
const initialState = {
  name: "kobe",
  counter: 100
}
  • action
    • Redux要求我们通过action来更新数据:
      • 所有数据的变化,必须通过派发(dispatch)action来更新;
      • action是一个普通的JavaScript对象,用来描述这次更新的typecontent
// 修改store中的数据: 必须action
const nameAction = { type: "change_name", name: "kobe" }
store.dispatch(nameAction)
  • reducer
    • 如何将stateaction联系在一起呢?--- reducer
      • reducer是一个纯函数;
      • reducer做的事情就是将传入的stateaction结合起来生成一个新的state
// 定义reducer函数: 纯函数
// 两个参数: 
// 参数一: store中目前保存的state
// 参数二: 本次需要更新的action(dispatch传入的action)
// 返回值: 它的返回值会作为store之后存储的state
function reducer(state = initialState, action) {
  switch(action.type) {
    case "change_name":
      return { ...state, name: action.name }
    case "add_number":
      return { ...state, counter: state.counter + action.num }
    default:
       // 没有新数据更新, 那么返回之前的state
      return state
  }
}

核心的原则有哪些?(面试)

详细版

  • 单一数据源
    • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
    • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
    • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
  • State是只读的
    • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
    • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
    • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
      • 竟态(Race Condition)指的是在多线程或多进程的程序中,由于执行顺序的不确定性,导致不同的线程在对共享资源进行读写操作时,出现了不可预测的、混乱的结果。竟态可能导致程序崩溃、数据损坏等问题。
  • 使用纯函数来执行修改
    • 通过reducer将 旧state action联系在一起,并且返回一个新的State
    • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducer,分别操作不同state tree的一部分;
    • 但是所有的reducer都应该是纯函数,不能产生任何的副作用;

简略版

Redux 的核心理念是:单一数据源、状态只读、使用纯函数来执行状态修改。

  1. 单一数据源Redux 应用的状态被存储在单一对象树中,这个对象被称作“状态树”,这让整个应用的状态变得可预测,也方便状态管理。
  2. 状态只读Redux 中的状态是只读的,唯一改变状态的方式是通过触发 actionaction 是一个描述状态变化的纯对象。
  3. 使用纯函数来执行状态修改:为了让应用状态更可控,Redux 要求修改状态的函数(称作 reducer)是一个纯函数,即给定同样的输入,总是返回同样的输出,不产生任何副作用。这样能够确保状态修改的可测试性和可维护性。

Redux的使用过程

  1. 创建一个对象,作为我们要保存的状态:
// 初始化的数据
const initialState = {
  name: "kobe",
  counter: 100
}

  1. 创建Store来存储这个state
    • 创建store时必须创建reducer
    • 我们可以通过 store.getState 来获取当前的state
// 定义reducer函数: 纯函数
// 两个参数: 
// 参数一: store中目前保存的state
// 参数二: 本次需要更新的action(dispatch传入的action)
// 返回值: 它的返回值会作为store之后存储的state
function reducer(state = initialState, action) {
  // 有新数据进行更新的时候, 那么返回一个新的state
  if (action.type === "change_name") {
    return { ...state, name: action.name }
  } else if (action.type === "add_number") {
    return { ...state, counter: state.counter + action.num }
  }

  // 没有新数据更新, 那么返回之前的state
  return state
}


// 创建的store
const store = createStore(reducer)
  1. 通过action来修改state
    • 通过dispatch来派发action
    • 通常action中都会有type属性,也可以携带其他的数据;
// 修改store中的数据: 必须action
const nameAction = { type: "change_name", name: "kobe" }
store.dispatch(nameAction)
//store.getState()是 Redux store 中的一个方法,用于获取当前存储的完整 state 树。
//它返回的是一个对象,该对象包含整个应用程序的状态。
//因为 Redux 的状态是不可变的,所以你不能改变这个对象,只能读取它。
console.log(store.getState())
  1. 修改reducer中的处理代码
    • 这里一定要记住,reducer是一个纯函数,不需要直接修改state

    • 后面我会讲到直接修改state带来的问题;

      • 因为reducer函数被设计为纯函数,它们的行为应该只依赖于它们的输入参数,而不依赖于外部环境的状态。如果在reducer函数中直接修改了state对象,意味着函数的输出结果不仅仅取决于输入参数,还依赖于外部的状态,这破坏了纯函数的定义,违反了Reduxreducer的设计思想。因此,不要直接修改state参数,而是通过返回一个新的state对象来实现状态更新。

假设有以下的reducer函数代码:

function reducer(state, action) {
  state.count++;
  return state;
}

在上面的reducer函数中,它直接修改了state对象的count属性,然后直接返回state对象。这种方式修改state破坏了纯函数的定义,并且也不遵守Redux对reducer函数的约束。这是因为函数的输出结果不仅仅取决于输入参数(即stateaction),还依赖于外部状态(state对象中的count属性)。由于这种方式修改state对象后会导致state对象被改变,因此它可能会影响到Redux store中其他部分的状态,从而导致后续的状态更新出现错误。正确的做法应该是返回一个新的state对象,而不是直接修改state对象,代码应该是这样的:

function reducer(state, action) {
  return {
    ...state,
    count: state.count + 1
  };
}

在这种方式下,reducer函数成为了纯函数,它们的行为只依赖于它们的输入参数,即stateaction,而不依赖于外部的状态。因此,这种方式保证了Redux store的状态的可预测性和可控性。

// 定义reducer函数: 纯函数
// 两个参数: 
// 参数一: store中目前保存的state
// 参数二: 本次需要更新的action(dispatch传入的action)
// 返回值: 它的返回值会作为store之后存储的state
function reducer(state = initialState, action) {
  switch(action.type) {
    case "change_name":
      return { ...state, name: action.name }
    case "add_number":
      return { ...state, counter: state.counter + action.num }
    default:
      return state
  }
}
  1. 可以在派发action之前,监听store的变化:
const unsubscribe = store.subscribe(() => {
  console.log("订阅数据的变化:", store.getState())
})


// 修改store中的数据: 必须action
store.dispatch({ type: "change_name", name: "kobe" })
store.dispatch({ type: "change_name", name: "lilei" })

unsubscribe()//取消订阅

Redux结构划分,每个文件是什么作用?

redux代码优化:

1.将派发的action生成过程放到一个actionCreators函数中

2.将定义的所有actionCreators的函数, 放到一个独立的文件中: actionCreators.js

3.actionCreators和reducer函数中使用字符串常量是一致的, 所以将常量抽取到一个独立constants的文件中

4.将reducer和默认值(initialState)放到一个独立的reducer.js文件中, 而不是在index.js

  • store -- index.js
const { createStore } = require("redux")
const reducer =  require("./reducer.js")

// 创建的store
const store = createStore(reducer)

module.exports = store
  • reducer -- reducer.js
const { ADD_NUMBER, CHANGE_NAME } = require("./constants")

// 初始化的数据
const initialState = {
  name: "kobe",
  counter: 100
}

function reducer(state = initialState, action) {
  switch(action.type) {
    case CHANGE_NAME:
      return { ...state, name: action.name }
    case ADD_NUMBER:
      return { ...state, counter: state.counter + action.num }
    default:
      return state
  }
}

module.exports = reducer
  • action -- actionCreators.js
const { ADD_NUMBER, CHANGE_NAME } = require("./constants")

const changeNameAction = (name) => ({
  type: CHANGE_NAME,
  name
})

const addNumberAction = (num) => ({
  type: ADD_NUMBER,
  num
})


module.exports = {
  changeNameAction,
  addNumberAction
}
  • constants -- constants.js
const ADD_NUMBER = "add_number"
const CHANGE_NAME = "change_name"

module.exports = {
  ADD_NUMBER,
  CHANGE_NAME
}

Redux使用流程

Redux官方图

redux如何和react结合在一起?如何共享数据,如何进行action操作?

  1. redux和react结合
    • 核心代码主要是两个:
      • componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;
      • 在发生点击事件时,调用storedispatch来派发对应的action
export class Home extends PureComponent {  
    constructor() {  
        super()

        this.state = {
          // 初始化状态
          counter: store.getState().counter, // 初始化为 store 中的 counter 状态

          message: "Hello World", // 一些其他状态
          friends: [
            {id: 111, name: "why"},
            {id: 112, name: "kobe"},
            {id: 113, name: "james"},
          ]
        }
    }

componentDidMount() {  
    // 组件挂载后监听 store 中状态变化,更新组件状态  
    store.subscribe(() => {  
    const state = store.getState() // 获取最新的状态  
    this.setState({ counter: state.counter }) // 更新 counter 状态  
    })  
}

addNumber(num) {  
    // 点击按钮后触发的事件,更新 store 中的状态  
    store.dispatch(addNumberAction(num))  
}

render() {  
    const { counter } = this.state // 获取当前组件的状态

    return (
      <div>
        <h2>Home Counter: {counter}</h2> // 渲染状态,显示当前 counter 值
        <div>
          <button onClick={e => this.addNumber(1)}>+1</button> // 点击按钮更新 store 中的状态
          <button onClick={e => this.addNumber(5)}>+5</button>
          <button onClick={e => this.addNumber(8)}>+8</button>
        </div>
      </div>
    )
}
  1. react-redux使用 - connect、Provider这些帮助我们完成连接redux、react
  • Provider

    1. <React.StrictMode>:这是一个在开发模式下运行时的辅助工具,它可以帮助你发现应用中潜在的问题。它会执行额外的检查并警告一些不推荐的用法。在生产环境中,它不会有任何影响。在这个例子中,它包裹了其他组件,以启用严格模式。
    2. <Provider store={store}>:这是一个 React Redux 库提供的组件,它允许将 Redux store 中的数据传递给组件树中的所有组件。store 是一个包含应用程序状态的 Redux store 对象,它通过这个组件被传递给了应用程序。这样,所有的组件都可以通过连接到 Redux store 来访问和更新数据。
    3. <App />:这是你的应用程序的根组件。它可能包含其他组件,形成一个组件树。在这个例子中,根组件被包裹在 <Provider> 中,这样它的子组件就可以通过连接到 Redux store 来获取状态和派发操作。

    当调用 root.render() 并传递上述 JSX 元素作为参数时,React 将会把这个元素及其子组件渲染到根节点中,从而启动你的应用程序。

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
  • connect
connect()//返回值是一个高阶组件
function mapStateToProps(state) {
  return {
    counter: state.counter
  }
}

function fn2(dispatch) {
  return {
    addNumber(num) {
      dispatch(addNumberAction(num))
    },
    subNumber(num) { 
      dispatch(subNumberAction(num))
    }
  }
}

// mapStateToProps 函数将 Redux store 中的状态映射到组件的 props 上
const mapStateToProps = (state) => ({
  counter: state.counter, anners prop recommends: state.recommends // 将 state 中的 recommends 属性映射为组件的 recommends prop })// 将 state 中的 counter 属性映射为组件的 counter prop
  banners: state.banners, // 将 state 中的 banners 属性映射为组件的 banners prop
  recommends: state.recommends // 将 state 中的 recommends 属性映射为组件的 recommends prop
})

// mapDispatchToProps 函数将 dispatch 方法映射到组件的 props 上
const mapDispatchToProps = (dispatch) => ({
  addNumber(num) {
    dispatch(addNumberAction(num)) // 在组件中调用 addNumber 方法时,将触发 addNumberAction 并将其派发到 Redux store
  },
  subNumber(num) {
    dispatch(subNumberAction(num)) // 在组件中调用 subNumber 方法时,将触发 subNumberAction 并将其派发到 Redux store
  }
})

// 使用 connect 高阶函数将 mapStateToProps 和 mapDispatchToProps 的结果与 About 组件连接起来
export default connect(mapStateToProps, mapDispatchToProps)(About)

上述代码段展示了使用 react-redux 库中的 connect 函数将 Redux 的状态和操作映射到组件的过程。mapStateToProps 函数将 Redux store 中的状态映射为组件的 props,而 mapDispatchToProps 函数将 dispatch 方法映射为组件的 props,以便在组件中调用相应的操作时触发 Redux 的 action。最后,通过将 mapStateToPropsmapDispatchToProps 的结果与 About 组件连接起来,我们将得到一个新的通过 Redux 管理状态的 About 组件。

直接在props中解构或者使用

 calcNumber(num, isAdd) {
    if (isAdd) {
      console.log("加", num)
      this.props.addNumber(num)
    } else {
      console.log("减", num)
      this.props.subNumber(num)
    }
  }

  render() {
    const { counter, banners, recommends } = this.props
  }