关于前端:React-中的重新渲染

12次阅读

共计 12757 个字符,预计需要花费 32 分钟才能阅读完成。

作者:梁瑞锋 (晓玉)

缘起

React 从新渲染,指的是在类函数中,会从新执行 render 函数,相似 Flutter 中的 build 函数,函数组件中,会从新执行这个函数

React 组件在组件的状态 state 或者组件的属性 props 扭转的时候,会从新渲染,条件简略,然而实际上稍不留神,会引起灾难性的从新渲染

类组件

为什么拿类组件先说,怎么说呢,更好了解?还有前几年比拟风行的一些常见面试题

React 中的 setState 什么时候是同步的,什么时候是异步的

React setState 怎么获取最新的 state

以下代码的输入值是什么,页面展现是怎么变动的

  test = () => {
    // s1 = 1
    const {s1} = this.state;
    this.setState({s1: s1 + 1});
    this.setState({s1: s1 + 1});
    this.setState({s1: s1 + 1});
    console.log(s1)
  };

  render() {
    return (
      <div>
        <button onClick={this.test}> 按钮 </button>
        <div>{this.state.s1}</div>
      </div>
    );
  }

看到这些类型的面试问题,相熟 React 事务机制的你肯定能答出来,毕竟不难嘛,哈?你不晓得 React 的事务机制?百度 | 谷歌 |360| 搜狗 | 必应 React 事务机制

React 合成事件

React 组件触发的事件会被冒泡到 document(在 react v17 中是 react 挂载的节点,例如 document.querySelector(‘#app’)),而后 React 依照触发门路上收集事件回调,散发事件。

  • 这里是不是突发奇想,如果禁用了,在触发事件的节点,通过原生事件禁止事件冒泡,是不是 React 事件就没法触发了?的确是这样,没法冒泡了,React 都没法收集事件和散发事件了,留神这个冒泡不是 React 合成事件的冒泡。
  • 发散一下还能想到的另外一个点,React,就算是在合成捕捉阶段触发的事件,仍旧在原生冒泡事件触发之后
reactEventCallback = () => {
  // s1 s2 s3 都是 1
  const {s1, s2, s3} = this.state;
  this.setState({s1: s1 + 1});
  this.setState({s2: s2 + 1});
  this.setState({s3: s3 + 1});
  console.log('after setState s1:', this.state.s1);
  // 这里仍旧输入 1,页面展现 2,页面仅从新渲染一次
};

<button
  onClick={this.reactEventCallback}
  onClickCapture={this.reactEventCallbackCapture}
>
  React Event
</button>
<div>
  S1: {s1} S2: {s2} S3: {s3}
</div>

定时器回调后触发 setState

定时器回调执行 setState 是同步的,能够在执行 setState 之后间接获取,最新的值,例如上面代码

timerCallback = () => {setTimeout(() => {
    // s1 s2 s3 都是 1
    const {s1, s2, s3} = this.state;
    this.setState({s1: s1 + 1});
    console.log('after setState s1:', this.state.s1);
    // 输入 2 页面渲染 3 次
    this.setState({s2: s2 + 1});
    this.setState({s3: s3 + 1});
  });
};

异步函数后调触发 setState

异步函数回调执行 setState 是同步的,能够在执行 setState 之后间接获取,最新的值,例如上面代码

asyncCallback = () => {Promise.resolve().then(() => {
    // s1 s2 s3 都是 1
    const {s1, s2, s3} = this.state;
    this.setState({s1: s1 + 1});
    console.log('after setState s1:', this.state.s1);
    // 输入 2 页面渲染 3 次
    this.setState({s2: s2 + 1});
    this.setState({s3: s3 + 1});
  });
};

原生事件触发

原生事件同样不受 React 事务机制影响,所以 setState 体现也是同步的

componentDidMount() {const btn1 = document.getElementById('native-event');
  btn1?.addEventListener('click', this.nativeCallback);
}

nativeCallback = () => {
  // s1 s2 s3 都是 1
  const {s1, s2, s3} = this.state;
  this.setState({s1: s1 + 1});
  console.log('after setState s1:', this.state.s1);
  // 输入 2 页面渲染 3 次
  this.setState({s2: s2 + 1});
  this.setState({s3: s3 + 1});
};


<button id="native-event">Native Event</button>

setState 批改不参加渲染的属性

setState 调用就会引起就会组件从新渲染,即便这个状态没有参加页面渲染,所以,请不要把非渲染属性放 state 外面,即便放了 state,也请不要通过 setState 去批改这个状态,间接调用 this.state.xxx = xxx 就好,这种不参加渲染的属性,间接挂在 this 上就好,参考下图

// s1 s2 s3 为渲染的属性,s4 非渲染属性
state = {
  s1: 1,
  s2: 1,
  s3: 1,
  s4: 1,
};

s5 = 1;

changeNotUsedState = () => {const { s4} = this.state;
  this.setState({s4: s4 + 1});
  // 页面会从新渲染

  // 页面不会从新渲染
  this.state.s4 = 2;
  this.s5 = 2;
};

<div>
  S1: {s1} S2: {s2} S3: {s3}
</div>;

只是调用 setState,页面会不会从新渲染

几种状况,别离是:

  • 间接调用 setState,无参数
  • setState,新 state 和老 state 完全一致,也就是同样的 state
sameState = () => {const { s1} = this.state;
  this.setState({s1});
  // 页面会从新渲染
};

noParams = () => {this.setState({});
  // 页面会从新渲染
};

这两种状况,解决起来和一般的批改状态的 setState 统一,都会引起从新渲染的

屡次渲染的问题

为什么要提下面这些,认真看,这里提到了很屡次渲染的 3 次,比拟符合咱们日常写代码的,异步函数回调,毕竟在定时器回调或者给组件绑定原生事件(没事找事是吧?),挺少这么做的吧,然而异步回调就很多了,比方网络申请啥的,扭转个 state 还是挺常见的,然而渲染屡次,就是不行!不过利用 setState 实际上是传一个新对象合并机制,能够把变动的属性合并在新的对象外面,一次性提交全副变更,就不必调用屡次 setState

asyncCallbackMerge = () => {Promise.resolve().then(() => {const { s1, s2, s3} = this.state;
    this.setState({s1: s1 + 1, s2: s2 + 1, s3: s3 + 1});
    console.log('after setState s1:', this.state.s1);
    // 输入 2 页面渲染 1 次
  });
};

这样就能够在非 React 的事务流中避开屡次渲染的问题

测试代码

import React from 'react';

interface State {
  s1: number;
  s2: number;
  s3: number;
  s4: number;
}

// eslint-disable-next-line @iceworks/best-practices/recommend-functional-component
export default class TestClass extends React.Component<any, State> {
  renderTime: number;
  constructor(props: any) {super(props);
    this.renderTime = 0;
    this.state = {
      s1: 1,
      s2: 1,
      s3: 1,
      s4: 1,
    };
  }

  componentDidMount() {const btn1 = document.getElementById('native-event');
    const btn2 = document.getElementById('native-event-async');
    btn1?.addEventListener('click', this.nativeCallback);
    btn2?.addEventListener('click', this.nativeCallbackMerge);
  }

  changeNotUsedState = () => {const { s4} = this.state;
    this.setState({s4: s4 + 1});
  };

  reactEventCallback = () => {const { s1, s2, s3} = this.state;
    this.setState({s1: s1 + 1});
    this.setState({s2: s2 + 1});
    this.setState({s3: s3 + 1});
    console.log('after setState s1:', this.state.s1);
  };
  timerCallback = () => {setTimeout(() => {const { s1, s2, s3} = this.state;
      this.setState({s1: s1 + 1});
      console.log('after setState s1:', this.state.s1);
      this.setState({s2: s2 + 1});
      this.setState({s3: s3 + 1});
    });
  };
  asyncCallback = () => {Promise.resolve().then(() => {const { s1, s2, s3} = this.state;
      this.setState({s1: s1 + 1});
      console.log('after setState s1:', this.state.s1);
      this.setState({s2: s2 + 1});
      this.setState({s3: s3 + 1});
    });
  };
  nativeCallback = () => {const { s1, s2, s3} = this.state;
    this.setState({s1: s1 + 1});
    console.log('after setState s1:', this.state.s1);
    this.setState({s2: s2 + 1});
    this.setState({s3: s3 + 1});
  };
  timerCallbackMerge = () => {setTimeout(() => {const { s1, s2, s3} = this.state;
      this.setState({s1: s1 + 1, s2: s2 + 1, s3: s3 + 1});
      console.log('after setState s1:', this.state.s1);
    });
  };
  asyncCallbackMerge = () => {Promise.resolve().then(() => {const { s1, s2, s3} = this.state;
      this.setState({s1: s1 + 1, s2: s2 + 1, s3: s3 + 1});
      console.log('after setState s1:', this.state.s1);
    });
  };
  nativeCallbackMerge = () => {const { s1, s2, s3} = this.state;
    this.setState({s1: s1 + 1, s2: s2 + 1, s3: s3 + 1});
    console.log('after setState s1:', this.state.s1);
  };
  sameState = () => {const { s1, s2, s3} = this.state;
    this.setState({s1});
    this.setState({s2});
    this.setState({s3});
    console.log('after setState s1:', this.state.s1);
  };
  withoutParams = () => {this.setState({});
  };

  render() {console.log('renderTime', ++this.renderTime);
    const {s1, s2, s3} = this.state;
    return (
      <div className="test">
        <button onClick={this.reactEventCallback}>React Event</button>
        <button onClick={this.timerCallback}>Timer Callback</button>
        <button onClick={this.asyncCallback}>Async Callback</button>
        <button id="native-event">Native Event</button>
        <button onClick={this.timerCallbackMerge}>Timer Callback Merge</button>
        <button onClick={this.asyncCallbackMerge}>Async Callback Merge</button>
        <button id="native-event-async">Native Event Merge</button>
        <button onClick={this.changeNotUsedState}>Change Not Used State</button>
        <button onClick={this.sameState}>React Event Set Same State</button>
        <button onClick={this.withoutParams}>
          React Event SetState Without Params
        </button>
        <div>
          S1: {s1} S2: {s2} S3: {s3}
        </div>
      </div>
    );
  }
}

函数组件

函数组件从新渲染的条件也和类组件一样,组件的属性 Props 和组件的状态 State 有批改的时候,会触发组件从新渲染,所以类组件存在的问题,函数组件同样也存在,而且因为函数组件的 state 不是一个对象,状况就更蹩脚

React 合成事件

const reactEventCallback = () => {
  // S1 S2 S3 都是 1
  setS1((i) => i + 1);
  setS2((i) => i + 1);
  setS3((i) => i + 1);
  // 页面只会渲染一次,S1 S2 S3 都是 2
};

定时器回调

const timerCallback = () => {setTimeout(() => {
    // S1 S2 S3 都是 1
    setS1((i) => i + 1);
    setS2((i) => i + 1);
    setS3((i) => i + 1);
    // 页面只会渲染三次,S1 S2 S3 都是 2
  });
};

异步函数回调

const asyncCallback = () => {Promise.resolve().then(() => {
    // S1 S2 S3 都是 1
    setS1((i) => i + 1);
    setS2((i) => i + 1);
    setS3((i) => i + 1);
    // 页面只会渲染三次,S1 S2 S3 都是 2
  });
};

原生事件

useEffect(() => {const handler = () => {
    // S1 S2 S3 都是 1
    setS1((i) => i + 1);
    setS2((i) => i + 1);
    setS3((i) => i + 1);
    // 页面只会渲染三次,S1 S2 S3 都是 2
  };
  containerRef.current?.addEventListener('click', handler);
  return () => containerRef.current?.removeEventListener('click', handler);
}, []);

更新没应用的状态

const [s4, setS4] = useState<number>(1);
const unuseState = () => {setS4((s) => s + 1);
  // s4 === 2 页面渲染一次 S4 页面上没用到
};

总结

以上的全副状况,在 React Hook 中体现的状况和类组件体现完全一致,没有任何差异,然而也有体现不统一的中央

不同的状况 设置同样的 State

React Hook 中设置同样的 State,并不会引起从新渲染,这点和类组件不一样,然而这个不肯定的,援用 React 官网文档说法

如果你更新 State Hook 后的 state 与以后的 state 雷同时,React 将跳过子组件的渲染并且不会触发 effect 的执行。(React 应用 Object.is 比拟算法 来比拟 state。)

须要留神的是,React 可能仍须要在跳过渲染前渲染该组件。不过因为 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必放心。如果你在渲染期间执行了高开销的计算,则能够应用 useMemo 来进行优化。

官网稳固有提到,新旧 State 浅比拟完全一致是不会从新渲染的,然而有可能还是会导致从新渲染

// React Hook
const sameState = () => {setS1((i) => i);
  setS2((i) => i);
  setS3((i) => i);
  console.log(renderTimeRef.current);
  // 页面并不会从新渲染
};

// 类组件中
sameState = () => {const { s1, s2, s3} = this.state;
  this.setState({s1});
  this.setState({s2});
  this.setState({s3});
  console.log('after setState s1:', this.state.s1);
  // 页面会从新渲染
};

这个个性存在,有些时候想要获取最新的 state,又不想给某个函数增加 state 依赖或者给 state 增加一个 useRef,能够通过这个函数去或者这个 state 的最新值

const sameState = () => {setS1((i) => {
    const latestS1 = i;
    // latestS1 是以后 S1 最新的值,能够在这里解决一些和 S1 相干的逻辑
    return latestS1;
  });
};

React Hook 中防止屡次渲染

React Hookstate 并不是一个对象,所以不会主动合并更新对象,那怎么解决这个异步函数之后屡次 setState 从新渲染的问题?

将全副 state 合并成一个对象

const [state, setState] = useState({s1: 1, s2: 1, s3: 1});
setState((prevState) => {setTimeout(() => {const { s1, s2, s3} = prevState;
    return {...prevState, s1: s1 + 1, s2: s2 + 1, s3: s3 + 1};
  });
});

参考类的的 this.state 是个对象的办法,把全副的 state 合并在一个组件外面,而后须要更新某个属性的时候,间接调用 setState 即可,和类组件的操作完全一致,这是一种计划

应用 useReducer

尽管这个 hook 的存在感的确低,然而多状态的组件用这个来代替 useState 的确不错

const initialState = {s1: 1, s2: 1, s3: 1};

function reducer(state, action) {switch (action.type) {
    case 'update':
      return {s1: state.s1 + 1, s2: state.s2 + 1, s3: state.s3 + 1};
    default:
      return state;
  }
}

const [reducerState, dispatch] = useReducer(reducer, initialState);
const reducerDispatch = () => {setTimeout(() => {dispatch({ type: 'update'});
  });
};

具体的用法不开展了,用起来和 redux 差异不大

状态间接用 Ref 申明,须要更新的时候调用更新的函数(不举荐)

// S4 不参加渲染
const [s4, setS4] = useState<number>(1);
// update 就是 useReducer 的 dispatch,调用就更更新页面,比定义一个不渲染的 state 好多了
const [, update] = useReducer((c) => c + 1, 0);
const state1Ref = useRef(1);
const state2Ref = useRef(1);

const unRefSetState = () => {
  // 优先更新 ref 的值
  state1Ref.current += 1;
  state2Ref.current += 1;
  setS4((i) => i + 1);
};

const unRefSetState = () => {
  // 优先更新 ref 的值
  state1Ref.current += 1;
  state2Ref.current += 1;
  update();};

<div>
  state1Ref: {state1Ref.current} state2Ref: {state2Ref.current}
</div>;

这样做,把真正渲染的 state 放到了 ref 外面,这样有个益处,就是函数外面不必申明这个 state 的依赖了,然而害处十分多,更新的时候必须说动调用 update,同时把 ref 用来渲染也比拟奇怪

自定义 Hook

自定义 Hook 如果在组件中应用,任何自定义 Hook 中的状态扭转,都会引起组件从新渲染,包含组件中没用到的,然而定义在自定义 Hook 中的状态

简略的例子,上面的自定义 hook,有 iddata 两个状态,id 甚至都没有导出,然而 id 扭转的时候,还是会导致援用这个 Hook 的组件从新渲染

// 一个简略的自定义 Hook,用来申请数据
const useDate = () => {const [id, setid] = useState<number>(0);
  const [data, setData] = useState<any>(null);

  useEffect(() => {fetch('申请数据的 URL')
      .then((r) => r.json())
      .then((r) => {
        // 组件从新渲染
        setid((i) => i + 1);
        // 组件再次从新渲染
        setData(r);
      });
  }, []);

  return data;
};

// 在组件中应用,即便只导出了 data,然而 id 变动,同时也会导致组件从新渲染,所以组件在获取到数据的时候,组件会从新渲染两次
const data = useDate();

测试代码

// use-data.ts
const useDate = () => {const [id, setid] = useState<number>(0);
  const [data, setData] = useState<any>(null);

  useEffect(() => {fetch('数据申请地址')
      .then((r) => r.json())
      .then((r) => {setid((i) => i + 1);
        setData(r);
      });
  }, []);

  return data;
};

import {useEffect, useReducer, useRef, useState} from 'react';
import useDate from './use-data';

const initialState = {s1: 1, s2: 1, s3: 1};

function reducer(state, action) {switch (action.type) {
    case 'update':
      return {s1: state.s1 + 1, s2: state.s2 + 1, s3: state.s3 + 1};
    default:
      return state;
  }
}

const TestHook = () => {const renderTimeRef = useRef<number>(0);
  const [s1, setS1] = useState<number>(1);
  const [s2, setS2] = useState<number>(1);
  const [s3, setS3] = useState<number>(1);
  const [s4, setS4] = useState<number>(1);
  const [, update] = useReducer((c) => c + 1, 0);
  const state1Ref = useRef(1);
  const state2Ref = useRef(1);
  const data = useDate();
  const [state, setState] = useState({s1: 1, s2: 1, s3: 1});
  const [reducerState, dispatch] = useReducer(reducer, initialState);
  const containerRef = useRef<HTMLButtonElement>(null);

  const reactEventCallback = () => {setS1((i) => i + 1);
    setS2((i) => i + 1);
    setS3((i) => i + 1);
  };

  const timerCallback = () => {setTimeout(() => {setS1((i) => i + 1);
      setS2((i) => i + 1);
      setS3((i) => i + 1);
    });
  };

  const asyncCallback = () => {Promise.resolve().then(() => {setS1((i) => i + 1);
      setS2((i) => i + 1);
      setS3((i) => i + 1);
    });
  };

  const unuseState = () => {setS4((i) => i + 1);
  };

  const unRefSetState = () => {
    state1Ref.current += 1;
    state2Ref.current += 1;
    setS4((i) => i + 1);
  };

  const unRefReducer = () => {
    state1Ref.current += 1;
    state2Ref.current += 1;
    update();};

  const sameState = () => {setS1((i) => i);
    setS2((i) => i);
    setS3((i) => i);
    console.log(renderTimeRef.current);
  };

  const mergeObjectSetState = () => {setTimeout(() => {setState((prevState) => {const { s1: prevS1, s2: prevS2, s3: prevS3} = prevState;
        return {...prevState, s1: prevS1 + 1, s2: prevS2 + 1, s3: prevS3 + 1};
      });
    });
  };

  const reducerDispatch = () => {setTimeout(() => {dispatch({ type: 'update'});
    });
  };

  useEffect(() => {const handler = () => {setS1((i) => i + 1);
      setS2((i) => i + 1);
      setS3((i) => i + 1);
    };
    containerRef.current?.addEventListener('click', handler);
    return () => containerRef.current?.removeEventListener('click', handler);
  }, []);

  console.log('render Time Hook', ++renderTimeRef.current);
  console.log('data', data);
  return (
    <div className="test">
      <button onClick={reactEventCallback}>React Event</button>
      <button onClick={timerCallback}>Timer Callback</button>
      <button onClick={asyncCallback}>Async Callback</button>
      <button id="native-event" ref={containerRef}>
        Native Event
      </button>
      <button onClick={unuseState}>Unuse State</button>
      <button onClick={sameState}>Same State</button>
      <button onClick={mergeObjectSetState}>Merge State Into an Object</button>
      <button onClick={reducerDispatch}>Reducer Dispatch</button>
      <button onClick={unRefSetState}>useRef As State With useState</button>
      <button onClick={unRefSetState}>useRef As State With useReducer</button>
      <div>
        S1: {s1} S2: {s2} S3: {s3}
      </div>
      <div>
        Merge Object S1: {state.s1} S2: {state.s2} S3: {state.s3}
      </div>
      <div>
        reducerState Object S1: {reducerState.s1} S2: {reducerState.s2} S3:{' '}
        {reducerState.s3}
      </div>
      <div>
        state1Ref: {state1Ref.current} state2Ref: {state2Ref.current}
      </div>
    </div>
  );
};

export default TestHook;

规定记不住怎么办?

下面列举了一大堆状况,然而这些规定难免会记不住,React 事务机制导致的两种齐全截然不然的从新渲染机制,的确让人感觉有点恶心,React 官网也留神到了,既然在事务流的中 setState 能够合并,那不在 React 事务流的回调,能不能也合并,答案是能够的,React 官网其实在 React V18 中,setState 能做到合并,即便在异步回调或者定时器回调或者原生事件绑定中,能够把测试代码间接丢 React V18 的环境中尝试,就算是下面列出的会屡次渲染的场景,也不会从新渲染屡次

具体能够看下这个地址

Automatic batching for fewer renders in React 18

然而,有了 React V18 最好也记录一下以上的规定,对于缩小渲染次数还是很有帮忙的

正文完
 0