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

跨页面(Tab和window)通信

本文介绍两种跨页面通信解决方案,可应用于以下四个使用场景。

场景一

编辑场景,在页面 A打开B页面, 在B页面操作数据,关闭B同时刷新页面 A 的数据;

用户通过一些筛选条件过滤出一些主题,且翻到了某一页,这时对某个主题进行编辑,编辑页是一个非常复杂的页面,不适合弹窗展示,因此打开了一个新的页面。

用户点击编辑,新打开一个页面,注意是window.open打开一个新的tab,而非在原来页面上更换url。

用户点击“保存”,更新了数据。

这时,合理的交互可以是:关闭新打开的编辑页,回到列表页,同时保持列表页的筛选条件和页码不变,并局部刷新列表的数据。

image.png | center | 2866x1538

image.png | center | 2240x940

场景2

编辑场景,在页面 A打开B页面, 在B页面做取消操作,关闭B同时回到A,A不刷新;

用户进行编辑主题操作(前置操作同场景1),并点击“取消”。

合理的交互可以是:关闭新打开的编辑页,列表页停留在进入编辑页前的状态,且不刷新数据。

场景3

添加场景,改变页面 A到B,B做数据操作提交或取消,页面由B回到A;

用户进行添加主题操作,并点击“保存”。

合理的交互可以是:改变当前列表页的url为添加页,在用户点击“保存”或“取消”时,跳转回列表页,筛选条件和页码都是初始状态(从1开始)。

场景4

同页面间的tab切换,TabA更新数据后,切换到TabB,B的数据需要同步

用户进入标签系统,在可用标签,待审核标签和不可用标签Tab之间切换,在某个tab页面操作完数据后,其他的tab内的数据需要同步。与前三个场景不同的是,前三个场景中,编辑页和列表页是两个不同的页面,而场景4中的tab共属于同一个页面,不同的tab内是不同的组件。

image.png | center | 2290x928

模型一:不同页面间的通信-storage事件触发

window有一个StorageEvent,每当localStorage改变的时候可以触发这个事件。(这个原理就像你给一个DOM绑定了click事件,当你点击它的时候,就会自动触发。)

每当一个页面改变了localStorage的值,都会触发StorageEvent事件。也就是说可以通过改变localStorage的值,来实现浏览器中跨页面( tab / window )之间的通讯。记住这个事件只有在localStorage发生改变的时候才会被触发,如果没改变则不会触发此事件。

1
2
3
4
5
6
7
8
9
10
11
//列表页 index.js 相关代码
...
componentDidMount(){
//添加对storge的监听事件 及时刷新页面
window.addEventListener('storage', (event)=>{
if(event.key === 'update_scg_list'){
//页面更新操作
this.refresh();
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//编辑页需要以window.open的方式打开编辑页,只有window.open打开的页面才能适用window.top.close()关闭
//antd table设置columns
this.columns = [
...
{
title: "操作",
key: "action",
render: record => {
return <a key="edit"
onClick={()=>window.open('...')} target='_blank'
>
编辑
</a>
}
}
]
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
42
43
44
45
//编辑页 index.js 相关代码
...
//点击取消 回到列表页面 场景3
handleCancelSubmit = () => {
//编辑 场景1
if(SCG_DATA){
window.top.close();
}
//添加 场景2
else{
const type = this.props.type || "";
location.href = `/scg/video/option.htm?from=baoluo&type=${type}`;
}
};
//更新localstorage 通知列表页面刷新 同时关闭自己
afterSaveProcess = ()=>{
if (localStorage) {
//为保证每次页面A都执行,此处需要设置一个随机字符串
localStorage.setItem('update_scg_list', randomId());
if(SCG_DATA){
setTimeout(()=>{
window.top.close();
}, 2000);
}
else{
location.href = `/scg/video/option.htm?from=baoluo&type=${this.props.type}`;
}
}
}
...
//提交表单 场景1+2
submit = value => {
...
IO.post(url, params)
.then(response => {
if (response.success) {
...
//提交成功后,通知列表页
this.afterSaveProcess();
}
...
})
...
};

注意这个方案在chrome中只能在不同页面之间生效,同个页面中监听事件无效。
参考:https://github.com/lin-xin/blog/issues/11

模型二:同页面消息通信-观察者模式

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
42
43
44
45
46
47
48
49
//全局观察tab切换
//事件集合
let events = {};
// 发布事件
const trigger = (event, ...data) => {
const fns = events[event];
// 如果没有对应方法
if (!fns || fns.length === 0) {
return false;
}
// 如果存在对应方法,依次执行
for ( let i = 0; i <= fns.length - 1; i++) {
fns[i](...data);
}
};
// 监听事件
const on = (event, fn) => {
// 如果尚没有该事件,创建一个数组来存储对应的方法
if (!events[event]) {
events[event] = [];
}
events[event].push(fn);
};
// 取消监听事件
const off = (event, fn) => {
const fns = events[event];
// 如果不存在事件集合
if (!fns) {
return false;
}
// 如果不存在事件
if (!fn && fns) {
fns.length = 0;
}
// 取消指定事件
else {
for (let i = fns.length - 1; i >= 0; i--) {
if (fn === fns[i]) {
fns.splice(i, 1);
}
}
}
};
const PubSub = {
on: on,
off: off,
trigger: trigger
};
export default PubSub;
1
2
3
4
5
6
7
//页面入口 相关代码
...
//tab切换时触发
onChange = activeKey => {
PubSub.trigger('tagChange',activeKey)
this.setState({ activeKey })
};
1
2
3
4
5
6
7
8
9
//每个tab组件中添加订阅事件
componentDidMount(){
//每次tab切换时,接收当前的activekey,如果是自己的key就刷新自己
PubSub.on('tagChange',(activeKey)=>{
if(activeKey === '2'){
this.refresh();
}
})
}

总结

在不使用redux等状态管理框架的情况下,多页面应用可使用模式一和模式二两种方式解决通信问题, 模式一适用于场景一,场景2和场景3;
模式二适用于场景四。