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
- UI 组件有以下几个特征。
- 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方法接受两个参数:
mapStateToProps
和mapDispatchToProps
。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。useSelector
- 作用:共享状态,从Redux的store中提取数据(state)
const num=useSelector(state=>state.num)
阐述store变化到组件更新的整个流程
当store变化时,刷新组件的props,触发组件的render方法,实现组件的更新
如何避免store变化时无关组件频繁渲染
PureComponent
、React.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 优化第二个场景有两个弊端
- 如果存在很多子孙组件,「找出所有子孙组件使用的属性」就会有很多工作量,也容易因为漏测导致 bug。
- 存在潜在的工程隐患
-
useMemo
、useCallback
实现稳定的 Props 值 如果传给子组件的派生状态或函数,每次都是新的引用,那么PureComponent
和React.memo
优化就会失效。所以需要使用useMemo
和useCallback
来生成稳定值,并结合PureComponent
或React.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 不是更香吗?
- 推荐使用 ID 作为每项的 key 值。其原因有两:
-
useMemo 返回虚拟 DOM
- 利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 类似,但与 React.memo 相比有以下优势:
- 更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。
- 更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,也可使用 useDeepCompareMemo 对用到的值进行深比较。
- 利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 类似,但与 React.memo 相比有以下优势:
-
跳过回调函数改变触发的Render过程
- React 组件的 Props 可以分为两类。
- a) 一类是在对组件 Render 有影响的属性,如:页面数据、getPopupContainer 和 renderProps 函数。
- b) 另一类是组件 Render 后的回调函数,如:onClick、onVisibleChange。
- b) 类属性并不参与到组件的 Render 过程,因此可以对 b) 类属性进行优化。当 b)类属性发生改变时,不触发组件的重新 Render ,而是在回调触发时调用最新的回调函数。
- React 组件的 Props 可以分为两类。
-
Hooks 按需更新
如果自定义 Hook 暴露多个状态,而调用方只关心某一个状态,那么其他状态改变就不应该触发组件重新 Render。