react-redux英雄指南

341 阅读7分钟

1.2.5. react-redux

阐述react-redux的作用

  • 为了方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux
  • React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。
    • UI 组件有以下几个特征。
      • 只负责 UI 的呈现,不带有任何业务逻辑
      • 没有状态(即不使用this.state这个变量)
      • 所有数据都由参数(this.props)提供
      • 不使用任何 Redux 的 API
    • 容器组件的特征恰恰相反。
      • 负责管理数据和业务逻辑,不负责 UI 的呈现
      • 带有内部状态
      • 使用 Redux 的 API
  • React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

阐述Provider,connect,useSelector相关API的用法

<Provider>组件

  • connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。
  • 一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。
  • React-Redux 提供Provider组件,可以让容器组件拿到state。
    import { Provider } from 'react-redux'
    import { createStore } from 'redux'
    import todoApp from './reducers'
    import App from './components/App'
    
    let store = createStore(todoApp);
    
    render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )
    
  • 上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。它的原理是React组件的context属性。 connect
  • React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。
import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
  • TodoList是 UI 组件,VisibleTodoList就是由 React-Redux 通过connect方法自动生成的容器组件。
  • 为了定义业务逻辑,需要给出下面两方面的信息。
    • (1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数
    • (2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。
  • connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。 useSelector
  • 作用:共享状态,从Redux的store中提取数据(state)
  • const num=useSelector(state=>state.num)

阐述store变化到组件更新的整个流程

当store变化时,刷新组件的props,触发组件的render方法,实现组件的更新

如何避免store变化时无关组件频繁渲染

  • PureComponentReact.memo

在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。从 React 的声明式设计理念来看,如果子组件的 Props 和 State 都没有改变,那么其生成的 DOM 结构和副作用也不应该发生改变。当子组件符合声明式设计理念时,就可以忽略子组件本次的 Render 过程。PureComponent 和 React.memo 就是应对这种场景的,PureComponent 是对类组件的 Props 和 State 进行浅比较,React.memo 是对函数组件的 Props 进行浅比较。

  • shouldComponentUpdate

    • 这个生命周期在组件被重新渲染前被调用,这个函数的返回值确定了组件是否需要重新渲染。函数默认返回true,表示组件的props或者state修改,就会重新构建virtual dom,然后使用diff算法进行比较,根据比较结果来决定是否需要重新渲染组件。如果返回false则表示,不需要重新渲染
    • 在项目初始阶段,开发者往往图方便会给子组件传递一个大对象作为 Props,后面子组件想用啥就用啥。当大对象中某个「子组件未使用的属性」发生了更新,子组件也会触发 Render 过程。在这种场景下,通过实现子组件的 shouldComponentUpdate 方法,仅在「子组件使用的属性」发生改变时才返回 true,便能避免子组件重新 Render。
    • 但使用 shouldComponentUpdate 优化第二个场景有两个弊端
      1. 如果存在很多子孙组件,「找出所有子孙组件使用的属性」就会有很多工作量,也容易因为漏测导致 bug。
      2. 存在潜在的工程隐患
  • useMemouseCallback 实现稳定的 Props 值 如果传给子组件的派生状态或函数,每次都是新的引用,那么 PureComponentReact.memo 优化就会失效。所以需要使用 useMemouseCallback 来生成稳定值,并结合 PureComponentReact.memo 避免子组件重新 Render。

  • 发布者订阅者跳过中间组件 Render 过程

    • React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上,但将状态放在公共祖先上后,该状态就需要层层向下传递,直到传递给使用该状态的组件为止。
    • 每次状态的更新都会涉及中间组件的 Render 过程,但中间组件并不关心该状态,它的 Render 过程只负责将该状态再传给子组件。在这种场景下可以将状态用发布者订阅者模式维护,只有关心该状态的组件才去订阅该状态,不再需要中间组件传递该状态。当状态更新时,发布者发布数据更新消息,只有订阅者组件才会触发 Render 过程,中间组件不再执行 Render 过程。
    • 只要是发布者订阅者模式的库,都可以进行该优化。比如:redux、use-global-state、React.createContext 等。
  • 状态下放,缩小状态影响范围

如果一个状态只在某部分子树中使用,那么可以将这部分子树提取为组件,并将该状态移动到该组件内部。如下面的代码所示,虽然状态 color 只在 <input /><p /> 中使用,但 color 改变会引起 <ExpensiveTree /> 重新 Render。

import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  )
}

function ExpensiveTree() {
  let now = performance.now()
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>
}

通过将 color 状态、<input /><p /> 提取到组件 Form 中

  • 列表项使用 key 属性

    • 推荐使用 ID 作为每项的 key 值。其原因有两:
      • 在列表中执行删除、插入、排序列表项的操作时,使用 ID 作为 key 将更高效。而翻页操作往往伴随着 API 请求,DOM 操作耗时远小于 API 请求耗时,是否使用 ID 在该场景下对用户体验影响不大。
      • 使用 ID 做为 key 可以维护该 ID 对应的列表项组件的 State。举个例子,某表格中每列都有普通态和编辑态两个状态,起初所有列都是普通态,用户点击第一行第一列,使其进入编辑态。然后用户又拖拽第二行,将其移动到表格的第一行。如果开发者使用索引作为 key,那么第一行第一列的状态仍然为编辑态,而用户实际希望编辑的是第二行的数据,在用户看来就是不符合预期的。尽管这个问题可以通过将「是否处于编辑态」存放在数据项的数据中,利用 Props 来解决,但是使用 ID 作为 key 不是更香吗?
  • useMemo 返回虚拟 DOM

    • 利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 类似,但与 React.memo 相比有以下优势:
      1. 更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。
      2. 更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,也可使用 useDeepCompareMemo 对用到的值进行深比较。
  • 跳过回调函数改变触发的Render过程

    • React 组件的 Props 可以分为两类。
      • a) 一类是在对组件 Render 有影响的属性,如:页面数据、getPopupContainer 和 renderProps 函数。
      • b) 另一类是组件 Render 后的回调函数,如:onClick、onVisibleChange。
      • b) 类属性并不参与到组件的 Render 过程,因此可以对 b) 类属性进行优化。当 b)类属性发生改变时,不触发组件的重新 Render ,而是在回调触发时调用最新的回调函数。
  • Hooks 按需更新

如果自定义 Hook 暴露多个状态,而调用方只关心某一个状态,那么其他状态改变就不应该触发组件重新 Render。