本節主要講解如下幾個新的特性:前端
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!react
定義:Context 提供了一種方式,可以讓數據在組件樹中傳遞而沒必要一級一級手動傳遞。webpack
這定義讀的有點晦澀,來看張圖:git
假設有如上的組件層級關係,若是最底層的 Item
組件,須要最頂層的 Window
組件中的變量,那咱們只能一層一層的傳遞下去。很是的繁瑣,最重要的是中間層可能不須要這些變量。github
有了 Context 以後,咱們傳遞變量的方式是這樣的:web
Item 能夠直接從 Window 中獲取變量值。數組
固然這種方式會讓組件失去獨立性,複用起來更困難。不過存在即合理,必定有 Context 適用場景。那 Context 是如何工做的呢。網絡
首先要有一個 Context 實例對象,這個對象能夠派生出兩個 React 組件,分別是 Provier 和 Consumer。異步
Provider 接收一個 value
屬性,這個組件會讓後代組件統一提供這個變量值。固然後代組件不能直接獲取這個變量,由於沒有途徑。因此就衍生出 Consumer 組件,用來接收 Provier
提供的值。ide
一個 Provider 能夠和多個消費組件有對應關係。多個 Consumer 也能夠嵌套使用,裏層的會覆蓋外層的數據。
所以對於同一個 Context 對象而言,Consumer 必定是 Provier 後代元素。
建立 Contect 方式以下:
const MyContext = React.createContext(defaultValue?);
複製代碼
來個實例:
import React, {createContext, Component} from 'react';
const BatteryContext = createContext();
class Leaf extends Component {
render() {
return (
<BatteryContext.Consumer>
{
battery => <h1>Battery: {battery}</h1>
}
</BatteryContext.Consumer>
);
}
}
// 爲了體現層級多的關係,增長一層 Middle 組件
class Middle extends Component {
render() {
return <Leaf />
}
}
class App extends Component {
render () {
return (
<BatteryContext.Provider value={60}>
<Middle />
</BatteryContext.Provider>
)
}
}
export default App;
複製代碼
上述,首先建立一個 Context 對象 BatteryContext
, 在 BatteryContext.Provider 組件中渲染 Middle 組件,爲了說明一開始咱們所說的多層組件關係,因此咱們在 Middle
組件內不直接使用 BatteryContext.Consumer
。而是在 其內部在渲染 Leaf
組件,在 Leaf 組件內使用 BatteryContext.Consumer 獲取BatteryContext.Provider 傳遞過來的 value
值。
運行結果:
當 Provider 的 value
值發生變化時,它內部的全部消費組件都會從新渲染。Provider 及其內部 consumer 組件都不受制於 shouldComponentUpdate
函數,所以當 consumer 組件在其祖先組件退出更新的狀況下也能更新。
來個實例:
...
class App extends Component {
state = {
battery: 60
}
render () {
const {battery} = this.state;
return (
<BatteryContext.Provider value={battery}>
<button type="button"
onClick={() => {this.setState({battery: battery - 1})}}>
Press
</button>
<Middle />
</BatteryContext.Provider>
)
}
}
...
複製代碼
首先在 App 中的 state 內聲明一個 battery
並將其傳遞給 BatteryContext.Provider 組件,經過 button 的點擊事件進減小 一 操做。
運行效果 :
一樣,一個組件可能會消費多個 context
,來演示一下:
import React, {createContext, Component} from 'react';
const BatteryContext = createContext();
const OnlineContext = createContext();
class Leaf extends Component {
render() {
return (
<BatteryContext.Consumer>
{
battery => (
<OnlineContext.Consumer>
{
online => <h1>Battery: {battery}, Online: {String(online)}</h1>
}
</OnlineContext.Consumer>
)
}
</BatteryContext.Consumer>
);
}
}
// 爲了體現層級多的關係,增長一層 Middle 組件
class Middle extends Component {
render() {
return <Leaf />
}
}
class App extends Component {
state = {
online: false,
battery: 60
}
render () {
const {battery, online} = this.state;
console.log('render')
return (
<BatteryContext.Provider value={battery}>
<OnlineContext.Provider value={online}>
<button type="button"
onClick={() => {this.setState({battery: battery - 1})}}>
Press
</button>
<button type="button"
onClick={() => {this.setState({online: !online})}}>
Switch
</button>
<Middle />
</OnlineContext.Provider>
</BatteryContext.Provider>
)
}
}
export default App;
複製代碼
同 BatteryContext 同樣,咱們在聲明一個 OnlineContext
,並在 App state 中聲明一個 online
變量,在 render
中解析出 online
。若是有多個 Context 的話,只要把對應的 Provier 嵌套進來便可,順序並不重要。一樣也加個 button 來切換 online
的值。
接着就是使用 Consumer,與 Provier 同樣嵌套便可,順序同樣不重要,因爲 Consumer 須要聲明函數,語法稍微複雜些。
運行結果:
接下來在 App 中註釋掉
// <BatteryContext.Provider></BatteryContext.Provider>
在看運行效果:
能夠看出,並無報錯,只是 battery
取不到值。這時候 createContext() 的默認值就派上用場了,用如下方式建立:
const BatteryContext = createContext(90);
複製代碼
這個默認值的使用場景就是在 Consumer 找不到 Provier 的時候。固然通常業務是不會有這種場景的。
...
class Leaf extends Component {
render() {
return (
<BatteryContext.Consumer>
{
battery => <h1>Battery: {battery}</h1>
}
</BatteryContext.Consumer>
);
}
}
...
複製代碼
回到一開始的實例,咱們在看下 Consuer
裏面的實現。因爲 Consumer 特性,裏面的 JSX 必須是該 Consumer 的回返值。這樣的代碼就顯得有點複雜。咱們但願在整個 JSX 渲染以前就能獲取 battery
的值。因此 ContextType
就派上用場了。這是一個靜態變量,以下:
...
class Leaf extends Component {
static contextType = BatteryContext;
render() {
const battery = this.context;
return (
<h1>Battery: {battery}</h1>
);
}
}
...
複製代碼
掛載在 class 上的 contextType
屬性會被重賦值爲一個由 React.createContext()
建立的 Context 對象。這能讓你使用 this.context
來消費最近 Context
上的那個值。你能夠在任何生命週期中訪問到它,包括 render
函數中。
你只經過該 API 訂閱單一 context。若是你想訂閱多個,就只能用較複雜的寫法了。
React.lazy 函數能讓你像渲染常規組件同樣處理動態引入(的組件)。
首先聲明一個 About 組件
import React, {Component} from 'react'
export default class About extends Component {
render () {
return <div>About</div>
}
}
複製代碼
而後在 APP 中使用 lazy
動態導入 About
組件:
import React, {Component, lazy, Suspense} from 'react'
const About = lazy(() => import(/*webpackChunkName: "about" */'./About.jsx'))
class App extends Component {
render() {
return (
<div>
<About></About>
</div>
);
}
}
export default App;
複製代碼
運行後會發現:
由於 App 渲染完成後,包含 About 的模塊尚未被加載完成,React 不知道當前的 About
該顯示什麼。咱們可使用加載指示器爲此組件作優雅降級。這裏咱們使用 Suspense
組件來解決。 只需將異步組件 About
包裹起來便可。
...
<Suspense fallback={<div>Loading...</div>}>
<About></About>
</Suspense>
...
複製代碼
fallback
屬性接受任何在組件加載過程當中你想展現的 React 元素。你能夠將 Suspense
組件置於懶加載組件之上的任何位置。你甚至能夠用一個 Suspense
組件包裹多個異步組件。
那若是 about 組件加載失敗會發生什麼呢?
複製代碼
上面咱們使用 webpackChunkName
導入的名加載的時候取個一個名字 about
,咱們看下網絡請求,右鍵點擊 Block Request URL
從新加載頁面後,會發現整個頁面都報錯了:
在實際業務開發中,咱們確定不能忽略這種場景,怎麼辦呢?
若是模塊加載失敗(如網絡問題),它會觸發一個錯誤。你能夠經過錯誤邊界技術來處理這些狀況,以顯示良好的用戶體驗並管理恢復事宜。
若是一個 class 組件中定義了 static getDerivedStateFromError()
或 componentDidCatch()
這兩個生命週期方法中的任意一個(或兩個)時,那麼它就變成一個錯誤邊界。當拋出錯誤後,請使用 static getDerivedStateFromError()
渲染備用 UI ,使用 componentDidCatch()
打印錯誤信息。
接着,借用錯誤邊界,咱們來優化以上當異步組件加載失敗的狀況:
class App extends Component {
state = {
hasError: false,
}
static getDerivedStateFromError(e) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>error</div>
}
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<About></About>
</Suspense>
</div>
);
}
}
複製代碼
運行效果:
先來看個例子:
class Foo extends Component {
render () {
console.log('Foo render');
return null;
}
}
class App extends Component {
state = {
count: 0
}
render() {
return (
<div>
<button onClick={() => this.setState({count: this.state.count + 1})}>Add</button>
<Foo name="Mike" />
</div>
);
}
}
複製代碼
例子很簡單聲明一個 Foo 組件,並在 APP 的 state 中聲明一個變量 count ,而後經過按鈕更改 count 的值。
運行結果:
能夠看出 count
值每變化一次, Foo
組件都會從新渲染一次,即便它沒有必要從新渲染,這個是咱們的能夠優化點。
React 中提供了一個 shouldComponentUpdate
,若是這個函數返回 false
,就不會從新渲染。在 Foo 組件中,這裏判斷只要傳入的 name
屬性沒有變化,就表示不用從新渲染。
class Foo extends Component {
...
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.name === this.props.name) {
return false
}
return true
}
...
}
複製代碼
運行效果:
Foo
組件不會從新渲染了。但若是咱們傳入數據有好多個層級,咱們得一個一個的對比,顯然就會很繁瑣且冗長。 其實 React 已經幫咱們提供了現層的對比邏輯就是 PureComponent
組件。咱們讓 Foo
組件繼承 PureComponent
... class Foo extends PureComponent { render () { console.log('Foo render'); return null; } } ...
運行效果同上。**但它的實現仍是有侷限性的,只有傳入屬性自己的對比,屬性的內部發生了變化,它就搞不定了。**來個粟子:
class Foo extends PureComponent {
render () {
console.log('Foo render');
return <div>{this.props.person.age}</div>;
}
}
class App extends Component {
state = {
person: {
count: 0,
age: 1
}
}
render() {
const {person} = this.state;
return (
<div>
<button
onClick={() => {
person.age ++;
this.setState({person})
}}>
Add
</button>
<Foo person={person}/>
</div>
);
}
}
複製代碼
在 App 中聲明一個 person
,經過點擊按鈕更改 person
中的age
屬性,並把 person
傳遞給 Foo
組件,在 Foo
組件中顯示 age
。
運行效果:
點擊按鍵後,本應該從新渲染的 Foo
組件,卻沒有從新渲染。就是由於 PureComponent
提供的 shouldComponentUpdate
發現的 person 自己沒有變化,才拒絕從新渲染。
因此必定要注意 PureComponent
使用的場景。只有傳入的 props
第一級發生變化,纔會觸發從新渲染。因此要注意這種關係,否則容易發生視圖不渲染的 bug。
PureComponent
還有一個陷阱,修改一下上面的例子,把 age
的修改換成對 count
,而後在 Foo 組件上加一個回調函數:
...
return (
<div>
<button
onClick={() => {
this.setState({count: this.state.count + 1})
}}>
Add
</button>
<Foo person={person} cb={() =>{}}/>
</div>
);
...
複製代碼
運行效果:
能夠看到 Foo
組件每次都會從新渲染,雖然 person
自己沒有變化,可是傳入的內聯函數每次都是新的。
解決方法就是把內聯函數提取出來,以下: ... callBack = () => {} ...
講了這麼多,咱們尚未講到 memo
,其實咱們已經講完了 memo
的工做原理了。
React.memo
爲高階組件。它與 React.PureComponent
很是類似,但它適用於函數組件,但不適用於 class 組件。
咱們 Foo
組件並無相關的狀態,因此能夠用函數組件來表示。
...
function Foo (props) {
console.log('Foo render');
return <div>{props.person.age}</div>;
}
...
複製代碼
接着使用 memo
來優化 Foo
組件
...
const Foo = memo(function Foo (props) {
console.log('Foo render');
return <div>{props.person.age}</div>;
})
...
複製代碼
運行效果
最後,若是你喜歡這個系列的,肯請你們給個讚的,我將會更有的動力堅持寫下去。
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。