盤點 React 16.0 ~ 16.5 主要更新及其應用

大約一年前,React 團隊發佈了 React 16.0。時至今日,已更新到 16.5 。這其中有很多激動人心的特性(如 Fiber 架構的引入、新的周期函數、全新 Context API、Fragment、Error Boundary、Portal 等)都值得開發者跟進學習。本文就以 React 更新日誌 爲引,選取幾個重要且用於工做的更新,和你們一塊兒學習。全部示例代碼在 react-upgrade-examples, 配合文章一塊兒食用更佳~ 😆

目錄

0. 生命週期函數的更新

1. 全新的 Context API

2. React Strict Mode

3. Portal

4. Refs

5. Fragment

6. 其餘

7. 總結


生命週期函數的更新

隨着 React 16.0 發佈, React 採用了新的內核架構 Fiber,在新的架構中它將更新分爲兩個階段:Render Parse 和 Commit Parse, 也由此引入了 getDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidCatch 等三個生命週期函數。同時,也將 componentWillMountcomponentWillReceivePropscomponentWillUpdate 標記爲不安全的方法。javascript

new lifecyclecss

新增html

  • static getDerivedStateFromProps(nextProps, prevState)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • componentDidCatch(error, info)

標記爲不安全java

  • componentWillMount(nextProps, nextState)
  • componentWillReceiveProps(nextProps)
  • componentWillUpdate(nextProps, nextState)

static getDerivedStateFromProps(nextProps, prevState)

根據 getDerivedStateFromProps(nextProps, prevState) 的函數簽名可知: 其做用是根據傳遞的 props 來更新 state。它的一大特色是 無反作用 : 因爲處在 Render Phase 階段,因此在每次的更新都要觸發, 故在設計 API 時採用了靜態方法,其好處是單純 —— 沒法訪問實例、沒法經過 ref 訪問到 DOM 對象等,保證了單純且高效。值得注意的是,其仍能夠經過 props 的操做來產生反作用,這時應該將操做 props 的方法移到 componentDidUpdate 中,減小觸發次數。react

例:git

state = { isLogin: false }

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

componentDidUpdate(prevProps, prevState){
  if(!prevState.isLogin && prevProps.isLogin) this.handleClose()
}

但在使用時要很是當心,由於它不像 componentWillReceiveProps 同樣,只在父組件從新渲染時才觸發,自己調用 setState 也會觸發。官方提供了 3 條 checklist, 這裏搬運一下:github

  1. 若是改變 props 的同時,有反作用的產生(如異步請求數據,動畫效果),這時應該使用 componentDidUpdate
  2. 若是想要根據 props 計算屬性,應該考慮將結果 memoization 化,參見 memoization
  3. 若是想要根據 props 變化來重置某些狀態,應該考慮使用受控組件

配合 componentDidUpdate 周期函數,getDerivedStateFromProps 是爲了替代 componentWillReceiveProps 而出現的。它將本來 componentWillReceiveProps 功能進行劃分 —— 更新 state 和 操做/調用 props,很大程度避免了職責不清而致使過多的渲染, 從而影響應該性能。web

getSnapshotBeforeUpdate(prevProps, prevState)

根據 getSnapshotBeforeUpdate(prevProps, prevState) 的函數簽名可知,其在組件更新以前獲取一個 snapshot —— 能夠將計算得的值或從 DOM 獲得的信息傳遞到 componentDidUpdate(prevProps, prevState, snapshot) 周期函數的第三個參數,經常用於 scroll 位置的定位。摘自官方的示例:npm

class ScrollingList extends React.Component {
  constructor(props) {
    super(props)
    // 取得dom 節點
    this.listRef = React.createRef()
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 根據新添加的元素來計算獲得所須要滾動的位置
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current
      return list.scrollHeight - list.scrollTop
    }
    return null
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 根據 snapshot 計算獲得偏移量,獲得最終滾動位置
    if (snapshot !== null) {
      const list = this.listRef.current
      list.scrollTop = list.scrollHeight - snapshot
    }
  }

  render() {
    return <div ref={this.listRef}>{/* ...contents... */}</div>
  }
}

componentDidCatch(error, info)

在 16.0 之前,錯誤捕獲使用 unstable_handleError 或者採用第三方庫如 react-error-overlay 來捕獲,前者捕獲的信息十分有限,後者爲非官方支持。而在 16.0 中,增長了 componentDidCatch 周期函數來讓開發者能夠自主處理錯誤信息,諸如展現,上報錯誤等,用戶能夠建立本身的Error Boundary 來捕獲錯誤。例:api

···

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

···

此外,用戶還能夠採用第三方錯誤追蹤服務,如 SentryBugsnag 等,保證了錯誤處理效率的同時也極大降級了中小型項目錯誤追蹤的成本。

圖片bugsnag

標記爲不安全 componentWillMountcomponentWillReceivePropscomponentWillUpdate

componentWillMount

componentWillMount 可被開發者用於獲取首屏數據或事務訂閱。

開發者爲了快速獲得數據,將首屏請求放在 componentWillMount中。實際上在執行 componentWillMount時第一次渲染已開始。把首屏請求放在componentWillMount 的與否都不能解決首屏渲染無異步數據的問題。而官方的建議是將首屏放在 constructorcomponentDidMount中。

此外事件訂閱也被常在 componentWillMount 用到,並在 componentWillUnmount 中取消掉相應的事件訂閱。但事實上 React 並不可以保證在 componentWillMount 被調用後,同一組件的 componentWillUnmount 也必定會被調用。另外一方面,在將來 React 開啓異步渲染模式後,在 · 被調用以後,組件的渲染也頗有可能會被其餘的事務所打斷,致使 componentWillUnmount 不會被調用。而 componentDidMount 就不存在這個問題,在 componentDidMount 被調用後,componentWillUnmount 必定會隨後被調用到,並根據具體代碼清除掉組件中存在的事件訂閱。

對此的升級方案是把 componentWillMount 改成 componentDidMount 便可。

componentWillReceivePropscomponentWillUpdate

componentWillReceiveProps 被標記爲不安全的緣由見前文所述,其主要緣由是操做 props 引發的 re-render。與之相似的 componentWillUpdate 被標記爲不安全也是一樣的緣由。除此以外,對 DOM 的更新操做也可能致使從新渲染。

對於 componentWillReceiveProps 的升級方案是使用 getDerivedStateFromPropscomponentDidUpdate 來代替。
對於 componentWillUpdate 的升級方案是使用 componentDidUpdate 代替。如涉及大量的計算,可在 getSnapshotBeforeUpdate 完成計算,再在 componentDidUpdate 一次完成更新。

經過框架級別的 API 來約束甚至限制開發者寫出更易維護的 Javascript 代碼,最大限度的避免了反模式的開發方式。

全新的 Context API

在 React 16.3 以前,Context API 一直被官方置爲不推薦使用(don’t use context),究其緣由是由於老的 Context API 做爲一個實驗性的產品,破壞了 React 的分形結構。同時在使用的過程當中,若是在穿透組件的過程當中,某個組件的 shouldComponentUpdate 返回了 false, 則 Context API 就不能穿透了。其帶來的不肯定性也就致使被不推薦使用。隨着 React 16.3 的發佈,全新 Context API 成了一等 API,能夠很容易穿透組件而無反作用,官方示例代碼:

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light')

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    )
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  )
}

function ThemedButton(props) {
  // Use a Consumer to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  return (
    <ThemeContext.Consumer>{theme => <Button {...props} theme={theme} />}</ThemeContext.Consumer>
  )
}

其過程大概以下:

  1. 經過 React.createContext 建立 Context 對象
  2. 在父組件上,使用<ThemeContext.Provider/> 來提供 Provider
  3. 在須要消費的地方,使用<ThemeContext.Consumer/> 以函數調用的方式{theme => <Button {...props} theme={theme} />}得到 Context 對象的值。

Context API 與 Redux

在狀態的管理上,全新的 Context API 徹底能夠取代部分 Redux 應用,示例代碼:

const initialState = {
  theme: 'dark',
  color: 'blue',
}

const GlobalStore = React.createContext()

class GlobalStoreProvider extends React.Component {
  render() {
    return (
      <GlobalStore.Provider value={{ ...initialState }}>{this.props.children}</GlobalStore.Provider>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <GlobalStoreProvider>
        <GlobalStore.Consumer>
          {context => (
            <div>
              <div>{context.theme}</div>
              <div>{context.color}</div>
            </div>
          )}
        </GlobalStore.Consumer>
      </GlobalStoreProvider>
    )
  }
}

全新的 Context API 帶來的穿透組件的能力對於須要全局狀態共享的場景十分有用,無需進入額外的依賴就能對狀態進行管理,代碼簡潔明瞭。

React Strict Mode

React StrictMode 能夠在開發階段發現應用存在的潛在問題,提醒開發者解決相關問題,提供應用的健壯性。其主要能檢測到 4 個問題:

  • 識別被標誌位不安全的生命週期函數
  • 對棄用的 API 進行警告
  • 探測某些產生反作用的方法
  • 檢測是否採用了老的 Context API

使用起來也很簡單,只要在須要被檢測的組件上包裹一層 React StrictMode ,示例代碼 React-StictMode

class App extends React.Component {
  render() {
    return (
      <div>
        <React.StrictMode>
          <ComponentA />
        </React.StrictMode>
      </div>
    )
  }
}

若出現錯誤,則在控制檯輸出具體錯誤信息:

React Strict Mode

Portal

由 ReactDOM 提供的 createPortal 方法,容許將組件渲染到其餘 DOM 節點上。這對大型應用或者獨立於應用自己的渲染頗有幫助。其函數簽名爲
ReactDOM.createPortal(child, container), child 參數爲任意的可渲染的 React Component,如 elementstingfragment 等,container 則爲要掛載的 DOM 節點.

以一個簡單的 Modal 爲例, 代碼見 Portal Modal :

import React from 'react'
import ReactDOM from 'react-dom'
const modalRoot = document.querySelector('#modal')

export default class Modal extends React.Component {
  constructor(props) {
    super(props)
    this.el = document.createElement('div')
  }
  componentDidMount() {
    modalRoot.appendChild(this.el)
  }
  componentWillUnmount() {
    modalRoot.removeChild(this.el)
  }
  handleClose = () => [this.props.onClose && this.props.onClose()]
  render() {
    const { visible } = this.props
    if (!visible) return null

    return ReactDOM.createPortal(
      <div>
        {this.props.children}
        <span onClick={this.handleClose}>[x]</span>
      </div>,
      this.el
    )
  }
}

具體過程就是使用了 props 傳遞 children後, 使用 ReactDOM.createPortal, 將 container 渲染在其餘 DOM 節點上的過程。

Refs

雖然 React 使用 Virtual DOM 來更新視圖,但某些時刻咱們還要操做真正的 DOM ,這時 ref 屬性就派上用場了。

React.createRef

React 16 使用了 React.createRef 取得 Ref 對象,這和以前的方式仍是有不小的差異,例:

// before React 16
···

  componentDidMount() {
    // the refs object container the myRef
    const el = this.refs.myRef
    // you can  also using ReactDOM.findDOMNode
    // const el = ReactDOM.findDOMNode(this.refs.myRef)
  }

  render() {
    return <div ref="myRef" />
  }
···
···
// React 16+
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }

  render() {
    return <div ref={this.myRef} />
  }
···

React.forwardRef

另一個新特性是 Ref 的轉發, 它的目的是讓父組件能夠訪問到子組件的 Ref,從而操做子組件的 DOM。
React.forwardRef 接收一個函數,函數參數有 propsref。看一個簡單的例子,代碼見 Refs:

const TextInput = React.forwardRef((props, ref) => (
  <input type="text" placeholder="Hello forwardRef" ref={ref} />
))
const inputRef = React.createRef()

class App extends Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }

  handleSubmit = event => {
    event.preventDefault()
    alert('input value is:' + inputRef.current.value)
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <TextInput ref={inputRef} />
        <button type="submit">Submit</button>
      </form>
    )
  }
}

這個例子使用了 React.forwardRefpropsref 傳遞給子組件,直接就能夠在父組件直接調用。

Fragment

在向 DOM 樹批量添加元素時,一個好的實踐是建立一個document.createDocumentFragment,先將元素批量添加到 DocumentFragment 上,再把 DocumentFragment 添加到 DOM 樹,減小了 DOM 操做次數的同時也不會建立一個新元素。

DocumentFragment 相似,React 也存在 Fragment 的概念,用途很相似。在 React 16 以前,Fragment 的建立是經過擴展包 react-addons-create-fragment 建立,而 React 16 中則經過 <React.Fragment></React.Fragment> 直接建立 'Fragment'。例如:

render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  )
}

如此,咱們不須要單獨包裹一層無用的元素(如使用<div></div>包裹),減小層級嵌套。
此外,還一種精簡的寫法:

render() {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  )
}

其餘

ReactDOMrender 函數能夠數組形式返回 React Component

render(){
  return [
    <ComponentA key='A' />,
    <ComponentB key='B' />,
  ]
}

移除內建的react-with-addons.js, 全部的插件都獨立出來

以前經常使用的react-addons-(css-)transition-group,react-addons-create-fragment,react-addons-pure-render-mixinreact-addons-perf 等,除部分被內置,其他所有都獨立爲一個項目,使用時要注意。

總結

窺一斑而見全豹,React 16.0 ~ 16.5 的升級給了開發者一個更爲純粹的開發流程。API 層面的更改、架構的更替、工具類的拆分都在爲構建更易維護的 JavaScript 應用而努力。擁抱變化,順應時勢。

因爲筆者能力有限,文中不免有疏漏,還望讀者不吝賜教。

以上。

Find me on Github

參考:

  1. React Docs
  2. Update on Async Rendering
  3. You Probably Don't Need Derived State
  4. React v16.3 版本新生命週期函數淺析及升級方案
  5. React 16: A look inside an API-compatible rewrite of our frontend UI library
  6. React Fiber Architecture
相關文章
相關標籤/搜索