0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

React重新渲染指的是什么

OSC开源社区 来源:阿里巴巴终端技术 作者:梁瑞锋(晓玉) 2022-11-23 09:59 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

缘起

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(
按钮
{this.state.s1}
); }

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

React 合成事件

在 React 组件触发的事件会被冒泡到 document(在 react v17 中是 react 挂载的节点,例如 document.querySelector('#app')),然后 React 按照触发路径上收集事件回调,分发事件。

这里是不是突发奇想,如果禁用了,在触发事件的节点,通过原生事件禁止事件冒泡,是不是 React 事件就没法触发了?确实是这样,没法冒泡了,React 都没法收集事件和分发事件了,注意这个冒泡不是 React 合成事件的冒泡。

发散一下还能想到的另外一个点,React ,就算是在合成捕获阶段触发的事件,依旧在原生冒泡事件触发之后

reactEventCallback=()=>{
//s1s2s3都是1
const{s1,s2,s3}=this.state;
this.setState({s1:s1+1});
this.setState({s2:s2+1});
this.setState({s3:s3+1});
console.log('aftersetStates1:',this.state.s1);
//这里依旧输出1,页面展示2,页面仅重新渲染一次
};


S1:{s1}S2:{s2}S3:{s3}

定时器回调后触发 setState

定时器回调执行 setState 是同步的,可以在执行 setState 之后直接获取,最新的值,例如下面代码

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

异步函数后调触发 setState

异步函数回调执行 setState 是同步的,可以在执行 setState 之后直接获取,最新的值,例如下面代码

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

原生事件触发

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

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

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


NativeEvent

setState 修改不参与渲染的属性

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

//s1s2s3为渲染的属性,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;
};

S1:{s1}S2:{s2}S3:{s3}
;

只是调用 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('aftersetStates1:',this.state.s1);
//输出2页面渲染1次
});
};

这样就可以在非 React 的事务流中避开多次渲染的问题

测试代码

importReactfrom'react';

interfaceState{
s1:number;
s2:number;
s3:number;
s4:number;
}

//eslint-disable-next-line@iceworks/best-practices/recommend-functional-component
exportdefaultclassTestClassextendsReact.Component{
renderTime:number;
constructor(props:any){
super(props);
this.renderTime=0;
this.state={
s1:1,
s2:1,
s3:1,
s4:1,
};
}

componentDidMount(){
constbtn1=document.getElementById('native-event');
constbtn2=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('aftersetStates1:',this.state.s1);
};
timerCallback=()=>{
setTimeout(()=>{
const{s1,s2,s3}=this.state;
this.setState({s1:s1+1});
console.log('aftersetStates1:',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('aftersetStates1:',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('aftersetStates1:',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('aftersetStates1:',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('aftersetStates1:',this.state.s1);
});
};
nativeCallbackMerge=()=>{
const{s1,s2,s3}=this.state;
this.setState({s1:s1+1,s2:s2+1,s3:s3+1});
console.log('aftersetStates1:',this.state.s1);
};
sameState=()=>{
const{s1,s2,s3}=this.state;
this.setState({s1});
this.setState({s2});
this.setState({s3});
console.log('aftersetStates1:',this.state.s1);
};
withoutParams=()=>{
this.setState({});
};

render(){
console.log('renderTime',++this.renderTime);
const{s1,s2,s3}=this.state;
return(

ReactEvent
TimerCallback
AsyncCallback
NativeEvent
TimerCallbackMerge
AsyncCallbackMerge
NativeEventMerge
ChangeNotUsedState
ReactEventSetSameState

ReactEventSetStateWithoutParams

S1:{s1}S2:{s2}S3:{s3}
); } }

函数组件

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

React 合成事件

constreactEventCallback=()=>{
//S1S2S3都是1
setS1((i)=>i+1);
setS2((i)=>i+1);
setS3((i)=>i+1);
//页面只会渲染一次,S1S2S3都是2
};

定时器回调

consttimerCallback=()=>{
setTimeout(()=>{
//S1S2S3都是1
setS1((i)=>i+1);
setS2((i)=>i+1);
setS3((i)=>i+1);
//页面只会渲染三次,S1S2S3都是2
});
};

异步函数回调

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

原生事件

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

更新没使用的状态

const[s4,setS4]=useState(1);
constunuseState=()=>{
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 浅比较完全一致是不会重新渲染的,但是有可能还是会导致重新渲染

//ReactHook
constsameState=()=>{
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('aftersetStates1:',this.state.s1);
//页面会重新渲染
};

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

constsameState=()=>{
setS1((i)=>{
constlatestS1=i;
//latestS1是当前S1最新的值,可以在这里处理一些和S1相关的逻辑
returnlatestS1;
});
};

React Hook 中避免多次渲染

React Hook 中 state 并不是一个对象,所以不会自动合并更新对象,那怎么解决这个异步函数之后多次 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 确实不错

constinitialState={s1:1,s2:1,s3:1};

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

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

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

状态直接用 Ref 声明,需要更新的时候调用更新的函数(不推荐)

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

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

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

state1Ref:{state1Ref.current}state2Ref:{state2Ref.current}
;

这样做,把真正渲染的 state 放到了 ref 里面,这样有个好处,就是函数里面不用声明这个 state 的依赖了,但是坏处非常多,更新的时候必须说动调用 update,同时把 ref 用来渲染也比较奇怪

自定义 Hook

自定义 Hook 如果在组件中使用,任何自定义 Hook 中的状态改变,都会引起组件重新渲染,包括组件中没用到的,但是定义在自定义 Hook 中的状态

简单的例子,下面的自定义 hook,有 id 和 data 两个状态, id 甚至都没有导出,但是 id 改变的时候,还是会导致引用这个 Hook 的组件重新渲染

//一个简单的自定义Hook,用来请求数据
constuseDate=()=>{
const[id,setid]=useState(0);
const[data,setData]=useState(null);

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

returndata;
};

//在组件中使用,即使只导出了data,但是id变化,同时也会导致组件重新渲染,所以组件在获取到数据的时候,组件会重新渲染两次
constdata=useDate();

测试代码

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

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

returndata;
};

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

constinitialState={s1:1,s2:1,s3:1};

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

constTestHook=()=>{
constrenderTimeRef=useRef(0);
const[s1,setS1]=useState(1);
const[s2,setS2]=useState(1);
const[s3,setS3]=useState(1);
const[s4,setS4]=useState(1);
const[,update]=useReducer((c)=>c+1,0);
conststate1Ref=useRef(1);
conststate2Ref=useRef(1);
constdata=useDate();
const[state,setState]=useState({s1:1,s2:1,s3:1});
const[reducerState,dispatch]=useReducer(reducer,initialState);
constcontainerRef=useRef(null);

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

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

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

constunuseState=()=>{
setS4((i)=>i+1);
};

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

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

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

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

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

useEffect(()=>{
consthandler=()=>{
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('renderTimeHook',++renderTimeRef.current);
console.log('data',data);
return(

ReactEvent
TimerCallback
AsyncCallback

NativeEvent

UnuseState
SameState
MergeStateIntoanObject
ReducerDispatch
useRefAsStateWithuseState
useRefAsStateWithuseReducer
S1:{s1}S2:{s2}S3:{s3}
MergeObjectS1:{state.s1}S2:{state.s2}S3:{state.s3}
reducerStateObjectS1:{reducerState.s1}S2:{reducerState.s2}S3:{''} {reducerState.s3}
state1Ref:{state1Ref.current}state2Ref:{state2Ref.current}
); }; exportdefaultTestHook;

规则记不住怎么办?

上面罗列了一大堆情况,但是这些规则难免会记不住,React 事务机制导致的两种完全截然不然的重新渲染机制,确实让人觉得有点恶心,React 官方也注意到了,既然在事务流的中 setState 可以合并,那不在 React 事务流的回调,能不能也合并,答案是可以的,React 官方其实在 React V18 中, setState 能做到合并,即使在异步回调或者定时器回调或者原生事件绑定中,可以把测试代码直接丢 React V18 的环境中尝试,就算是上面列出的会多次渲染的场景,也不会重新渲染多次

但是,有了 React V18 最好也记录一下以上的规则,对于减少渲染次数还是很有帮助的





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

原文标题:React中的重新渲染

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    用WEB技术栈开发NATIVE应用(二):WEEX 前端SDK原理详解

    、createElement 、appendChild 这三个接口,简单构建了一个用于渲染的节点树,最终生成了两条渲染指令。Platform API 指的是原生环境提供的 API,这些 API 是 Weex
    发表于 03-02 14:10

    优化React开发体验

    从零构建React开发环境(四)
    发表于 04-01 16:00

    基于react父组件传值给子组件的例子说明state和props

    react-9 state , props和map 循环渲染的简单描述
    发表于 04-12 06:12

    React优势,react学习笔记分享

    1、React优势react适合大型团队的开发,每个人负责自己的组件,开发完毕之后由react渲染到前端页面
    发表于 10-27 06:53

    求助,imageProgress Widget--调用setValue()时是否重新渲染整个进度?

    关于 imageProgress 小部件,有人能告诉我在调用函数 setValue() 时整个矩形是否被重新渲染(无效)了吗?或者只渲染正在改变的部分?在我看来是的。如果只渲染变化的部
    发表于 01-30 06:01

    react 渲染html字符串

    React Fiber中,render函数可以直接返回一个字符串了,换言之,一个组件可以直接渲染为一个字符串,而不是必须渲染为一个HTML模样的物体。 举个例子,下面这个控件LongString
    发表于 09-28 19:31 0次下载
    <b class='flag-5'>react</b> <b class='flag-5'>渲染</b>html字符串

    前端渲染引擎的优势分析

    React、Vue、Angular等均属于MVVM模式,在一些只需完成数据和模板简单渲染的场合,显得笨重且学习成本较高,而解决该问题非常优秀框架之一是doT.js,本文将对它进行详解。 背景 前端
    发表于 09-30 13:14 0次下载
    前端<b class='flag-5'>渲染</b>引擎的优势分析

    详谈 Vue 和 React 的八大区别

    为什么 React 不精确监听数据变化呢 ?这是因为 Vue 和 React 设计理念上的区别, Vue 使用的是可变数据,而React更强调数据的不可变。所以应该说没有好坏之分,Vue更加简单,而
    的头像 发表于 09-15 15:27 8704次阅读
    详谈 Vue 和 <b class='flag-5'>React</b> 的八大区别

    React-in-patterns React开发设计模式/技术列表

    react-in-patterns.zip
    发表于 04-19 11:28 0次下载
    <b class='flag-5'>React</b>-in-patterns <b class='flag-5'>React</b>开发设计模式/技术列表

    React Sight React组件层次可视化工具

    ./oschina_soft/React-Sight.zip
    发表于 05-18 15:36 4次下载
    <b class='flag-5'>React</b> Sight <b class='flag-5'>React</b>组件层次可视化工具

    React Native for Windows使用React构建原生Windows应用

    ./oschina_soft/react-native-windows.zip
    发表于 06-22 10:06 1次下载
    <b class='flag-5'>React</b> Native for Windows使用<b class='flag-5'>React</b>构建原生Windows应用

    React正在经历Angular.js的时刻吗?

    文章指出,React 的核心仍然是一个视图库,这一点没有改变:使用 React 服务器组件,您仍然可以使用 JSX 构建组件,并渲染作为 props 传递的动态内容。但是,除此之外,服务器组件中
    的头像 发表于 07-17 16:27 887次阅读
    <b class='flag-5'>React</b>正在经历Angular.js的时刻吗?

    前端框架的Signals有何优势?

    这在 React 中是不可能做到的,React 是基于视图驱动的,状态改变会重新执行整个渲染函数,并且 React 完全无法识别状态是如何被
    的头像 发表于 10-11 11:04 1131次阅读

    简述大前端技术栈的渲染原理

    应用开发:Android、iOS、鸿蒙(HarmonyOS)等; •Web前端框架:Vue、React、Angular等; •小程序开发:微信小程序、京东小程序、支付宝小程序等; •跨平台解决方案:React Native、Flutter、Taro、Weex等。 什么是
    的头像 发表于 11-07 10:11 958次阅读

    使用SSR构建React应用的步骤

    使用SSR(Server-Side Rendering,服务器端渲染)构建React应用的步骤通常包括以下几个阶段: 一、项目初始化与配置 创建React项目 : 可以使用Create Rea
    的头像 发表于 11-18 11:30 1175次阅读