setState詳解
實現原理
開發中我們并不能直接修改State來重新渲染界面:
因為修改State之后,希望React根據最新的State來重新渲染界面,但這種方式的修改React并不知道數據發生了變化;
React并沒有類似于Vue2中的Object.defineProperty或Vue3中的Proxy的方式來監聽數據的變化;
我們必須通過setState來告知React數據已經發生了變化;
該方法的源碼實現如下:
三種使用方式
import React, { Component } from 'react'export class App extends Component {constructor () {super () this.state={message:'hello',count:20}}changeText() {// 1.setState更多用法// 1.1基本使用// this.setState({// message:'你好'// })/* 1.2setState可以傳入一個回調函數好處一:可在回調函數中編寫新的state處理邏輯好處二:當前回調函數會將之前的state和props傳遞進來*/// this.setState((state,props) => {// console.log(state,props);// return {// message:'你好'// }// })/* 1.3setState在React的事件處理中是一個異步調用如果希望在數據更新之后(數據合并),獲取到對應的結果執行一些邏輯代碼那么可以在setState中傳入第二個參數:callback*/this.setState({message:"你好呀,小橙子"},() => {console.log(this.state.message);//你好呀,小橙子})// 會先執行這行代碼console.log("-----------",this.state.message);// hello}addCount () {}render() {const { message,count } = this.statereturn (<div><h2>{message}</h2><button onClick={() => this.changeText()}>修改文本</button><h2>當前計數:{count}</h2><button onClick={() => this.addCount()}>count+1</button></div>)}
}export default App
setState異步更新
為什么setState設計為異步呢?
React核心成員(Redux的作者)Dan Abramov也有對應的回復:RFClarification: why is `setState` asynchronous? · Issue #11527 · facebook/react · GitHub
- setState設計為異步,可以顯著的提升性能;
- 如果每次調用 setState都進行一次更新,那么意味著render函數會被頻繁調用,界面重新渲染,這樣效率是很低的;
- 最好的辦法應該是獲取到多個更新,之后進行批量更新;
- 如果同步更新了state,但是還沒有執行render函數,那么state和props不能保持同步;
- state和props不能保持一致性,會在開發中產生很多的問題;
import React, { Component } from 'react'
function Hello (props) {return <h2>{props.message}</h2>
}export class App extends Component {constructor () {super () this.state={message:'hello',count:20}}changeText() {}addCount () {// 一直是20+1——也說明setState是異步的/* this.setState({count:this.state.count + 1})this.setState({count:this.state.count + 1})this.setState({count:this.state.count + 1}) */this.setState((state) => {console.log(this.state.count);//20return {count:state.count + 1}})this.setState((state) => {console.log(this.state.count);//20return {count:state.count + 1}})this.setState((state) => {console.log(this.state.count);//20return {count:state.count + 1}})}/* 假設setState是同步的,那么點擊按鈕后,this.state.message已經改變——但是還未執行render函數——那么頁面上的數據就還是之前的-即state和props不能保持同步*/render() {// 調用addCount函數,里面有三個setState,但render只會重新渲染一次——批量更新console.log("render函數被執行了幾次");const { message,count } = this.statereturn (<div><h2>{message}</h2><button onClick={() => this.changeText()}>修改文本</button><h2>當前計數:{count}</h2><button onClick={() => this.addCount()}>count+1</button><Hello message={message} /></div>)}
}export default App
如何獲取異步修改完的結果?
方式一:setState的回調
setState接受兩個參數:第二個參數是一個回調函數,這個回調函數會在更新后會執行;
方式二:生命周期函數
注意:在React18之前,如果在setState外邊包個setTimeout這種宏任務,它不由React回調,而是瀏覽器。故setState就變成了同步操作
React18之前:setTimeout中setState操作,是同步操作
React18之后:setTimeout中setState操作,是異步操作
setTimeout ( () => {this.setState({message:'你好'})console.log(this.state.message);//React18之前:你好;React18之后:hello
},0)
性能優化SCU
當我們修改根組件中的數據,所有組件都需要重新render,進行diff算法,性能極低!——只更新數據改變的即可——通過shouldComponentUpdate方法
//nextProps:修改之后最新的props; nextState:修改之后最新的state
shouldComponentUpdate(nextProps, nextState) {// 性能優化:自己對前后state進行對比,如果前后state沒有變化,就不更新組件if(this.state.message !== nextState.message || this.state.count !== nextState.count){return true}//根據返回值覺得是否調用render方法return false
}
但如果所有的類都需要手動設置,那工作量也太大了!
React內部已幫我們實現PureComponent
底層原理實現:
注意:PureComponent只能檢測第一層數據的變化,也就是復雜數據類型若地址未發生變化,是檢測不到的
import React, { PureComponent } from 'react'export default class App extends PureComponent {constructor () {super()this.state = {books:[{name:"你不知道的JS",price:99,count:2},{name:"JS高級程序設計",price:78,count:2},{name:"React高級設計",price:94,count:2},{name:"LeetCode",price:88,count:2},]}}/* PureComponent底層實現原理:shouldComponentUpdate(nextProps,nextState) {淺層比較return (!shallowEqual(nextProps,this.props) || shallowEqual(nextState,this.state))}*/addBook() {const newBook = {name:"算法",price:99,count:8}/* React 依賴于狀態的不可變性來確定何時需要更新 UI直接修改狀態對象的屬性不會讓 React 知道狀態已經發生了變化,因此不會重新渲染組件該做法在PureComponent中是不能引起重新渲染的this.state.books.push(newBook) */this.setState({books:[...this.state.books,newBook]})}addCount (index) {// this.state.books[index]++ //引用對象并未發生改變const newBooks = [...this.state.books]//引用發生改變newBooks[index].count++this.setState({books:newBooks})}render() {const {books} = this.statereturn (<div><h2>書籍列表</h2><ul>{books.map((item,index) => {return (<li key={item.name}><span>{item.name}-{item.price}-{item.count}</span><button onClick={() => this.addCount(index)}>+1</button></li>)})}</ul><button onClick={() => this.addBook()}>添加新書籍</button></div>)}
}
針對類組件可使用 PureComponent。那函數組件那???
我們可使用高階函數memo將組件都包裹一層
import { memo } from "react";
const Profile = memo(function(props) {console.log("Profile render函數");return (<div><h2>Profile-{props.message}</h2></div>)
})export default Profile
ref獲取元素或組件實例
傳入一個對象
1.類組件:通過 React.createRef() 創建 ref 對象,并綁定到 JSX 元素的 ref 屬性
class ClassComponent extends React.Component {constructor(props) {super(props);this.myRef = React.createRef(); // 創建 ref 對象}componentDidMount() {console.log(this.myRef.current); // 獲取 DOM 元素}render() {return <div ref={this.myRef}>類組件中使用 ref 對象</div>;}
}
2.函數組件:通過 React.useRef() Hook 創建 ref 對象,并綁定到 JSX 元素
function FunctionComponent() {const myRef = React.useRef(null); // 創建 ref 對象React.useEffect(() => {console.log(myRef.current); // 獲取 DOM 元素}, []);return <div ref={myRef}>函數組件中使用 useRef</div>;
}
使用回調 ref
原理:將回調函數傳遞給元素的 ref 屬性,React 在掛載/卸載時調用該回調,參數為 DOM 元素
1.類組件
class ClassComponent extends React.Component {constructor(props) {super(props);this.myRef = null; // 直接保存 DOM 引用}componentDidMount() {console.log(this.myRef); // 直接訪問 DOM 元素}render() {return (<div ref={(el) => { this.myRef = el; }}> {/* 回調 ref */}類組件中使用回調 ref</div>);}
}
2.函數組件:
function FunctionComponent() {const myRef = React.useRef(null); // 持久化保存 DOM 引用const setRef = (el) => {myRef.current = el; // 通過回調更新 ref};React.useEffect(() => {console.log(myRef.current); // 獲取 DOM 元素}, []);return <div ref={setRef}>函數組件中使用回調 ref</div>;
}
獲取函數子組件的DOM
import React, { PureComponent, createRef,forwardRef } from 'react'const HelloWorld = forwardRef(function(props,ref) {return(<h1 ref={ref}>Hello World</h1>)
})export class App extends PureComponent {constructor(props) {super(props);this.whyRef = createRef();}getNativeDOM(){console.log(this.whyRef.current);}render() {return (<div>{/* 不能在函數組件上直接使用 ref屬性,因為他們沒有實例——通過forwardRef做一個轉發*/}<HelloWorld ref={this.whyRef}/><button onClick={() => this.getNativeDOM()}>獲取組件實例</button></div>)}
}export default App