<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
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
元件觸發的事件會被冒泡到 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
之後直接獲取,最新的值,例如下面程式碼
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
之後直接獲取,最新的值,例如下面程式碼
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
呼叫就會引起就會元件重新渲染,即使這個狀態沒有參與頁面渲染,所以,請不要把非渲染屬性放 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
,新 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
不是一個物件,情況就更糟糕
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
中表現的情況和類元件表現完全一致,沒有任何差別,但是也有表現不一致的地方
在 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
中 state
並不是一個物件,所以不會自動合併更新物件,那怎麼解決這個非同步函數之後多次 setState
重新渲染的問題?
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
即可,和類元件的操作完全一致,這是一種方案
雖然這個 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
差別不大
// 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
,有 id
和 data
兩個狀態, 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
最好也記錄一下以上的規則,對於減少渲染次數還是很有幫助的
以上就是React 中的重新渲染類元件及函陣列件的詳細內容,更多關於React 重新渲染元件的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45