前言
首先欢迎大家关注我的 GitHub 博客 https://github.com/MrErHu/blog , 也算是对我的一点鼓励, 毕竟写东西没法获得变现, 能坚持下去也是靠的是自己的热情和大家的鼓励, 希望大家多多关注呀! 从今年年初离开 React 开发岗, React 就慢慢淡出我的学习范围. 现在想重拾一下 React 相关的知识, 可能文章所提及的知识点已经算是过时了, 仅仅算作是自己的学习体验吧,
React 16.0
React 16.0 发布于 2017 年九月, 此次新版本作为一次大的版本升级, 为我们许多新特性以及全新的内部架构, 分别了解一下:
新的 JavaScript 环境支持
React 依赖于 ES6 中的 Map 与 Set 类型以及 requestAnimationFrame 函数(requestAnimationFrame 函数用来告知浏览器在每次动画重绘之前都调用给定的回调函数), 如果你需要支持 IE11 以下的老版本浏览器和设备, React 原生不再提供支持, 必须引入 polyfill.
对于 Map 与 Set, 我们可以在全局引入 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 标签包裹, 但是因为 td 被 div 包裹会导致浏览器无法识别, 当然我们可以将 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 的 try 和 catch.
createPortal
React 16 之前, 并没有提供 Portal 的功能, 如果需要渲染类似于对话框的组件则必须借助于 unstable_renderSubtreeIntoContainer 与 unmountComponentAtNode, 例如我们想要实现一个对话框 Dialog 的组件:
- class Dialog extends React.Component {
- render() {
- return null;
- }
- componentDidMount() {
- const doc = Windows.document;
- this.node = doc.createElement('div');
- doc.body.appendChild(this.node);
- this.renderPortal(this.props);
- }
- componentDidUpdate() {
- this.renderPortal(this.props);
- }
- componentWillUnmount() {
- unmountComponentAtNode(this.node);
- Windows.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 = Windows.document;
- this.node = doc.createElement('div');
- doc.body.appendChild(this.node);
- }
- render() {
- return createPortal(
- <div class="dialog">
- {this.props.children}
- </div>,
- this.node
- );
- }
- componentWillUnmount() {
- Windows.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 的回调函数 (第二个参数) 是在所有组件重新渲染完之后调用, 而现在会在 componentDidMount 和 componentDidUpdate 后立即调用.
关于生命周期中, 如果一个组件从 < 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, 看到如此多的变化, 不禁想说一句: 真香!
来源: https://juejin.im/post/5c0c7504518825501076b49f