重拾React: React 16.0

前言

  首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵,但願你們多多關注呀!從今年年初離開React開發崗,React就慢慢淡出個人學習範圍。如今想重拾一下React相關的知識,可能文章所說起的知識點已經算是過期了,僅僅算做是本身的學習體驗吧,
  javascript

React 16.0  

  React 16.0發佈於2017年九月,這次新版本做爲一次大的版本升級,爲咱們許多新特性以及全新的內部架構,分別瞭解一下:html

新的JavaScript環境支持

  React依賴於ES6中的MapSet類型以及requestAnimationFrame函數(requestAnimationFrame函數用來告知瀏覽器在每次動畫重繪以前都調用給定的回調函數),若是你須要支持IE11如下的老版本瀏覽器和設備,React原生再也不提供支持,必須引入polyfill。前端

  對於MapSet,咱們能夠在全局引入core-js處理,對於requestAnimationFrame而言,咱們能夠經過引入raf:java

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函數中返回字符串數組node

  好比存在下面的場景,假設有如下兩個組件:react

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提供了直接返回數組的形式,所以咱們能夠直接方便的寫成:git

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

  在組件中直接返回字符串至關於直接建立匿名文本。es6

異常處理處理

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

所謂的錯誤邊界(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,看到如此多的變化,不由想說一句:真香!

相關文章
相關標籤/搜索