React 进阶 - 渲染控制

2023-05-17 20:47:56 浏览数 (1)

# React 渲染

对于 React 渲染,不要仅仅理解成类组件触发 render 函数,函数组件本身执行,事实上,从调度更新任务到调和 fiber,再到浏览器渲染真实 DOM,每一个环节都是渲染的一部分,至于对于每个环节的性能优化,React 在底层已经处理了大部分优化细节,包括设立任务优先级、异步调度、diff 算法、时间分片都是 React 为了提高性能,提升用户体验采取的手段。开发者只需要告诉 React 哪些组件需要更新,哪些组件不需要更新。

React 提供了 PureComponentshouldComponentUpdatedmemo 等优化手段。

# render 阶段

render 的作用是根据一次更新中产生的新状态值,通过 React.createElement ,替换成新的状态,得到新的 React element 对象,新的 element 对象上,保存了最新状态值。 createElement 会产生一个全新的 props。到此 render 函数使命完成了。

接下来,React 会调和由 render 函数产生 chidlren,将子代 element 变成 fiber(这个过程如果存在 alternate,会复用 alternate 进行克隆,如果没有 alternate ,那么将创建一个),将 props 变成 pendingProps ,至此当前组件更新完毕。然后如果 children 是组件,会继续重复上一步,直到全部 fiber 调和完毕。完成 render 阶段。

# React 控制 render 的方法

render 的控制,究其本质,主要有以下两种方式:

  1. 从父组件直接隔断子组件的渲染,经典的就是 memo,缓存 element 对象。
  2. 组件从自身来控制是否 render ,比如:PureComponentshouldComponentUpdate

# 缓存 React.element 对象

一种父对子的渲染控制方案,来源于一种情况,父组件 render ,子组件有没有必要跟着父组件一起 render ,如果没有必要,则就需要阻断更新流。

代码语言:javascript复制
/* Children */
function Child({ number }) {
  console.log("Child render")
  return <div>Child {number}</div>
}

/* Parent */
export default class Index extends React.Component {
  state = {
    numberA: 0,
    numberB: 0,
  }
  render() {
    return (
      <div>
        <Child number={this.state.numberA} />
        <button onClick={() => this.setState({ numberA: this.state.numberA   1 })}>
          numberA   1 {this.state.numberA}
        </button>
        <button onClick={() => this.setState({ numberB: this.state.numberB   1 })}>
          numberB   1 {this.state.numberB}
        </button>
      </div>
    )
  }
}

对于子组件 Child ,只有 propsnumberA 更新才是有用的, numberB 更新带来渲染,Child 根本不需要。但是如果不处理子组件的话,就会出现如下情况。无论改变 numberA 还是改变 numberB ,子组件都会重新渲染,显然这不是想要的结果。

代码语言:javascript复制
export default class Index extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      numberA: 0,
      numberB: 0,
    }
    this.component = <Child number={this.state.numberA} />
  }
  controlComponentRender = () => {
    const { props } = this.component
    // 只有当 props.number !== this.state.numberA 时,才会重新渲染
    if (props.number !== this.state.numberA) {
      return (this.component = React.cloneElement(this.component, { number: this.state.numberA }))
    }
    // 否则返回缓存的 component
    return this.component
  }

  render() {
    return (
      <div>
        {this.controlComponentRender()}
        <button onClick={() => this.setState({ numberA: this.state.numberA   1 })}>
          numberA   1 {this.state.numberA}
        </button>
        <button onClick={() => this.setState({ numberB: this.state.numberB   1 })}>
          numberB   1 {this.state.numberB}
        </button>
      </div>
    )
  }
}

可以在函数组件用 useMemo 达到同样的效果:

代码语言:javascript复制
export default function Index() {
  const [numberA, setNumberA] = useState(0)
  const [numberB, setNumberB] = useState(0)
  const component = useMemo(() => <Child number={numberA} />, [numberA])
  return (
    <div>
      {component}
      <button onClick={() => setNumberA(numberA   1)}>numberA   1 {numberA}</button>
      <button onClick={() => setNumberB(numberB   1)}>numberB   1 {numberB}</button>
    </div>
  )
}

# useMemo

用法

代码语言:javascript复制
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

  • computeExpensiveValue 是一个函数,返回值是一个计算的结果,这个结果会被缓存起来,直到 a 或者 b 发生变化
  • [a, b] 是一个数组,数组中的值是依赖项,只有当依赖项发生变化时,才会重新计算 computeExpensiveValue 的值
  • memoizedValue 是一个缓存的值,只有当依赖项发生变化时,才会重新计算 computeExpensiveValue 的值

原理

  • useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是依赖项发生变化时,会重新执行 create 函数,重新计算缓存值

应用

  • 可以缓存 element 对象,从而达到按条件渲染组件,优化性能的作用
  • 如果组件中不期望每次 render 都重新计算一些值,可以使用 useMemo 缓存这些值,从而避免不必要的计算
  • 可以把函数和属性缓存起来,作为 PureComponent 的绑定方法,或配合其他 Hooks 一起使用

# Pure Component

纯组件是一种发自组件本身的渲染优化策略,当开发类组件选择了继承 PureComponent ,就意味这要遵循其渲染规则。规则就是浅比较 stateprops 是否相等。

代码语言:javascript复制
/* 纯组件本身 */
class Children extends React.PureComponent {
  state = {
    name: "Cell",
    age: 18,
    obj: {
      num: 1,
    },
  }
  changeObjeNum = () => {
    const { obj } = this.state
    obj.num  
    this.setState({ obj })
  }
  render() {
    console.log("组件渲染")
    return (
      <div>
        <div>组件本身改变 state </div>
        <button onClick={() => this.setState({ name: "Cell" })}>state 相同</button>
        <button onClick={() => this.setState({ age: this.state.age   1 })}>state 不同</button>
        <button onClick={this.changeObjeNum}>state 为引用类型</button>
      </div>
    )
  }
}
/* 父组件 */
export default function Home() {
  const [numberA, setNumberA] = useState(0)
  const [numberB, setNumberB] = useState(0)
  return (
    <div>
      <div>父组件改变 props</div>
      <button onClick={() => setNumberA(numberA   1)}>改变传递给子组件的 state</button>
      <button onClick={() => setNumberB(numberB   1)}>改变不传递给子组件的 state</button>
      <Children number={numberA} />
    </div>
  )
}

  • 对于 propsPureComponent 会浅比较 props 是否发生改变,再决定是否渲染组件,所以只有点击 numberA 才会促使组件重新渲染
  • 对于 state ,也会浅比较处理,当上述触发 ‘ state 相同情况’ 按钮时,组件没有渲染
  • 浅比较只会比较基础数据类型,对于引用类型,比如 Demo 中 stateobj ,单纯的改变 obj 下属性是不会促使组件更新的,因为浅比较两次 obj 还是指向同一个内存空间

PureComponent 注意事项:

避免使用箭头函数

  • 不要给是 PureComponent 子组件绑定箭头函数,因为父组件每一次 render ,如果是箭头函数绑定的话,都会重新生成一个新的箭头函数, PureComponent 对比新老 props 时候,因为是新的函数,所以会判断不想等,而让组件直接渲染,PureComponent 作用终会失效

PureComponent 的父组件是函数组件的情况,绑定函数要用 useCallback 或者 useMemo 处理

  • 在用 class function 组件开发项目的时候,如果父组件是函数,子组件是 PureComponent ,那么绑定函数要小心,因为函数组件每一次执行,如果不处理,还会声明一个新的函数,所以 PureComponent 对比同样会失效
代码语言:javascript复制
export default function () {
  const callback = React.useCallback(function handlerCallback() {}, [])
  return <Child callback={callback} />
}

`useCallback` 和 `useMemo` 的区别

useCallback 第一个参数就是缓存的内容,useMemo 需要执行第一个函数,返回值为缓存的内容,比起 useCallbackuseMemo 更像是缓存了一段逻辑,或者说执行这段逻辑获取的结果。对于缓存 element 也可以用 useCallback

# shouldComponentUpdate

有时,把控制渲染,性能调优交给 React 组件本身处理显然是靠不住的,React 需要提供给使用者一种更灵活配置的自定义渲染方案,使用者可以自己决定是否更新当前组件,shouldComponentUpdate 就能达到这种效果。

代码语言:javascript复制
/* 子组件 */
class Index extends React.Component {
  state = {
    numA: 0,
    numB: 0,
  }
  shouldComponentUpdate(newProp, newState, newContext) {
    if (newProp.prposNumA !== this.props.prposNumA || newState.numA !== this.state.numA) {
      return true
    }
    return false
  }
  render() {
    console.log("组件渲染")
    const { numA, numB } = this.state
    return (
      <div>
        <button onClick={() => this.setState({ numA: numA   1 })}> 改变 numA</button>
        <button onClick={() => this.setState({ numB: numB   1 })}> 改变 numB</button>
        <div>Learn React</div>
      </div>
    )
  }
}

/* 父组件 */
export default function Home() {
  const [numberA, setNumberA] = useState(0)
  const [numberB, setNumberB] = useState(0)
  return (
    <div>
      <button onClick={() => setNumberA(numberA   1)}>改变传递给子组件的 state</button>
      <button onClick={() => setNumberB(numberB   1)}>改变传递给子组件的 state</button>
      <Index prposNumA={numberA} prposNumB={numberB} />
    </div>
  )
}

shouldComponentUpdate 可以根据传入的新的 propsstate ,或者 newContext 来确定是否更新组件。

有一种情况就是如果子组件的 props 是引用数据类型,比如 object ,还是不能直观比较是否相等。那么如果想有对比新老属性相等,怎么对比呢,而且很多情况下,组件中数据可能来源于服务端交互,对于属性结构是未知的。

immutable.js 可以解决此问题,immutable.js 不可变的状态,对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。鉴于这个功能,所以可以把需要对比的 props 或者 state 数据变成 Immutable 对象,通过对比 Immutable 是否相等,来证明状态是否改变,从而确定是否更新组件。

# React.memo

代码语言:javascript复制
React.memo(Component, compare)

React.memo 可作为一种容器化的控制渲染方案,可以对比 props 变化,来决定是否渲染组件。

  • 参数
    • Component 原始组件本身
    • compare 是一个函数,可以根据一次更新中 props 是否相同决定原始组件是否重新渲染
  • 特点
    • React.memo: 第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染
      • shouldComponentUpdate 相反: 返回 true 组件渲染 , 返回 false 组件不渲染
    • memo 当二个参数 compare 不存在时,会用浅比较原则处理 props ,相当于仅比较 props 版本的 pureComponent
    • memo 同样适合类组件和函数组件

# 打破渲染限制

  • forceUpdate
    • 类组件更新如果调用的是 forceUpdate 而不是 setState ,会跳过 PureComponent 的浅比较和 shouldComponentUpdate 自定义比较
    • 原理是组件中调用 forceUpdate 时候,全局会开启一个 hasForceUpdate 的开关。当组件更新的时候,检查这个开关是否打开,如果打开,就直接跳过 shouldUpdate
  • context 穿透
    • 上述的几种方式,都不能本质上阻断 context 改变,而带来的渲染穿透,所以开发者在使用 context 要格外小心,既然选择了消费 context ,就要承担 context 改变,带来的更新作用

# 渲染控制流程图

# render 注意点

# 有没有必要在乎组件不必要渲染

在正常情况下,无须过分在乎 React 没有必要的渲染,要理解执行 render 不等于真正的浏览器渲染视图,render 阶段执行是在 js 当中,js 中运行代码远快于浏览器的 Rendering 和 Painting 的,更何况 React 还提供了 diff 算法等手段,去复用真实 DOM 。

# 什么时候需要注意渲染节流

对于以下情况,需要采用渲染节流:

  • 数据可视化的模块组件(展示了大量的数据)
    • 一次更新,可能伴随大量的 diff ,数据量越大也就越浪费性能
    • 对于数据展示模块组件,有必要采取 memoshouldComponentUpdate 等方案控制自身组件渲染
  • 含有大量表单的页面
    • React 一般会采用受控组件的模式去管理表单数据层,表单数据层完全托管于 props 或是 state ,而用户操作表单往往是频繁的,需要频繁改变数据层,所以很有可能让整个页面组件高频率 render
  • 越是靠近 app root 根组件越值得注意
    • 根组件渲染会波及到整个组件树重新 render ,子组件 render ,一是浪费性能,二是可能执行 useEffectcomponentWillReceiveProps 等钩子,造成意想不到的情况发生

# 开发注意点

  • 对于大量数据展示的模块,有必要用 shouldComponentUpdatePureComponent 来优化性能
  • 对于表单控件,最好办法单独抽离组件,独自管理自己的数据层,这样可以让 state 改变,波及的范围更小
  • 如果需要更精致化渲染,可以配合 immutable.js
  • 组件颗粒化,配合 memo 等 api ,可以制定私有化的渲染空间

0 人点赞