前言
在之前的學習中,我們已經知道了React組件的定義和使用,路由配置,組件通信等其他方法的React知識點,那么本篇文章將針對
React
的一些進階知識點以及React16.8
之后的一些新特性進行講解。希望對各位有所幫助。
一、setState
(一)更新狀態的2種寫法
在之前更新state的時候,我們常常使用的是this.setState()
方法進行狀態的更新,傳入的參數是一個{key:value}
對象,用于對state中指定的值進行更新。比如下面更新count值的寫法:
import React, { Component } from 'react'
export default class Demo extends Component {state = {count:0}increment = ()=>{const {count} = this.state;this.setState({count:count+1}}render() {return (<div>當前的數值為: {this.state.count}<br/><button onClick={this.increment}>點我+1</button></div>)}
}
上面的寫法固然不錯,但實際上setState
函數傳入的參數不僅可以是一個對象,還可以是一個函數,在函數中我們可以接收到入參state
,然后直接進行狀態的更新。我們之前之間傳入一個對象,其實可以看成是這種函數寫法的簡寫方式。
export default class Demo extends Component {state = {count:0}increment = ()=>{this.setState(state=>({count:state.count+1}));}render() {return (<div>當前的數值為: {this.state.count}<br/><button onClick={this.increment}>點我+1</button></div>)}
}
值得一提的是,雖然這種寫法自帶的state可以方便我們后續操作,但實際上我們還是用的比較少,畢竟直接傳入對象相對來說更加簡單~
(二)指定setState的回調函數
setState函數除了可以傳入要更新的狀態之外, 還可以接收第二個參數,更新完狀態后的回調函數。我們可以先看下面這個案例:
export default class Demo extends Component {state = {count:0}increment = ()=>{this.setState(state=>({count:state.count+1}));console.log(this.state.count)}render() {return (<div>當前的數值為: {this.state.count}<br/><button onClick={this.increment}>點我+1</button></div>)}
}
按道理說,當點擊按鈕時,會對count
的值進行+1操作,并打印出更新完狀態的最新值,但實際的結果是:

我們可以看到,此時打印的結果卻是count未更新前的值,原因是setState()雖然是同步方法,但具體更新狀態的方法卻是異步的,如果想要更新最新狀態值做某些操作的話,就需要我們把對應的操作放在回調函數中。
export default class Demo extends Component {state = {count:0}increment = ()=>{this.setState(state=>({count:state.count+1}),()=>{console.log(this.state.count)});}render() {return (<div>當前的數值為: {this.state.count}<br/><button onClick={this.increment}>點我+1</button></div>)}
}
使用回調函數之后,我們可以看到,打印的結果就是我們想要的了:

二、懶加載:lazyLoad
實際上當頁面導航欄的對應著多個組件時,即使用戶只需要使用其中的1個或者幾個,但頁面在加載的時候還是會將所有組件都加載出來,這其實是不太合理的。組件的懶加載就可以幫助我們解決這個問題。
同時,懶加載需要配合Suspense 一起使用。這樣做的原因很簡單:一次性把組件加載出來,雖然一開始全部加載的時候速度會比較慢,但后面打開的速度就會很快。而懶加載的話,由于還要再發一次網絡請求,所以react會要求我們在等待結果返回的過程中,定義一個類似于Loading提示的組件,以此來提高用戶體驗(否則用戶看到的將是白屏)
未使用懶加載之前,如上述所說,頁面就可以把對應的組件都加載好了(我們可以直接在main.chunk.js
文件中找到對應組件的代碼):

而實際上,React
本身就提供了lazy
函數供我們對組件進行懶加載配置:
步驟1: 引入 lazy函數和Susqense組件
import React, { Component,lazy,Suspense} from 'react'
步驟2:使用lazy函數,把需要懶加載的組件作為參數傳入
const About = lazy(()=>import('./About'))
const Home = lazy(()=>import('./Home'))
步驟三:使用Suspense組件,對路由進行包裹
<Suspense><Route path="/about" component={About} /><Route path="/home" component={Home} />
</Suspense>
步驟四:定義Loading組件
在開頭我們就解釋過,使用懶加載的話只有當每次用戶點擊后,才會真正發起獲取組件的網路請求,Loading組件存在的意義就是在請求的響應回來之前,頁面上先顯示Loading組件,不讓頁面出現白屏的效果。
import React, { Component } from 'react'export default class Loading extends Component {render() {return (<div><h1 style={{backgroundColor: "gray",color:'orange'}}>Loading...</h1></div>)}
}
步驟五:引入組件并將Loading組件作為回調傳入Suspense組件中:
<Suspense fallback={<Loading/>}><Route path="/about" component={About} /><Route path="/home" component={Home} />
</Suspense>
最終,我們可以在瀏覽器的控制臺上查看效果:

當使用懶加載之后,只有當我們點擊的時候,才會發起新的請求,這里的
0.chunk.js
就是請求回來的結果,如果我們點進去看的話,就可以在其中發現我們的組件代碼了。 三、Fragment標簽的使用
我們知道,使用JSX語法定義組件時,會要求我們返回唯一一個根標簽,但是這樣有一個缺點:讓我們的頁面嵌套層次過深,我們可以使用 Fragment 或者 <></> 這種空標簽來避免上述問題。需要注意的是,雖然二者都可以解決根標簽的問題,但是在使用上二者還是有些不同的,Fragment標簽中可以傳遞參數key,而空標簽是不允許傳遞參數的。
我們可以看看未使用Fragment標簽時,頁面上的元素:
App組件
export default class App extends Component {render() {return (<div><Demo/></div>)}
}
Demo組件
export default class Demo extends Component {render() {return (<div><input /><br /><input /></div>)}
}

我們可以看到,雖然頁面上的元素正常顯示,但還是會嵌套在不需要使用到的div標簽中,我們可以使用Fragment
標簽或者空標簽來解決這個問題:
App組件:
import React, { Component,Fragment } from 'react'
import Demo from './4_fragement'export default class App extends Component {render() {return (<Fragment><Demo/></Fragment>)}
}
Demo組件
import React, { Component, Fragment } from 'react'export default class Demo extends Component {render() {return (<><input /><br /><input /></>)}
}

我們可以看到,使用<Fragment>
或者使用<></>
標簽都有著使組件不需要有一個真實DOM標簽的功能,但實際上二者在使用上還是略有不同的,Fragment標簽可以接收key屬性的參數,而空標簽是不可以接收屬性參數的。
四、實例對象Context屬性的應用
我們知道,使用props屬性自然可以將方法或者數據從父組件上逐層傳遞到子組件中。但是當組件間的嵌套關系比較深的時候,這時再通過props
來傳遞值就會很麻煩,針對這種情況,我們可以考慮用Context
來解決這個問題:Context
其實是組件實例對象上的一個屬性,利用Provider
和Consumer
,我們可以實現值的傳遞。
假如目前有A、B、C三個組件,B是A的子組件,C是B的子組件:
export default class A extends Component {state = { name: '小明', age: 18 }render() {const {name,age} = this.state;return (<div className="parent">我是A組件<br /><h4>姓名是:{this.state.name},年齡是:{this.state.age}</h4><B /></div>)}
}
class B extends Component {render() {return (<div className="child">我是B組件<C /></div>)}
}
class C extends Component {render() {return (<div className="grand">我是C組件<h4>從A組件獲取到的值為</h4></div>)}
}
如果C組件想要獲取A組件的值,我們可以通過以下步驟來實現:
步驟1:通過React.createContext()
,獲取Provider
和Consumer
const MyContext = React.createContext();
const { Provider, Consumer } = MyContext;
步驟2:在A組件中使用Provider包裹住子組件
export default class A extends Component {state = { name: '小明', age: 18 }render() {const {name,age} = this.state;return (<div className="parent">我是A組件<br /><h4>姓名是:{this.state.name},年齡是:{this.state.age}</h4>{/* 使用Provider包裹住子組件,有需要的后代組件通過Consumer標簽即可獲取,需要注意,value傳入的值是一個對象 */}<Provider value={{name,age}}><B /></Provider></div>)}
}
步驟3:在C組件中通過Consumer標簽獲取A組件的值
class C extends Component {render() {return (<div className="grand">我是C組件<h4>從A組件獲取到的值為<Consumer>{value => `姓名為:${value.name},年齡為:${value.age}`}</Consumer></h4></div>)}
}
五、PureComponent的使用
對于子組件來說,一旦父組件重新render,那么其也會跟著一起重新執行一次render方法,即使子組件并沒有使用到父組件傳遞的任何屬性。或者當父組件空調用一次setState方法,那么此時state并沒有真的改變,但是子組件還是要重新執行一次render,這其實是不太合理的。
export default class Parent extends Component {state = {carName:'哈雷摩托',stus:['小明','小紅']}changeCar = ()=>{//this.setState({carName:'特斯拉'})this.setState({})}addStus = ()=>{this.setState({stus:['小明',...this.state.stus]})}render() {console.log('父組件的render方法調用了')const {carName,stus} = this.state;return (<div className="parent"><h3>父組件的車名為:{carName}</h3><h4>{stus}<button onClick={this.addStus}>點我加人</button></h4><button style={{marginBottom: '5px'}} onClick={this.changeCar}>點擊換車</button><Child carName={carName}></Child></div>)}
}
class Child extends Component {render() {console.log('子組件的render方法調用了')return (<div className="child"><h4>接收到父組件的車名為:{this.props.carName}</h4></div>)}
}

解決這個問題的方法有2個:
(1)手動寫 componentShouldUpdate方法,對狀態是否改變進行判斷
比如我們可以通過下面這個鉤子來避免組件發生無效的render:
shouldComponentUpdate(nextProps,nextState){return nextState.carName !== this.state.carName }
(2)利用PureComponent組件,里面幫我們重寫了shouldComponentUpdate方法
我們可以利用組件內componentShouldUpdate這個鉤子,來根據實際需要判斷是否執行render,但如果說狀態比較多,就需要我們手動寫很多判斷條件,比較麻煩。我們更加常用的是直接繼承和使用PureComponent
組件。
import React, { Component,PureComponent } from 'react'
import './index.css'export default class Parent extends PureComponent {
...
}class Child extends PureComponent {...
}

六、render props屬性的應用
對于關系明確的父子組件來說,我們可以比較簡單的從組件的嵌套關系來判斷組件間的父子關系。但是有時候我們可能并不能確定父組件中的子組件具體是哪一個,而是等到具體調用的時候才知道。
比如說有A、B、C三個組件,且我們知道A是B的父組件,B是C的父組件,那么對應的代碼為:
export default class A extends Component {render() {return (<div className="parent"><B/></div>)}
}class B extends Component {render() {return (<div className="parent"><C/></div>)}
}class C extends Component {render() {return (<div className="parent"></div>)}
}
但有時候我們可能只知道B組件內需要傳遞一個組件,但具體是哪個組件卻不能確定,只能等到具體使用的時候才能確定,這個時候就需要用props的方法來解決了。
在Vue
中,這種問題的解決方案被稱為插槽技術
在React
中,我們通常使用children props
以及render props
屬性來解決這類問題。其中,children props
雖然可以完成組件的傳遞,但是卻不能實現父子組件的數據傳遞而render props
則可以解決這類問題
export default class A extends Component {render() {return (<div className="parent"><B><C/></B></div>)}
}class B extends Component {render() {return (<div className="parent">{this.props.children}</div>)}
}class C extends Component {render() {return (<div className="parent"></div>)}
}
用上面這種方式,我們可以通過 通過組件標簽體的方式傳入組件,在對應的組件內使用{this.props.children}
進行展示。這樣就使得組件的靈活性大大提高,但這種方式的缺點也很明顯,C組件雖然是B組件的子組件,但是卻不能獲取B組件的值。這時,就需要用到第二種方式:使用render props
進行參數的傳遞。
export default class A extends Component {render() {return (<div className="parent"><B render={name => <C name={name} />}/></div>)}
}class B extends Component {state = {name:'小明'}render() {return (<div className="parent">{this.props.render(name)}</div>)}
}class C extends Component {render() {return (<div className="parent"><h4>接收到的父組件參數是: {this.props.name}</h4></div>)}
}
通過上述這種方式,我們就可以把父組件的參數傳遞給子組件了,需要注意的是本質上這里使用的還是組件的props屬性,所以props不一定名字要叫render
,只是很多時候我們約定俗成地將這種傳遞值的方式的props值設為render
。
七、錯誤邊界:ErrorBoundary
當子組件發生錯誤時,我們會希望子組件的錯誤不影響到父組件其他功能的使用,這時候我們就可以在父組件上設置錯誤邊界,來防止由于子組件錯誤而影響到整個頁面不能使用的情況出現。解決方法是利用react的一個生命周期 getDerivedStateFromError
我們先來看一下案例,現在存在有Parent
組件和Child
組件:
Parent組件:
export default class Parent extends Component {state = {hasError:''}static getDerivedStateFromError(err){return {hasError: err}}render() {return (<div><h2>我是父組件</h2><Child/></div>)}
}
Child組件:
export default class Child extends Component {state = {person:[{name:'小明',age:10},{name:'大明',age:31},{name:'小紅',age:16},]}render() {return (<div><h2>我是子組件</h2>{this.state.person.map(ele=>{return <li>{ele.name}--{ele.age}</li>})}</div>)}
}
頁面上顯示的效果如下:

組件中state的數據往往是通過請求后臺返回回來的,如果說后臺發生意外,傳來的數據格式有問題,如果沒有合理的解決這個問題,頁面可能就直接用不了了。
export default class Child extends Component {state = {person: 'hh'}render() {return (<div><h2>我是子組件</h2>{this.state.person.map(ele=>{return <li>{ele.name}--{ele.age}</li>})}</div>)}
}

由于返回的數據有問題,使得原先的代碼邏輯判斷發生了錯誤,遇到這種情況,我們會希望把錯誤縮小到當前組件上,而不會對父組件以及其他組件的使用造成干擾。這時,我們就可以使用 getDerivedStateFromError
來對子組件的錯誤進行捕獲和限制。
給父組件加上鉤子
export default class Parent extends Component {state = {hasError:''}static getDerivedStateFromError(err){return {hasError: err}}render() {return (<div><h2>我是父組件</h2>{this.state.hasError ? '網絡不通暢,請稍后再試...':<Child/>}</div>)}
}
getDerivedStateFromError
會捕獲到子組件傳遞的錯誤,我們可以利用這個特性,對組件的顯示進行判斷。
需要注意的是,要想更好的觀察錯誤邊界這個效果,最好是打包后在服務器上面看效果。(在開發模式下,錯誤邊界不起作用)。

我們可以看到,此時雖然子組件發生錯誤,但還是不影響頁面的正常使用。另外,錯誤邊界只能捕獲后代組件生命周期產生的錯誤,不能捕獲自己組件產生的錯誤和其他組件在合成事件、定時器中產生的錯誤。
八、Hook
我們知道,函數式組件由于無法使用this,所以也就不能像類式組件一樣使用state和ref等屬性。但是在React16.8 之后,官方推出了3個hook,使得函數式組件也可以像類式組件一般使用對應的屬性以及常用的生命周期函數:
(1). State Hook: React.useState(): 可以使用state
(2). Effect Hook: React.useEffect(): 可以模擬生命周期鉤子
(3). Ref Hook: React.useRef(): 可以使用ref
(一)State Hook 鉤子
export default function Demo() {function add(){}return (<div><h2>當前總和為:???</h2><button onClick={add}>點我+1</button></div>)
}
我們想要賦予給函數式組件Demo一個state屬性count,此時我們就可以這樣實現:
使用React.useState()
方法,我們可以傳入對應屬性的初始值,方法會返回一個數組,第一個參數是當前屬性的值,第二個參數是修改這個屬性的方法,我們可以在后續使用的時候,具體來設置方法的邏輯。
export default function Demo() {const [count,setCount] = React.useState(0);function add(){// setCount(count+1); setCount的第一種寫法setCount(count => count+1)}return (<div><h2>當前總和為:???</h2><button onClick={add}>點我+1</button></div>)
}
(二)Effect Hook
Effect Hook
可以讓你在函數組件中執行副作用操作(用于模擬類組件中的生命周期鉤子)。Effect Hook
算是三個Hook之中較為復雜的一個了,我們可以用它來模擬componentDidMount
、componentDidUpdate
、componentWillUnmount
這三個生命周期函數。
我們現在在上個案例的基礎上再新增一個功能:每隔1秒鐘,將count
的值進行+1,如果是放在類式組件中,我們很容易就會想到使用componentDidMount
來調用一個定時器,但是在函數式組件中,由于沒有生命周期鉤子函數,所以我們需要用React.useEffect()
來實現我們的代碼。
export default function Demo() {const [count,setCount] = React.useState(0);// 使用 Ref Hook,用法和React.createRef()一樣const myRef = React.useRef();React.useEffect(()=>{let timer = setInterval(()=>{setCount(count => count+1);},1000)},[])function add() {// setCount(count+1); setCount的第一種寫法setCount(count => count+1)}return (<div><input type="text" ref={myRef}/><h2>當前總和為:{count}</h2><button onClick={add}>點我+1</button></div>)
}
userEffect
需要傳入2個參數,第一個相當于是組件加載好后執行的函數,第二個是指定具體監測的state
當第二個參數為空數組時,表示不監測任何一個state,即組件只加載一次,相當于是 componentDidMount
鉤子, 當第二個參數傳入真實的state時,那么一旦監測的state狀態發生改變,那么第一個函數就會調用,此時相當于是componentDidUpdate
鉤子 。同時,useEffect
的return需要傳入一個方法,在組件銷毀的時候調用,相當于是 componentWillUnmount
鉤子。
我們現在再新增一個需求,利用useEffect
的return方法,在卸載組件后停止運行定時器:
export default function Demo() {const [count,setCount] = React.useState(0);// 使用 Ref Hook,用法和React.createRef()一樣const myRef = React.useRef();React.useEffect(()=>{let timer = setInterval(()=>{setCount(count => count+1);},1000)return ()=>{clearInterval(timer)}},[])function add() {// setCount(count+1); setCount的第一種寫法setCount(count => count+1)}function unmount(){ReactDOM.unmountComponentAtNode(document.getElementById('root'));}return (<div><input type="text" ref={myRef}/><h2>當前總和為:{count}</h2><button onClick={add}>點我+1</button><button onClick={unmount}>點擊卸載組件</button></div>)
}
實際上,在點擊卸載組件的按鈕時,就會觸發useEffect函數中return中定義的函數,成功停止計時器。
(三)Ref Hook
Ref Hook可以在函數組件中存儲/查找組件內的標簽或任意其它數據,其實也就是賦予函數式組件使用類似于ref
屬性的功能。在上面的案例的基礎上,我們在添加一個需求,新增一個文本框和按鈕,當點擊按鈕時,把文本框的內容進行彈框顯示:
const [count,setCount] = React.useState(0);// 使用 Ref Hook,用法和React.createRef()一樣const myRef = React.useRef();function add() {// setCount(count+1); setCount的第一種寫法setCount(count => count+1)}function show(){alert(myRef.current.value);}return (<div><input type="text" ref={myRef}/><h2>當前總和為:{count}</h2><button onClick={add}>點我+1</button><button onClick={show}>點擊提示文本框內容</button><br/></div>)
}
說在最后:
本篇文章的代碼已經放在了碼云上,有需要的可以通過下面的鏈接下載:
https://gitee.com/moutory/react-extension

喜歡的朋友記得點贊、收藏、關注哦!!!