React 新特性

weixin_33806914發表於2019-03-07
  1. React 中一個常見模式是為一個元件返回多個元素。Fragments 可以讓你聚合一個子元素列表,並且不在DOM中增加額外節點。
class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

注意:①key 是唯一可以傳遞給 Fragment 的屬性。② 在 React 中,<></><React.Fragment><React.Fragment/> 的語法糖。

  1. Strict Mode嚴格模式
    StrictMode是一個用以標記出應用中潛在問題的工具。與Fragment類似,StrictMode不會渲染任何真實的UI。它為其後代元素觸發額外的檢查和警告。
import { StrictMode, Component } from 'react'

class Child extends Component {
  // 以下三個函式在 React v16.3 已不被推薦,未來的版本會廢棄。
  componentWillMount() {
    console.log('componentWillMount')
  }
  componentWillUpdate() {
    console.log('componentWillUpdate')
  }
  componentWillReceiveProps() {
    console.log('componentWillReceiveProps')
  }
  render() {
    return (
      <div />
    )
  }
}

export default class StrictModeExample extends Component {
  render() {
    return (
      <StrictMode>
        <Child />
      </StrictMode>
    )
  }
}

由於在StrictMode內使用了三個即將廢棄的API,開啟控制檯 ,可看到如下錯誤提醒:

4989175-8d2e8edf94a90d7c.png
控制檯報錯資訊

註釋:嚴格模式檢查只在開發模式下執行,不會與生產模式衝突。

  1. createRef (v16.3)
    (1) 老版本ref使用方式
    ① 字串形式: <input ref="input" />
    ② 回撥函式形式:<input ref={input => (this.input = input)} />
    (2) 字串形式缺點
    ① 需要內部追蹤 refthis 取值,會使 React 稍稍變慢。
    ② 有時候this與你想象的並不一致。
import React from 'react'

class Children extends React.Component {
  componentDidMount() {
    // <h1></h1>
    console.log('children ref', this.refs.titleRef)
  }
  render() {
    return (
      <div>
        {this.props.renderTitle()}
      </div>
    )
  }
}

class Parent extends React.Component {
  // 放入子元件渲染
  renderTitle = () => (
    <h1 ref='titleRef'>{this.props.title}</h1>
  )

  componentDidMount() {
    // undefined
    console.log('parent ref:', this.refs.titleRef)
  }
  render() {
    return (
      <Children renderTitle={this.renderTitle}></Children>
    )
  }
}

export default Parent

(3) createRef語法

import React from 'react'

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }
}

export default MyComponent
  1. 呼叫setState更新狀態時,若之後的狀態依賴於之前的狀態,推薦使用傳入函式形式。
    語法:setState((prevState, props) => stateChange, [callback])
    例如,假設我們想通過props.step在狀態中增加一個值:
this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});
  1. 錯誤邊界
    (1) 錯誤邊界用於捕獲其子元件樹 JavaScript 異常,記錄錯誤並展示一個回退的 UIReact 元件,避免整個元件樹異常導致頁面空白。
    (2) 錯誤邊界在渲染期間、生命週期方法內、以及整個元件樹建構函式內捕獲錯誤。
    (3) 元件如果定義了static getDerivedStateFromError()componentDidCatch()中的任意一個或兩個生命週期方法 。當其子元件丟擲錯誤時,可使用static getDerivedStateFromError()更新state,可使用componentDidCatch()記錄錯誤資訊。
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // 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>
  <MyWidget />
</ErrorBoundary>

註釋:getDerivedStateFromErrorcomponentDidCatch方法使用一個即可捕獲子元件樹錯誤。

  1. Portal
    (1) Portals 提供了一種很好的將子節點渲染到父元件以外的 DOM 節點的方式。
    (2) 通過 Portals 進行事件冒泡
    儘管 portal 可以被放置在DOM 樹的任何地方,但在其他方面其行為和普通的 React 子節點行為一致。一個從 portal 內部會觸發的事件會一直冒泡至包含 React 樹 的祖先。
import React from 'react';
import ReactDOM from 'react-dom';

class Modal extends React.Component {
  render() {
    return this.props.clicks % 2 === 1
      ? this.props.children
      : ReactDOM.createPortal(
        this.props.children,
        document.getElementById('modal'),
      );
  }
}

class Child extends React.Component {
  render() {
    return (
      <button>Click</button>
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      clicks: prevState.clicks + 1
    }));
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Number of clicks: {this.state.clicks}</p>
        <Modal clicks={this.state.clicks}>
          <Child />
        </Modal>
      </div>
    );
  }
}

export default Parent

註釋:當元件由portal渲染方式切換為普通渲染方式,會導致該元件被解除安裝之後重新渲染。元件放大功能如果通過portal方式實現,放大前的狀態(滾動位置、焦點位置等)無法保持。

  1. fiber介紹
    fiberReact 16中新的和解引擎。它的主要目的是使虛擬DOM能夠進行增量渲染。
    (1) 同步更新過程的侷限
    React決定要載入或者更新元件樹時,會做很多事,比如呼叫各個元件的生命週期函式,計算和比對Virtual DOM,最後更新DOM樹,這整個過程是同步進行的。瀏覽器那個唯一的主執行緒都在專心執行更新操作,無暇去做任何其他的事情。
    4989175-6b295ca95c2974d0.png
    更新過程的函式呼叫棧

    (2) React Fiber的方式
    破解JavaScript中同步操作時間過長的方法其實很簡單——分片。
    React Fiber把更新過程碎片化,執行過程如下面的圖所示,每執行完一段更新過程,就把控制權交還給React負責任務協調的模組,看看有沒有其他緊急任務要做,如果沒有就繼續去更新,如果有緊急任務,那就去做緊急任務。
    維護每一個分片的資料結構,就是Fiber
    4989175-6f3f928c50c4f630.png
    更新過程的函式呼叫棧

    (3) React Fiber更新過程的兩個階段
    React Fiber中,一次更新過程會分成多個分片完成,所以完全有可能一個更新任務還沒有完成,就被另一個更高優先順序的更新過程打斷,這時候,優先順序高的更新任務會優先處理完,而低優先順序更新任務所做的工作則會完全作廢,然後等待機會重頭再來。
    React Fiber一個更新過程被分為兩個階段(Phase):第一個階段Reconciliation Phase和第二階段Commit Phase
    在第一階段Reconciliation PhaseReact Fiber會找出需要更新哪些DOM,這個階段是可以被打斷的;但是到了第二階段Commit Phase,那就一鼓作氣把DOM更新完,絕不會被打斷。
    (4) React Fiber對現有程式碼的影響
    4989175-bbfba8211a547989.png
    image.png

    因為第一階段的過程會被打斷而且“重頭再來”,就會造成第一階段中的生命週期函式在一次載入和更新過程中可能會被多次呼叫!
    第一個階段的四個生命週期函式中,componentWillReceivePropscomponentWillMountcomponentWillUpdate這三個函式可能包含副作用,所以當使用React Fiber的時候一定要重點看這三個函式的實現。
    註釋:大家應該都清楚程式(Process)和執行緒(Thread)的概念,在電腦科學中還有一個概念叫做Fiber,英文含義就是“纖維”,意指比Thread更細的線,也就是比執行緒(Thread)控制得更精密的併發處理機制。
  2. 宣告週期變化
    v16.3 開始,原來的三個生命週期 componentWillMountcomponentWillUpdatecomponentWillReceiveProps 將被廢棄,取而代之的是兩個全新的生命週期:
    static getDerivedStateFromProps
    getSnapshotBeforeUpdate
  3. getDerivedStateFromProps用法
    static getDerivedStateFromProps(nextProps, prevState)
    元件例項化後接受新屬性時將會呼叫getDerivedStateFromProps。它應該返回一個物件來更新狀態,或者返回null來表明新屬性不需要更新任何狀態。
    如果父元件導致了元件的重新渲染,即使屬性沒有更新,這一方法也會被呼叫。
    如果你只想處理變化,你可能想去比較新舊值。呼叫this.setState()通常不會觸發getDerivedStateFromProps()
  4. getStapshotBeforeUpdate
    getSnapshotBeforeUpdate()在最新的渲染輸出提交給DOM前將會立即呼叫。它讓你的元件能在當前的值可能要改變前獲得它們。這一生命週期返回的任何值將會 作為引數被傳遞給componentDidUpdate()
class ScrollingList extends React.Component {
  listRef = React.createRef();

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the current height of the list so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      return this.listRef.current.scrollHeight;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    if (snapshot !== null) {
      this.listRef.current.scrollTop +=
        this.listRef.current.scrollHeight - snapshot;
    }
  }

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

在上面的例子中,為了支援非同步渲染,在getSnapshotBeforeUpdate 中讀取scrollHeight而不是componentWillUpdate,這點很重要。由於非同步渲染,在“渲染”時期(如componentWillUpdaterender)和“提交”時期(如getSnapshotBeforeUpdatecomponentDidUpdate)間可能會存在延遲。如果一個使用者在這期間做了像改變瀏覽器尺寸的事,從componentWillUpdate中讀出的scrollHeight值將是滯後的。

  1. Context用法
import React from 'react'

const ThemeContext = React.createContext();

const ThemedButton = (props) => (
  <ThemeContext.Consumer>
    { context => <span style={{color: context}}>{props.text}</span> }
  </ThemeContext.Consumer>
)

const Toolbar = () => (
  <ThemeContext.Provider value='red'>
    <ThemedButton text="Context API"/>
  </ThemeContext.Provider>
)

export default Toolbar

註釋:在 Context.Consumer 中,children必須為函式。

參考資料

React中文文件
深入React v16新特性(一)
深入React v16新特性(二)

相關文章