react v16新特性

Fiber架构是怎么回事

什么是fiber

当用户和网页应用进行交互时,如果有很多渲染任务要执行,会有两个问题难以解决:
(1) V16前react的渲染动作是同步的,比如:画一个组件,这个组件有很多的子组件,渲染这个超级组件用的时间就可能会很长,且这个过程无法被打断,整个过程中,浏览器那个唯一的主线程都在专心运行更新操作,用户做的其他交互都是无反应的,也无法渲染别的组件。这就是所谓的界面卡顿,造成不友好的用户体验。
(2) 渲染动作没有优先级的,同时来78个组件,先渲染谁后渲染谁,得到的结果可能不是我们预期的。
Fiber是一个架构,解决的就是上面这两个问题。

Fiber的方式

Fiber的核心思想就是分片,将同步的工作分成一个个的chunck,且chunck是可以被打断的。
v16之前是Stack Reconciler,v16之后是Fiber Reconciler.
Fiber Reconciler是指,React Fiber把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

Fiber对现有代码的影响

因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。
image.png
以render函数为界,如下图
image.png
phase1里面的生命周期函数都有可能被执行多次,因为是可被打断的。用了fiber之后,render之前的函数最好都是纯函数。
phase2状态是不会被打断的。
也就是说,在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用。

我们挨个看一看这些可能被重复调用的函数。
componentWillReceiveProps,即使当前组件不更新,只要父组件更新也会引发这个函数被调用,所以多调用几次没啥,通过!

shouldComponentUpdate,这函数的作用就是返回一个true或者false,不应该有任何副作用,多调用几次也无妨,通过!

render,应该是纯函数,多调用几次无妨,通过!

只剩下componentWillMount和componentWillUpdate这两个函数往往包含副作用,所以当使用React Fiber的时候一定要重点看这两个函数的实现。

对于fiber的理解

1.react之外的工作可以有机会做,例如用户在input中的输入,鼠标的移动等。
2.react自己的任务让高优先级的任务可以抢先做。
3.Fiber对于渲染速度的提高可能并没不明显,但其最大的贡献应该是能让用户感知到的性能大大提高。

更好的错误处理

在V16之前,一旦报错,整个组件就会被unmout,页面可能就一片空白了。
在V16中,react新增加了componentDidCatch生命周期,能够捕捉到任何子组件的生命周期函数中抛出的error,从而使用户能够在定义了这个componentDidCatch的父组件里恢复现场。值得注意的是,这个函数只能捕捉子组件的错误,自己组件内错误没法捕捉到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
componentDidCatch(errorString, errorInfo) {
this.setState({
error: errorString
});
ErrorLoggingTool.**log**(errorInfo);
}
render() {
if (this.state.error) {
return <div>Error: {this.state.error}</div>;
} else {
return this.props.children;
}
}

Portal的应用

什么是Portal

Portal翻译过来就是传送门,作用就是把任意一段jsx送到某个node上,即把一段jsx传到一个组件里面去render,实际改变的是网页上另一处的DOM结构。

为什么React需要传送门

以实现一个Dialog为例,从用户感知角度,dialog应该是一个独立的组件,通常应该显示在屏幕的最中间,现在Dialog被包在其他组件中,要用CSS的position属性控制Dialog位置,同时,希望他的样式不被其他元素干扰且Dialog的内容又是可定制的,这个时候Portal就可以派上用场了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react';
import {createPortal} from 'react-dom';
class Dialog extends React.Component {
constructor() {
super(...arguments);
//利用原生API来在body上创建一个div,这个div的样式绝对不会被其他元素的样式干扰。
this.node = document.createElement('div');
}
componentDidMount() {
// Append the element into the DOM on mount. We'll render
// into the modal container element (see the HTML tab).
document.body.appendChild(this.node);
}
render() {
// Use a portal to render the children into the element
return ReactDOM.createPortal(
// Any valid React child: JSX, strings, arrays, etc.
this.props.children,
// A DOM element
this.el,
);
}
componentWillUnmount() {
document.body.removeChild(this.node);
}
}

render可返回数组和字符串的意义

react规定jsx必须有一个根节点的js表达式, V16之后支持返回数组和字符串,下面是几个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// React是不可能支持这样的语法的
const renderMultiple = () => (
<div>A</div><div>B</div><div>C</div>
);
// React只可能支持这样的语法的
const renderMultiple = () => [
<div>A</div>,
<div>B</div>,
<div>C</div>
];
// 数组字符串混合玩法
const RenderArray = () => [
<div>A</div>,
<div>B</div>,
<div>C</div>,
];
const RenderString = () => 'Hello world';
const RenderArrayOfString = () => [
'A',
'B',
'C',
];
const RenderArrayOfArray = () => [
[
<div>S1</div>,
<div>S2</div>,
],
[
<div>What</div>,
<div>Ever</div>,
],
[
'Hello',
'World',
]
];

无论怎么边,key这个原则依然存在,动态大小数组依然是需要key的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 这个组件的唯一作用就是免去子组件使用key属性
const Wrap = (props) => props.children;
const renderDynamic = () => {
return [1, 2, 3].map(i => <div>{i}</div>); //这里会出warning,因为没有key
}
const Demo = () => (
<Wrap>
{renderDynamic()}
<div>hello</div>
<div>world</div>
</Wrap>
);
//HOC写法
// v16之前必须这样
const hoc = (Component) => {
return (props) => (
<div>
<Component {...props} />
<div>extra content</div>
</div>
);
};
// v16之后可以这样
const hoc = (Component) => {
return (props) => [
<Component {...props} />
<div>extra content</div>
];
};

更强的服务器端渲染

不再关心 checksum 是否相等,尽量复用dom

v16之前的react的SSR非常严格,服务端渲染一次产生html,会在跟元素上生成一个checksum,类似一个验证码,到浏览器端,react会重新做一遍这个过程并重新产生一个checksum,再将这两个checksum进行比较,如果一样,则什么都不做,如果不一样,则会报错,同时将把服务端产生的html全都扔掉,把自己在浏览器端渲染的html替换上去,页面会闪一下。

也就是说,在V16之前,是否复用 server rendering 创建的 DOM 元素,是根据 [data-reactroot] 元素上的 data-react-checksum 属性与前端渲染内容的 checksum 是否一致这个条件来判断的。即:只有完全复用和完全不复用两种情况

V16版本放宽了限制:产生html,但不生成checksum了,新增加了 ReactDOM.hydrate(…) 这个方法,把与 server rendering 相关的处理从 ReactDOM.render(…) 中拆分了出来。

hydrate不再要求两端一致了,不一致就忽略掉,能重用的就尽量重用。

image.png

支持streaming

渲染一个很大的html结构时候,可以流式的方式产生多少推送多少,而不用把整个html都产生完全之后再一次性推送

服务端不产生Virtual DOM,portal不好使

v16中,服务端不再产生Virtual DOM,错误处理componentDidCatch在服务器渲染时是没用的,portal也是没用的。

关于性能提升

1
process.env.NODE_ENV=production //提高SSR性能

对于服务端渲染而言,性能其实不是最大的问题,如何容易的进行两端同构才是关键问题。

可定制JSX元素标签

html5鼓励在自定义属性前加上data-前缀,react也推荐这样。所以在v16之前,react对自定义attribute进行了限制,设置了一个attribute白名单进行过滤,没有加data前缀的且不在这个白名单中的都不会生效。

V16版本把这个限制给去掉了,但是仍然限制用户使用驼峰的写法来定义属性。针对这个改动,可能最大的好处是把jquery代码迁移到react时更为方便了吧,毕竟原来jquery代码里面很多定义的attribute,在V16之前,改成react就不好使了。

当然还是推荐大家使用data-前缀来定义attribute和html5标准看齐。

1
2
3
4
5
6
7
8
9
// 在HTML中
<meta charset="utf-8">
<div tabindex="-1"></div>
//在JSX中
<meta charSet="utf-8" />
<div tabIndex="-1"></div>
<div data-foo="bar">what</div>

选择合适的升级策略

V16是react的一个breaking change,现在很多第三方的react库都还没有进行16版本的升级,而且对于一套完整的业务解决方案而言,立即升级这个新版本是存在风险的。总结来说,针对V16的升级策略可以遵循以下三个原则:

  1. 已经在生产环境的产品项目不要立即升级;
  2. 密切关注项目依赖以及比较著名的第三方library的升级情况,比如React Bootstrap,antd等;
  3. 新写代码尽量按照v16的特性来写,比如注意在componentWillMount和componentWillUpdate这两个函数中不要有side effect。

参考文章

React Fiber Architecture

Lin Clark - A Cartoon Intro to Fiber - React Conf 2017

传送门:React Portal

React v16.0

React 16: hydrate