React 16.0+ 新特性初探(How to use)

luffyZhou發表於2018-08-01

React 16.0之後的改動還是很大的,除了新增加了很多新特性之外,還明確表示未來會增加async render,增加async render之後,將會在17.0的版本完全廢除當前版本的三個生命週期,對於已經習慣現在寫法的小夥伴來說感覺有點方(至少我有點方),所以還是提前熟悉一下,做好升級的準備吧~

個人覺得升級是必然的事情,所以,還是提前準備一下,做好升級準備!

我技術沒有大牛的水平,所以我寫文章並不是為了吸引人,一方面是記錄自己新學的東西,寫出來覺得自己的理解也會加深;另一方面是讓比我還入門的人找到個非常合適的入門文章。我喜歡配上一些Demo,這樣不太明白的人才能看懂,受教人群不一樣,大牛可以去看官方文件說明,小白可以看看demo感受一下新特性~ Demo地址 Demo大概長這個樣子:

React 16.0+ 新特性初探(How to use)

V16.0

16.0算是一個大版本,增加了很多新特性,解決了很多痛點問題~比如,可以render字串和陣列,比如增加了Fragment,這些在使用中都有效減少了dom節點的數量;還有可以使用portals將新節點插入在任何其他非父節點的dom節點下,對於modal,message等外掛是福音;還有增加了error boundary,如果使用的好你再也不會在專案裡看到滿屏紅色或者崩潰了,哈哈~

render多型別

16.0以後,react的render函式增加了幾種型別,包括字串和陣列型別。

 render() {

     //不需要再把所有的元素繫結到一個單獨的元素中了

      return [

        // 別忘記加上key值

        <li key="A"/>First itemli>,

        <li key="B"/>Second itemli>,

        <li key="C"/>Third itemli>,

      ];

    }
// 也可以用下面這種寫法
 // 不需要再把所有的元素繫結到一個單獨的元素中了
  render() {
    const arr = ['Adams', 'Bill', 'Charlie'];
    const Arr = () => (arr.map((item, index) => <p key={index}>{item}</p>));

    return <Arr />
  }
複製程式碼

React 16.0+ 新特性初探(How to use)

從上圖可以看出,解決了以往必須在外層包一個父元素div的限制,有效的減少了不必要的dom元素。

React.Fragment

解決的痛點問題與上面陣列是相同的,不過個人感覺更加優雅,首先不需要加上key,其次就是增加一個不渲染的空標籤看起來更加的整體,因為以前已經習慣了JSX語法需要一個父標籤,這種寫法更符合習慣。但是在16.0裡提到了Fragment,而更詳細的介紹是在16.2版本里,之所以放在這裡說因為和返回陣列解決的痛點是類似的~ 下面例子來自官網:

// 一個Table元件,裡面巢狀了columns元件


class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />
        </tr>
      </table>
    );
  }
}
// columns元件


class Columns extends React.Component {
  render() {
    return (
      <div>
        <td>Hello</td>
        <td>World</td>
      </div>
    );
  }
}
複製程式碼

上面設計符合react,元件式劃分,但是最後渲染出來卻不是最佳的,因為columns的最外層巢狀了一層沒用的div標籤。這個問題存在於16.0之前。
有了Fragment以後,很好的解決問題:

import React, { Fragment } from 'react';



class Columns extends React.Component {
  render() {
    return (
      <Fragment>
        <td>Hello</td>
        <td>World</td>
      </Fragment>
    );
  }
}
// Fragment的語法糖

  <>
    <td>Hello</td>
    <td>World</td>
  </>
兩個空標籤
複製程式碼

這塊糖有點苦,官方明明說的是語法糖,但是我試了,編譯通不過,並且官方也特意說明了可能使用該語法糖會出現問題,但是給出的解決辦法我都試了,還是不成功,可能配置的不對吧,有誰配置好了可以留言告訴我一下,不過無傷大雅,我倒是覺得語法糖也不一定必須使用。

Error Boundary

什麼是Error Boundary?

單一元件內部錯誤,不應該導致整個應用報錯並顯示空白頁,而Error Boundaries解決的就是這個問題。

在以前的React版本中,如果某一個元件內部出現異常錯誤,會導致整個專案崩潰直接顯示空白頁或者error紅頁,很不友好。error boundary就是解決這個問題的。

Error Boundary本質上是一個元件

按照我的個人理解,error boundary本質上就是一個元件,只不過元件內部多出現了一個生命週期,componentDidCatch,在這個生命週期裡面,它會捕捉本元件下的所有子元件丟擲的異常錯誤,包括堆疊資訊。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
// 使用起來就跟普通元件一樣

<ErrorBoundary>
  <ChildCompA />
  <ChildCompB />
  ...
</ErrorBoundary>

複製程式碼

上面程式碼是官網給出的例子,在ErrorBoundary元件內,定義state={ hasError: false },在componentDidCatch內部捕捉到error,然後動態渲染元件,如果出現異常使用提前定義好的替換元件代替發生異常的元件,這樣整個頁面只有發生異常的部分被替換不影響其他內容的展示。

React 16.0+ 新特性初探(How to use)

Portals

有些元素需要被掛載在更高層級的位置。最典型的應用場景:當父元件具有overflow: hidden或者z-index的樣式設定時,元件有可能被其他元素遮擋,這個時候你就可以考慮要不要使用Portal使元件的掛載脫離父元件。

Portals 提供了一種很好的將子節點渲染到父元件以外的 DOM 節點的方式。

render() {
  // React does *not* create a new div. It renders the children into `domNode`.
  // `domNode` is any valid DOM node, regardless of its location in the DOM.
  return ReactDOM.createPortal(
    this.props.children,
    domNode,
  );
}

複製程式碼

一般而言,元件在裝載的時候會就近裝載在該元件最近的父元素下,而現在你可以使用Portal將元件渲染到任意一個已存在的dom元素下,這個dom元素並不一定必須是元件的父元件。

Portals的應用 —— Modal,message等訊息提示

React 16.0+ 新特性初探(How to use)

Portals的事件冒泡

從上圖可以看出來,彈窗的父元件應該是掛載在#app這個dom下面的,通過portals,我們將modal框掛載在#portal_modal這個dom下了。雖然最後的modal元件沒有掛載在整個應用所在的#app下,但是portals建立的元件裡面的事件依然會冒泡給它自身的父元件,父元件可以捕獲到被掛載在#portal_modal節點下面的modal的點選事件。

class PortalsComp extends Component {
  constructor(props) {
    super(props);
    this.state = { showModal: false, clickTime: 0 };
  }

  handleShow = () => {
    this.setState({ showModal: true });
  }
  
  handleHide = () => {
    this.setState({ showModal: false });
  }

  handleClick = () => {
    let { clickTime } = this.state;
    clickTime += 1;
    this.setState({ clickTime });
  }

  render() {
    const protalModal = this.state.showModal ? (
      <PortalModal>
        <ModalContent hideModal={this.handleHide} />
      </PortalModal>
    ) : null;
    return (
      <div className={s.portalContainer} onClick={this.handleClick}>
        <div>該元件被點選了: {this.state.clickTime}次</div>
        <Button onClick={this.handleShow} type='primary'>點我彈出Modal</Button>
        {protalModal}
      </div>
    );
  }
}

export default PortalsComp;
複製程式碼

React 16.0+ 新特性初探(How to use)

從上圖可以看出來,portals的元件雖然掛載在其他dom下,但是父元件依然可以捕獲到modal的冒泡事件,開啟和關閉,父元件顯示點選次數為2。

V16.3

廢棄的幾個生命週期

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

這三個生命週期之中,componentWillReceiveProps平時用的頻率還是特別多的,所以對於以前的專案,可能升級會是一種麻煩事,但是說是廢棄,但是其實在整個V16版本,還都是可以使用的,只不過會丟擲警告,而且官方會建議使用的時候加上字首UNSAFE_。 componentWillReceiveProps ---> UNSAFE_componentWillReceiveProps

React 16.0+ 新特性初探(How to use)

為什麼要廢棄這三個生命週期

React16.0之前的生命週期設計如下圖:

React 16.0+ 新特性初探(How to use)
可以看到從開始到結束,這些生命週期的設計可以捕捉到元件的每一個state和props的改變,並沒有任何邏輯上的問題,而且對於我們來說寫法已經形成習慣,如果廢棄肯定是費力不討好的事情。那麼為啥官方還是要皮這麼一下呢?

React 16.0+ 新特性初探(How to use)
雖然我英文不好,但是還是大致看了一下,意思呢,首先就是說這三個API經常被濫用和誤用,再者就是在未來版本中,要引入async render(非同步渲染),而在非同步渲染的場景下,這些生命週期裡面的程式碼會在未來的React版本里存在缺陷,因此就拋棄了。 這三個API存在的問題:React v16.3 版本新生命週期函式淺析及升級方案這裡講的很清楚。

為了彌補不足新增了兩個生命週期

React 16.0+ 新特性初探(How to use)

static getDerivedStateFromProps

觸發時間:在元件構建之後(虛擬dom之後,實際dom掛載之前) ,以及每次獲取新的props之後。

每次接收新的props之後都會返回一個物件作為新的state,返回null則說明不需要更新state. 配合componentDidUpdate,可以覆蓋componentWillReceiveProps的所有用法。

// before
componentWillReceiveProps(nextProps) {
  if (nextProps.flag !== this.props.flag) {
    this.setState({	flag: nextProps.flag }, () => {
        if (nextProps.flag) {
            this.doSmething();
          }
    });
  }
}
// 在16.3之後的版本使用,react推薦下面這種寫法,否則eslint可能會提示警告
UNSAFE_componentWillReceiveProps(nextProps) {
   // your code
}

// after
static getDrivedStateFromProps(nextProps, prevState) {
  if (nextProps.flag !== prevState.flag) {
      // 更新state
      return {
          flag: nextProps.flag
      }
  }
  // 不更新state
  return null;
}
// state更新過後需要做的事放在componentDidUpdate裡
componentDidUpdate(prevProps, prevState) {
    if (prevState.flag !== this.props.flag) {
        this.doSomething();
    }
}
複製程式碼

寫法與之前相比要麻煩了一些,但是處理邏輯上應該是更清晰了。在 componentWillReceiveProps 中,一般會進行兩件事,第一、判斷this.props與nextProps的異同,然後更新元件state;第二、根據state的變化更新元件或者執行一些回撥函式。在以前的寫法裡,這兩件事我們都需要在 componentWillReceiveProps 中去做。而在新版本中,官方將兩件事分配到了兩個不同的生命週期 getDerivedStateFromProps 與 componentDidUpdate 中去做,使得元件整體的更新邏輯更為清晰,getDerivedStateFromProps裡面進行state的更新,componentDidUpdate裡做更新之後的各種回撥。而且在 getDerivedStateFromProps 中還禁止了元件去訪問 this.props(static方法,獲取不到元件的this),強制讓開發者去比較 nextProps 與 prevState 中的值,以確保當開發者用到 getDerivedStateFromProps 這個生命週期函式時,就是在根據當前的 props 來更新元件的 state,而不是去做其他一些讓元件自身狀態變得更加不可預測的事情。

還是需要適應,雖然習慣了以前的寫法,但是現在這種效能要更好。而且畢竟以後會廢棄。

這裡解決了我的很久一個困惑,元件最後的更新過程其實是: componentWillReceiveProps(static getDerivedStateFromProps)判斷state的變化 ---> shouldComponentUpdate判斷是否進行更新 render階段會根據diff演算法來生成需要更新的虛擬dom結構 ---> 更新虛擬dom ---> 虛擬dom更新完畢立刻呼叫componentDidUpdate ---> 最後完成渲染。

因為官方給出的定義是,componentDidUpdate是在元件dom更新結束之後立即呼叫,那麼這個更新結束我理解的就是dom已經更新完畢渲染好了,但是我在componentDidUpdate裡面呼叫了alert,發現其實進入該生命週期之後,其實dom還未發生變化,但是頁面上的dom未發生變化,而componentDidUpdate獲取dom的時候值確實正確的,可能這裡是虛擬dom和真實dom不同步的關係吧,總之就是,在componentDidUpdate裡面可以獲取dom節點的操作,獲取的值也是更新完畢的,下面的例子也是這樣的。

React 16.0+ 新特性初探(How to use)

getSnapshotBeforeUpdate ---- 針對對dom的一些操作

觸發時間: update發生的時候,在render之後,在元件dom渲染之前。

返回一個值,作為componentDidUpdate的第三個引數。 配合componentDidUpdate, 可以覆蓋componentWillUpdate的所有用法。

componentWillUpdate存在的問題

  • 與componentWillReceiveProps類似,同樣在一層更新過程中可能會被呼叫多次,這樣就會造成裡面的回撥函式可能會執行多次,浪費效能。
  • 在React17引入async render之後,render階段和commit階段可能並不是同步連貫的,因此,componentDidUpdate和componentWillUpdate獲取到的Dom可能是不同的,這樣就會導致讀取到的dom元素的狀態是不安全的。

getSnapshotBeforeUpdate配合componentDidUpdate來保證狀態的一致

getSnapshotBeforeUpdate的發生時間在render之後,元件dom渲染之前,這樣可以保證此時讀取的dom和componentDidUpdate的dom是一致的。

getSnapshotBeforeUpdate不是靜態方法,裡面可以讀取this.props和this.state等資訊,並且呼叫之後應該返回一個值作為componentDidUpdate的第三個引數

static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.disabled !== prevState.disabled) {
      return {
        disabled: nextProps.disabled
      };
    }
    return null;
  }
  

  getSnapshotBeforeUpdate(prevProps, prevState) {
    return this.props.disabled;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (!snapshot) {
      // 如果snapshot是false,獲取焦點
      this.domRef.focus();
    }
  }

  render() {
    return (
      <div>
        <input ref={(ref) => this.domRef = ref} disabled={this.state.disabled} />
      </div>
    );
  }
複製程式碼

V16.4

修復了getDerivedStateFromProps的bug,為了更好地相容即將到來的非同步渲染

這個點我還沒太弄明白,因為我準備寫的時候就已經是16.4了,也不太知道這個bug會導致什麼影響,不過看了一些文章,大概意思是下面這樣: 參考文章:React16.4 新特性

React這次更新修復了getDerivedStateFromProps這個生命週期的觸發節點, 在之前, 它觸發的方式和舊生命週期getDerivedStateFromProps類似, 都是在被父元件re-render的時候才會觸發,並且本元件的setState的呼叫也不會觸發
這種方式在之前同步渲染的時候是沒有問題的, 但是為了支援新的還未啟用的fiber非同步渲染機制, 現在, getDerivedStateFromProps在元件每一次render的時候都會觸發,也就是說無論是來自父元件的re-render, 還是元件自身的setState, 都會觸發getDerivedStateFromProps這個生命週期。
要理解為什麼react修復了這個生命週期的觸發方式, 我們首先得了解react的非同步渲染機制

react非同步渲染

要理解react非同步渲染的機制, 我們首先要說一說react之前是如何進行渲染。
在react16之前, 元件的渲染都是同步進行的, 也就是說從constructor開始到componentDidUpdate結束, react的執行都是沒有中斷的, 生命週期開始之後就會執行到其結束為止, 這樣帶來的一個缺點就是,如果元件巢狀很深, 渲染時間增長了之後, 一些重要的, 高優先順序的操作就會被阻塞, 例如使用者的輸入等, 這樣就會造成體驗上的不友好。

在之後即將到來的非同步渲染機制中, 會允許首先解決高優先順序的執行,同時會暫停當前的渲染程式,當高優先順序的程式結束之後, 再返回繼續執行當前程式, 這樣會大大的提高react的流暢度,給使用者帶來更好的體驗

而這次修復getDerivedStateFromProps, 正是為了保證與即將到來的非同步渲染模式的相容。
複製程式碼

React pointer events

pointer events是HTML5規範的WEB API,它主要目的是用來將滑鼠(Mouse)、觸控(touch)和觸控筆(pen)三種事件整合為統一的API。

如果你的應用涉及到指標的相關事件,那麼這個API還是很有用的,不過這個API的相容性不怎麼樣,基本主流瀏覽器的最新版本才支援,從React增加了這個pointer events事件來看,說明React官方還是很看重這個API的,我覺得相容性肯定滿滿的會越來越好。

因為相容性不太好,所以官方的建議是使用的時候配合第三方的polyfill來用。

React提供的pointer events

  • onPointerDown
  • onPointerMove
  • onPointerUp
  • onPointerCancel
  • onGotPointerCapture
  • onLostPointerCapture
  • onPointerEnter
  • onPointerLeave
  • onPointerOver
  • onPointerOut

因為平時接觸較少,所以沒怎麼用過,就用官方Demo給大家看看吧,一定要升級到14以上哦,否則沒有這些屬性,感興趣的深入研究研究,畢竟這篇文章目的就是讓自己瞭解一下新特性~

官方demo效果如下:

React 16.0+ 新特性初探(How to use)

【坑來了】:我自信滿滿的升級到Firefox和chrome到最新版本,然後把官方demo跑了一下,但是WTF?是下面這樣的結果。。。

React 16.0+ 新特性初探(How to use)
很明顯,這些屬性依然不能被支援,也可能是我自己的問題?不清楚了,反正就是不能用。然後呢,我就查唄,讓我查到了這個東東 —— react-pointable,

// 首先,安裝包
yarn add react-pointable
// 然後程式碼變成下面
import Pointable from 'react-pointable';
...
<Pointable
  style={circleStyle}
  onPointerDown={this.onDown}
  onPointerMove={this.onMove}
  onPointerUp={this.onUp}
  onPointerCancel={this.onUp}
  onGotPointerCapture={this.onGotCapture}
  onLostPointerCapture={this.onLostCapture}
/>
複製程式碼

這個包就是一種polyfill吧,按照我的理解,它最後渲染出來的效果就是官方程式碼那個樣子。 然後看下執行效果:

React 16.0+ 新特性初探(How to use)
OK,可以動了!等一下~官方Demo摁住和鬆開的時候會變顏色,這裡沒變顏色,開啟控制檯發現還是有兩個報錯:

React 16.0+ 新特性初探(How to use)
嗯,原來是6個,現在變成了兩個,說明還是解決了一部分問題,這是為啥呢,原來官方文件說了:它支援的事件如下,但是並不支援官方Demo裡面的onGotPointerCapture和onLostPointerCapture。

React 16.0+ 新特性初探(How to use)

原本我想提個issue來的,O(∩_∩)O哈哈~,但是發現好像有人提了,反正暫時不支援就對了。也可能是我配置的不對?因為官方demo確實可以執行,而且瀏覽器版本也都支援pointer events事件,如果有大牛給我解答還是萬分感謝的~

總結

升級react日後應該是必然的事情,所以提前瞭解一下還是有幫助的,作為使用者暫時不做深入分析,當然,我也分析不明白,單純從更新角度來寫幾個demo給大家看一下變化,應該還挺清楚的~感謝閱讀!

相關文章