React 新特性講解及實例(一)

本節主要講解如下幾個新的特性:前端

  • Context
  • ContextType
  • lazy
  • Suspense
  • 錯誤邊界(Error boundaries)
  • memo

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!react

Context

定義:Context 提供了一種方式,可以讓數據在組件樹中傳遞而沒必要一級一級手動傳遞。webpack

這定義讀的有點晦澀,來看張圖:git

假設有如上的組件層級關係,若是最底層的 Item 組件,須要最頂層的 Window 組件中的變量,那咱們只能一層一層的傳遞下去。很是的繁瑣,最重要的是中間層可能不須要這些變量。github

有了 Context 以後,咱們傳遞變量的方式是這樣的:web

Item 能夠直接從 Window 中獲取變量值。數組

固然這種方式會讓組件失去獨立性,複用起來更困難。不過存在即合理,必定有 Context 適用場景。那 Context 是如何工做的呢。網絡

首先要有一個 Context 實例對象,這個對象能夠派生出兩個 React 組件,分別是 ProvierConsumer異步

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 的時候。固然通常業務是不會有這種場景的。

ContextType

...
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。若是你想訂閱多個,就只能用較複雜的寫法了。

lazy 和 Supense 的使用

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

從新加載頁面後,會發現整個頁面都報錯了:

在實際業務開發中,咱們確定不能忽略這種場景,怎麼辦呢?

錯誤邊界(Error boundaries)

若是模塊加載失敗(如網絡問題),它會觸發一個錯誤。你能夠經過錯誤邊界技術來處理這些狀況,以顯示良好的用戶體驗並管理恢復事宜。

若是一個 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>
    );
  }
}
複製代碼

運行效果:

memo

先來看個例子:

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>;
})
...
複製代碼

運行效果

最後,若是你喜歡這個系列的,肯請你們給個讚的,我將會更有的動力堅持寫下去。

參考

  1. React 官方文檔
  2. 《React勁爆新特性Hooks 重構去哪兒網》

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索