react

概览

虚拟DOM

什么是虚拟Dom?

一个JavaScript对象,通过对象的方式来表示DOM结构

优点

  • 将页面抽象为JS对象,配合不同的渲染工具,实现跨平台渲染

  • 将多次DOM修改的结果一次性的更新到页面上,有效的减少DOM的重绘重排,提高渲染性能

  • 保证性能下限的同时,省略手动DOM操作可以大大提高开发效率

缺点

  • 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢

  • 能保证性能下限,但是性能的极致优化不如DOM

jsx

定义:JSX即JavaScript XML,是Javascript和XML结合的一种格式

  • JSX是react开发的语法糖,一种JavaScript的语法扩展,运用于React架构;元素是React应用最小单位,JSX就是用来声明React当中的元素,React使用JSX来描述用户界面

特点:

  • 可以将HTML语言直接写在JavaScript语言之中

  • JSX允许直接在模板插入JavaScript变量

  • 防注入攻击

  • 在JSX中嵌入用户输入是安全的

  • React DOM在渲染之前默认会过滤所有传入的值。它可以确保应用不会被注入攻击。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止XSS(跨站脚本攻击)

  • Babel转译器会把JSX转换成一个名为React.createElement的方法调用

  • 如果在普通的html里面要写jsx语法,要将script的type改成text/jsx,JSXTransformer.js库是将JSX语法转为JavaScript语法

问题:

  • 在语法层面有更多灵活性,而高灵活性会带来不确定性,在性能优化上有所欠缺

  • 对于模版语言(vue),由于模版语法的约束,可以明确知道模版中的哪些是变量,进而来识别变量,并做相应的优化

生命周期

1、setup props and state

挂载阶段

1、componentWillMount 即将弃用:调用this.setState不会引起组件重新渲染,可以把这边的内容提前到constructor()中

2、render:返回一个React元素,React根据此元素去渲染出页面DOM。render是纯函数,不能在里面执行this.setState,会有改变组件状态的副作用

3、 componentDidMount: 组件挂载到DOM后调用,且只会被调用一次,接口请求使用

更新阶段API

componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render,componentDidUpdate

a、父组件重新render重传props,重新渲染,无论props是否有变化,shouldComponentUpdate 方法优化

b、在componentWillReceiveProps方法中,将props转换成自己的state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Child extends Component {
constructor(props) {
super(props);
this.state = {
count: props.count
};
}
// componentWillReceiveProps中 this.setState 将不会引起第二次渲染
componentWillReceiveProps(nextProps) {
this.setState({count: nextProps.count});
}
render() {
return <div>{this.state.count}</div>
}
}

1、 componentWillReceiveProps(nextProps)只调用于props引起的组件更新过程中

2、shouldComponentUpdate(nextProps, nextState)比较nextProps,nextState及当前组件的this.props,返回true更新,返回false更新停止,优化组件性能。

3、componentWillUpdate(nextProps, nextState)调用render方法前执行,执行一些组件更新发生前的业务

4、componentDidUpdate(prevProps, prevState)组件更新后被调用,可以操作组件更新的DOM,prevProps和prevState组件更新前的props和state

卸载阶段

1、componentWillUnmount 此方法在组件被卸载前调用,清理定时器

其他

forceUpdate

  • 通过调用 forceUpdate 重新渲染,会跳过 shouldComponentUpdate

性能优化

1
2
<div>{{name}}</div>
<div>name</div>

缺少编译时优化手段的React为了速度快需要在运行时做更多处理

  • 使用 PureComponent 或 React.memo

  • 使用shouldComponentUpdate

  • 列表使用key

  • 使用useCallback缓存函数(父级传入子级)和 useMemo 缓存变量

响应自然

只要组件=同步的更新,那么更新开始到渲染完毕前,组件中总会有一定数量的工作占用线程,导致浏览器不能绘制UI视图,造成卡顿

为浏览器留出时间渲染UI,让输入不卡顿,需将同步更新变为可中断的异步更新

React 15

Reconciler(协调器)—— 找出变化的组件

Renderer(渲染器)—— 将变化的组件渲染到页面上

Reconciler和Renderer是交替工作的,当第一个元素在页面上已经变化后,第二个再进入Reconciler

工作流程

  1. 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  2. 将虚拟DOM和上次更新时的虚拟DOM对比
  3. 通过对比找出本次更新中变化的虚拟DOM
  4. 通知Renderer将变化的虚拟DOM渲染到页面上

React 16

Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler

Reconciler(协调器)—— 找出变化的组件

Renderer(渲染器)—— 将变化的组件渲染到页面上

工作流程

  1. 点击页面发生更新 state.count 从 1 变成 2
  2. 调度器接收更新,查看是否有高优先级任务,没有责 state.count 从 1 变成 2 交给协调器
  3. 查看变化导致哪些虚拟DOM变化,打上标记update,交给渲染器
  4. 渲染器找到找到被标记的虚拟DOM进行更新

2,3两步会被 高优任务更新或当前帧没有剩余时间 两种情况中断

Scheduler

Scheduler(调度器)我们以浏览器是否有剩余时间作为任务中断的标准,浏览器原生 requestIdleCallback 可以实现

但是,由于以下因素,React放弃使用,自己实现了功能更完备的requestIdleCallbackpolyfill(Scheduler):

  • 浏览器兼容性

  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

异步可中断更新

可以理解为更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。浏览器原生就支持类似的实现,这就是Generator

但Generator的一些缺陷:

  • 类似async,Generator也是传染性的,使用了Generator则上下文的其他函数也需要作出改变

  • Generator执行的中间状态是上下文关联的

考虑如下例子:

1
2
3
4
5
6
7
8
function* doWork(A, B, C) {
var x = doExpensiveWorkA(A);
yield;
var y = x + doExpensiveWorkB(B);
yield;
var z = y + doExpensiveWorkC(C);
return z;
}

每当浏览器有空闲时间都会依次执行其中一个doExpensiveWork,当时间用尽则会中断,当再次恢复时会从中断位置继续执行

只考虑“单一优先级任务的中断与继续”情况下Generator可以很好的实现异步可中断更新

但是当我们考虑“高优先级任务插队”的情况,如果此时已经完成doExpensiveWorkA与doExpensiveWorkB计算出x与y

此时B组件接收到一个高优更新,由于Generator执行的中间状态是上下文关联的,所以计算y时无法复用之前已经计算出的x,需要重新计算

如果通过全局变量保存之前执行的中间状态,又会引入新的复杂度

基于这些原因,React没有采用Generator实现协调器

Fiber

React Fiber可以理解为:

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态

其中每个任务更新单元为React Element对应的Fiber节点

含义

  • 作为静态的数据结构,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息

  • 作为动态的工作单元,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)

Fiber 如何形成树

1
2
3
4
5
6
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

双缓存Fiber树

双缓存:在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,这种在内存中构建并直接替换的技术叫做

React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新

React中最多会同时存在两棵Fiber树。屏幕上的Fiber树称为current Fiber,内存中构建的Fiber树称为workInProgress Fiber树

每次状态更新都会产生新的 workInProgress Fiber 树,通过current与workInProgress的替换,完成DOM更新

diff

设计动力

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。

这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。 然而,即使在最前沿的算法中,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量

于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

  • 两个不同类型的元素会产生出不同的树;

  • 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定

diffing 算法

1、 tree diff

  • React 通过使用 updateDepth 对 虚拟DOM树进行层次遍历

  • 两棵树只对同一层级节点进行比较,只要该节点不存在了,那么该节点与其所有子节点会被完全删除,不在进行进一步比较

2、 component diff

  • 同一类型的组件,按照原策略(tree diff)比较 virtual DOM tree

  • 同类型组件,组件A转化为了组件B,如果virtual DOM 无变化,可以通过 shouldComponentUpdate 方法来判断是否需要更新

  • 不同类型的组件,替换整个组件的所有节点

  • 当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps 和 componentWillUpdate 方法

3、element diff

  • 对比两颗树时,首先比较两棵树的根节点是否同类型

  • 不同类型的元素时,会拆卸原有的树并且建立起新的树。<Button> -> <div> 会触发一个完整的重建流程

  • 拆卸一棵树时,对应的 DOM 节点也会被销毁,旧的组件实例将执行 componentWillUnmount。建立新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 componentWillMount -> componentDidMount 所有跟之前的树所关联的 state 也会被销毁

在根节点以下的组件也会被卸载,它们的状态会被销毁:

1
2
3
4
5
6
7
<div>
<Components />
</div>

<span>
<Components />
</span>

React 会销毁 Components 组件并且重新装载一个新的组件

  • 相同类型时,React 会保留 DOM 节点,仅比对及更新有改变的属性
1
2
3
4
5
6
7
// 只需要修改 DOM 元素上的 className 属性
<div className="before" title="stuff" />
<div className="after" title="stuff" />

// 只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
  • 对子节点进行递归
1
2
3
4
5
6
7
8
9
10
11
// React 不会保留 <li>1</li> 和 <li>2</li>,而是会重建每一个子元素 。这种情况会带来性能问题
<ul>
<li>1</li>
<li>2</li>
</ul>

<ul>
<li>3</li>
<li>1</li>
<li>2</li>
</ul>

使用 key 来规避上述问题

注意:当基于下标的组件进行重新排序时,由于组件实例是基于它们的 key 来决定是否更新以及复用,如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改导致无法预期的变动

diff

返回
顶部