1. 程式人生 > >重拾React: React 16.0

重拾React: React 16.0

前言

  首先歡迎大家關注我的Github部落格,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵,希望大家多多關注呀!從今年年初離開React開發崗,React就慢慢淡出我的學習範圍。現在想重拾一下React相關的知識,可能文章所提及的知識點已經算是過時了,僅僅算作是自己的學習體驗吧,   

React 16.0  

  React 16.0釋出於2017年九月,此次新版本作為一次大的版本升級,為我們許多新特性以及全新的內部架構,分別瞭解一下:

新的JavaScript環境支援

  React依賴於ES6中的MapSet型別以及requestAnimationFrame

函式(requestAnimationFrame函式用來告知瀏覽器在每次動畫重繪之前都呼叫給定的回撥函式),如果你需要支援IE11以下的老版本瀏覽器和裝置,React原生不再提供支援,必須引入polyfill。

  對於MapSet,我們可以在全域性引入core-js處理,對於requestAnimationFrame而言,我們可以通過引入raf:

import 'core-js/es6/map';
import 'core-js/es6/set';
import 'raf/polyfill';

import React from 'react';
import ReactDOM from 'react-dom'
; ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('root') ); 複製程式碼

新特性

元件返回

  React之前的版本中,元件render的返回值必須包含在一個根元素,因此我們經常都是將其包裹在一個div標籤中,在React16中我們直接在render函式中返回字串陣列

  比如存在下面的場景,假設有以下兩個元件:

class Row extends Component{
    render() {
        return (
            <div
>
<td>React</td> <td>Vue</td> <td>Angular</td> </div>
); } } class Table extends Component{ render() { return ( <table <tr> <Row /> </tr> </table> ); } } 複製程式碼

  在之前的版本中元件僅能返回一個根元件,Row中的元件不得已只能用div標籤包裹,但是因為tddiv包裹會導致瀏覽器無法識別,當然我們可以將tr挪到Row中,但是React 16.0提供了直接返回陣列的形式,因此我們可以直接方便的寫成:

class Row extends Component{
    render() {
        return [
            <th>React</th>,
            <th>Vue</th>,
            <th>Angular</th>
        ];
    }
}
複製程式碼

  在元件中直接返回字串相當於直接建立匿名文字。

異常處理處理

  React 16.0 增強了異常的處理能力,在之前的React中,元件內部的錯誤可能會使得狀態發生錯亂從而導致下一次渲染髮生未知的錯誤,然而React沒有提供能優雅地捕捉這些錯誤並且從中恢復的方式。試想,部分程式的錯誤不應該干擾整個應用的流程,因而React16引入了新的概念: Error boundaries(錯誤邊界)。

所謂的錯誤邊界(Error boundaries )是指能夠捕獲子孫元件中錯誤,並提供列印這些錯誤和展示錯誤UI介面的元件。錯誤邊界能夠捕捉子孫元件render方法、生命週期以及建構函式中的錯誤。

  舉個例子:

class MyComponent extends Component {
    render(){
        throw new Error('I crashed!');
        return "MrErHu";
    }
}

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

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
    }

    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
    }
}

export default class App extends Component {
    render() {
        return (
        <ErrorBoundary>
            <MyComponent />
        </ErrorBoundary>
        );
    }
}
複製程式碼

  如上所示,含有componentDidCatch的元件被稱為錯誤邊界,其功能類似於JavaScript中的catch。值得注意是的,錯誤邊界僅僅能夠捕捉子孫元件的錯誤而不誤捕獲自身的錯誤。React 16.0引入了一個新的行為,任何未被捕獲的錯誤都會解除安裝整個React元件樹,雖然這個行為富有爭議,但React開發者們認為即使什麼也不顯示,也比顯示一堆錯誤更好。當然了,錯誤邊界僅能捕捉我們上面所提到特定位置的錯誤,如果是事件處理中的錯誤,你還是得使用JavaScript的trycatch

createPortal

  React 16之前,並沒有提供Portal的功能,如果需要渲染類似於對話方塊的元件則必須藉助於unstable_renderSubtreeIntoContainerunmountComponentAtNode,例如我們想要實現一個對話方塊Dialog的元件:

class Dialog extends React.Component {
    render() {
        return null;
    }

    componentDidMount() {
        const doc = window.document;
        this.node = doc.createElement('div');
        doc.body.appendChild(this.node);

        this.renderPortal(this.props);
    }

    componentDidUpdate() {
        this.renderPortal(this.props);
    }

    componentWillUnmount() {
        unmountComponentAtNode(this.node);
        window.document.body.removeChild(this.node);
    }

    renderPortal(props) {
        unstable_renderSubtreeIntoContainer(
            this,
            <div class="dialog">
                {props.children}
            </div>,
            this.node
        );
    }
}
複製程式碼

  我們知道對話方塊是非常特殊的一種情況,不能渲染在父元件內而是需要直接渲染在body標籤下,為了解決了這個問題,在上面的程式碼中render實際上並沒有返回任何元件,而是在componentDidMount生命週期中利用unstable_renderSubtreeIntoContainer方法將對應元件直接渲染在this.node下。需要注意的是,unstable_renderSubtreeIntoContainer渲染的元件需要手動解除安裝,否則可能會造成記憶體洩露,因此我們在componentWillUnmount中手動呼叫unmountComponentAtNode

  有ReactDom.createPortal,一切都變得簡單的起來,既不需要手動去解除安裝元件,也不需要擔心unstable的API會在後續的版本中移出,上面的例子,在React 16.0可以如下實現:

class Dialog extends React.Component {
    constructor(props) {
        super(props);
        const doc = window.document;
        this.node = doc.createElement('div');
        doc.body.appendChild(this.node);
    }

    render() {
        return createPortal(
            <div class="dialog">
                {this.props.children}
            </div>,
            this.node
        );
    }

    componentWillUnmount() {
        window.document.body.removeChild(this.node);
    }
}
複製程式碼

renderToNodeStream

  React伺服器渲染在React 16.0之前僅僅支援renderToString,後端用字串的方式將渲染好的HTML傳送給客戶端,而React 16.0則提供了renderToNodeStream,返回一個可讀流,二者有什麼區別?

// using renderToString
import { renderToString } from "react-dom/server"
import App from "./App"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>App</title></head><body>");
  res.write("<div id='content'>");  
  res.write(renderToString(<App/>));
  res.write("</div></body></html>");
  res.end();
});
複製程式碼
// using renderToNodeStream
import { renderToNodeStream } from "react-dom/server"
import App from "./App"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>App</title></head><body>");
  res.write("<div id='content'>"); 
  const stream = renderToNodeStream(<App/>);
  stream.pipe(res, { end: false });
  stream.on('end', () => {
    res.write("</div></body></html>");
    res.end();
  });
});
複製程式碼

  回答這個問題之前,我們需要了解一下什麼是流(Stream),對於從事前端的同學而言,流這個概念相對比較陌生,流本質上是對輸入輸出裝置的抽象,比如:

ls | grep *.js
複製程式碼

  ls產生的資料通過管道符號(|)流向了grep命令中,資料就像水流一樣在管道符號中流動。裝置流向程式我們稱為readable,程式流向裝置我們稱為writable,我們舉一個例子:

const fs = require('fs');
const FILEPATH = './index';

const rs = fs.createReadStream(FILEPATH);
const ws = fs.createWriteStream(DEST);

rs.pipe(ws);
複製程式碼

  資料通過管道中從rs流向了ws,實現了複製的功能,並且資料在管道流動的過程中我們還可以對資料進行處理。那麼流有哪些優點呢?首先資料不需要一次性從裝置全部拿出,然後再寫入另外一個裝置。流可以實現一點點的放入記憶體中,一點點的存入裝置,帶來的就是記憶體開銷的下降。並且我們可以在管道中優雅的處理資料,方便程式拓展。

  講了這麼多流的優點,renderToNodeStream為伺服器渲染帶來了什麼呢?首先同樣的道理,renderToNodeStream可以降低渲染伺服器的記憶體消耗,更重要的是帶來TTFB的降低。

TTFB(Time to First Byte):瀏覽器從最初的網路請求被髮起到從伺服器接收到第一個位元組前所花費的毫秒數

  我們知道HTTP協議在傳輸層使用的TCP協議,而TCP協議每次會將應用層資料切割成一個個報文傳輸,因此使用流不必等待所有的渲染完成才傳輸,可以有效降低TTFB

非標準DOM屬性的支援

  在React 16之前,React會忽視非標準DOM屬性,例如:

<div mycustomattribute="something" />
複製程式碼

  在React 15中僅會輸出:

<div />
複製程式碼

  在React 16中則會輸出:

<div mycustomattribute="something" />
複製程式碼

  允許使用非標準DOM屬性使得在整合第三方庫或者嘗試新的DOM API時更加的方便。

其他變化

  關於setState函式,setState(null)將不會再觸發更新,因此如果是以函式作為引數的形式呼叫setState,可以通過返回null的方式控制組件是否重新渲染,例如:

this.setState(function(state) {
    return null;
})
複製程式碼

  需要注意的是,與之前不同,如果在render中直接呼叫setState會觸發更新,當前實際的情況是,你也不應該在render中直接觸發setState。並且,之前的setState的回撥函式(第二個引數)是在所有元件重新渲染完之後呼叫,而現在會在componentDidMountcomponentDidUpdate後立即呼叫。

  關於生命週期中,如果一個元件從<A>被替換成<B>,那麼React 16中B元件的componentWillMount一定總是先於A元件的componentWillUnmount,但是在React 16之前的版本某些情況下可能是相反的順序。還有,componentDidUpdate方法不會再接收到prevContext的引數。

關於React Fiber

  React歷經兩年的核心程式碼重構,在16.0中推出了矚目的React Fiber

  React最引以自豪的應該就是Virtual Dom了,Virtual Dom的運用首先使得我們前端編碼的難度大大降低,所需要考慮的只有在特定狀態描述UI介面,也不需要考慮瀏覽器該如何處理。其次,正是因為Virtual Dom的引入,使得React具備了跨平臺的能力,既可以在瀏覽器執行(React Dom),也可以在移動端裝置上執行(React Native),也就是React所宣稱的:

Write once, run anywhere

  順著這個思路往下走,其實React的實現分為兩個部分:

  • 不同狀態下不同的UI描述,React需要對比前後UI描述的差異性,明白介面到底實際發生了什麼改變,這個過程在React中被稱為Reconciler。React 16.0版本之前屬於Stack Reconciler,現在則是Fiber Reconcile
  • 第二個則是Virtual Dom對真實環境的對映,在React Dom中是對瀏覽器的對映,在移動端是對特定平臺(iOS、Andriod)的對映,這部分屬於外掛式實現,並不屬於React核心程式碼。

  正如上圖所示,React執行時首先會根據返回的JSX建立對應的Element,用以描述UI介面。然後通過Element則會對應建立元件例項Instance,也就是我們所說的Virtual Dom,最後通過Virtual Dom去對映真實的瀏覽器環境。在首次渲染之後,後序的更新Reac只需要找到(Reconciler)兩次Virtual Dom的差異性(diff),然後通過diff去更新真實DOM,這樣就實現了增量更新真實DOM,畢竟DOM的操作是非常昂貴的。

  然而之前的Stach Reconcile相當於從最頂層的元件開始,自頂向下遞迴呼叫,不會被中斷,這樣就會持續佔用瀏覽器主執行緒。眾所周知,JavaScript是單執行緒執行,長時間佔用主執行緒會阻塞其他類似於樣式計算、佈局繪製等運算,從而出現掉幀的情況。

  Fiber Reconcile力圖解決這個問題,通過將Reconcile進行拆分成一個個小任務,當前任務執行結束後即使還有後序任務沒有執行,也會主動交還主執行緒的控制權,暫時將自己掛起,等到下次獲得主執行緒的控制權時再繼續執行,不僅如此,Fiber還可以對任務通過優先順序進行排序,優先進行那些至關重要的操作,是不是非常類似作業系統的程序排程演算法。這樣做的好處就是其他類似於頁面渲染的操作也能獲得執行,避免因此造成卡頓。

  當然至於Fiber是如何實現如此強大的功能,已經超過文章的討論範圍,目前也超過了本人的能力範圍。不過,React 16帶來的效能改善和一系列新特性都讓我欣喜。重新使用React,看到如此多的變化,不禁想說一句:真香!