重拾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,看到如此多的变化,不由想说一句:真香!

相关文章
相关标签/搜索