本文是对 React Fiber 知识点的学习记录,网上有很多大佬对 React Fiber 的详细讲解,想要深入了解 React Fiber 的可以查看文章后面的引用。
一、React Fiber 是什么,解决了什么问题
React Fiber 在 React v16 引入,相当于是对核心渲染机制的一次重构。在没有引入这种算法之前,React 在所有更新都没有完成时会一直占用主线程,直接导致的现象是渲染期间页面的其他 js 动效会卡住直到主线程继续,使页面出现卡顿的现象。看一个官方例子:
<!DOCTYPE html>
<html style=”width: 100%; height: 100%; overflow: hidden”>
<head>
<meta charset=”utf-8″>
<title>Fiber Example</title>
</head>
<body>
<h1>Fiber Example</h1>
<div id=”container”>
<p>
To install React, follow the instructions on
<a href=”https://github.com/facebook/react/”>GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
</p>
</div>
<script src=”https://unpkg.com/react@16/umd/react.development.js”></script>
<script src=”https://unpkg.com/react-dom@16/umd/react-dom.development.js”></script>
<script src=”https://unpkg.com/babel-standalone@6/babel.js”></script>
<script type=”text/babel”>
var dotStyle = {
position: ‘absolute’,
background: ‘#61dafb’,
font: ‘normal 15px sans-serif’,
textAlign: ‘center’,
cursor: ‘pointer’,
};
var containerStyle = {
position: ‘absolute’,
transformOrigin: ‘0 0’,
left: ‘50%’,
top: ‘50%’,
width: ’10px’,
height: ’10px’,
background: ‘#eee’,
};
var targetSize = 25;
class Dot extends React.Component {
constructor() {
super();
this.state = {hover: false};
}
enter() {
this.setState({
hover: true
});
}
leave() {
this.setState({
hover: false
});
}
render() {
var props = this.props;
var s = props.size * 1.3;
var style = {
…dotStyle,
width: s + ‘px’,
height: s + ‘px’,
left: (props.x) + ‘px’,
top: (props.y) + ‘px’,
borderRadius: (s / 2) + ‘px’,
lineHeight: (s) + ‘px’,
background: this.state.hover ? ‘#ff0’ : dotStyle.background
};
return (
<div style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}>
{this.state.hover ? ‘*’ + props.text + ‘*’ : props.text}
</div>
);
}
}
class SierpinskiTriangle extends React.Component {
shouldComponentUpdate(nextProps) {
var o = this.props;
var n = nextProps;
return !(
o.x === n.x &&
o.y === n.y &&
o.s === n.s &&
o.children === n.children
);
}
render() {
let {x, y, s, children} = this.props;
if (s <= targetSize) {
return (
<Dot
x={x – (targetSize / 2)}
y={y – (targetSize / 2)}
size={targetSize}
text={children}
/>
);
return r;
}
var newSize = s / 2;
var slowDown = true;
if (slowDown) {
var e = performance.now() + 0.8;
while (performance.now() < e) {
// Artificially long execution time.
}
}
s /= 2;
return [
<SierpinskiTriangle x={x} y={y – (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x – s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
];
}
}
class ExampleApplication extends React.Component {
constructor() {
super();
this.state = {
seconds: 0,
useTimeSlicing: true,
};
this.tick = this.tick.bind(this);
this.onTimeSlicingChange = this.onTimeSlicingChange.bind(this);
}
componentDidMount() {
this.intervalID = setInterval(this.tick, 1000);
}
tick() {
if (this.state.useTimeSlicing) {
// Update is time-sliced.
// 使用时间分片的方式更新
// https://github.com/facebook/react/pull/13488 将此 api 移除
// deferredUpdates 是将更新推迟 https://juejin.im/entry/59c4885f6fb9a00a4456015d
ReactDOM.unstable_deferredUpdates(() => {
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
});
} else {
// Update is not time-sliced. Causes demo to stutter.
// 更新没有做时间分片,导致卡顿 stutter(结巴)
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
}
}
onTimeSlicingChange(value) {
this.setState(() => ({ useTimeSlicing: value}));
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
render() {
const seconds = this.state.seconds;
const elapsed = this.props.elapsed;
const t = (elapsed / 1000) % 10;
const scale = 1 + (t > 5 ? 10 – t : t) / 10;
const transform = ‘scaleX(‘ + (scale / 2.1) + ‘) scaleY(0.7) translateZ(0.1px)’;
return (
<div>
<div>
<h3>Time-slicing</h3>
<p>Toggle this and observe the effect</p>
<Toggle
onLabel=”On”
offLabel=”Off”
onChange={this.onTimeSlicingChange}
value={this.state.useTimeSlicing}
/>
</div>
<div style={{…containerStyle, transform}}>
<div>
<SierpinskiTriangle x={0} y={0} s={1000}>
{this.state.seconds}
</SierpinskiTriangle>
</div>
</div>
</div>
);
}
}
class Toggle extends React.Component {
constructor(props) {
super();
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.props.onChange(event.target.value === ‘on’);
}
render() {
const value = this.props.value;
return (
<label onChange={this.onChange}>
<label>
{this.props.onLabel}
<input type=”radio” name=”value” value=”on” checked={value} />
</label>
<label>
{this.props.offLabel}
<input type=”radio” name=”value” value=”off” checked={!value} />
</label>
</label>
);
}
}
var start = new Date().getTime();
function update() {
ReactDOM.render(
<ExampleApplication elapsed={new Date().getTime() – start} />,
document.getElementById(‘container’)
);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
</script>
</body>
</html>
react fiber 针对这种情况做了什么优化呢?主要是两点:1、将任务拆分成一小块一小块,2、获取到时间片才执行任务下面主要来讲讲这两点
二、React Fiber 的任务拆分
学过 React 的都知道,React 有虚拟 dom 树,vDom-tree,要把计算任务拆分,那就要有任务恢复和任务完成后提交等功能,普通的 vDom 并不具备记录这些信息的能力,因此 React Fiber 从虚拟节点衍生出了一套 Fiber 节点,来记录任务状态。每一个 Fiber 节点都会对应一个虚拟节点,这样计算任务就拆分成了一个个小块,类似于下图,会有一个对应关系简单描述下它的流程:1. 数据状态发生改变,即发生 setState2. 开始 diff 算法 3. 到达一个节点,生成对应的 fiber 节点,记录状态 4. 查看当前是否有执行时间 5. 有执行时间,计算完当前节点(节点的增删改),到下一个节点,继续这个步骤 6. 到某个节点没有执行时间,则保存 fiber 状态,每个 fiber 都记录了下一个任务指向 7. 重新获取到了执行时间,从当前记录的 fiber 节点开始往下继续 8. 执行到最后的节点,将结果进行一级一级提交,这样直到根节点,组件就知道它已经完成了数据计算 9. 最后一步,将最终确认的 dom 结果渲染到页面中
三、如何获取到时间片的
这个就比较简单了,刚开始我还以为 React 使用了什么骚操作来弄得,看了大佬们的文章后才知道,原来是用了浏览器的 api: requestIdleCallback 和 requestAnimationFrame,requestAnimationFrame 会在每一帧结束后确定调用它的回调函数,用于处理高优先级任务,为了达到不影响页面效果 requestIdleCallback 用于处理低优先任务,它的执行时机不确定。主要说下 requestIdleCallback,每次调用 requestIdleCallback,会告诉你现在是否是空闲时间,空闲时间有多久,它的用法:
// 一窥它的行为
requestIdleCallback(function(){
console.log(‘1’);
let a = 1000000000;
while(a > 0){a–;}
})
requestIdleCallback(function(){console.log(‘2’)})
requestIdleCallback(function(){console.log(‘3’)}, {timeout:10})
// 使用方式
const tasks = […] // 任务队列
function taskHandler(deadline) {
// deadline.timeRemaining() 可以获取到当前帧剩余时间
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
// do something
}
// 没时间了,但还有任务,继续注册一个,等到下次有时间了会执行
if (tasks.length > 0){
requestIdleCallback(taskHandler);
}
}
requestIdelCallback(taskHandler);
上面说到任务的优先级,在 fiber 任务执行完进行 dom 更新的时候,这块是没法做任务拆分的,如果遇到 dom 变化很大,更新耗时的情况也会造成卡顿,这个没法避免,如果此时有用户交互发生,造成卡顿会降低用户体验,Fiber 针对这种情况也做了优化,将任务分成优先级,像用户输入,交互等行为属于高优先级,会优先处理,然后页面渲染,diff 计算等属于次优先级。
几篇对 React Fiber 不错的介绍:React- 从源码分析 React Fiber 工作原理理解 react16.3 的 fiber 架构 React FiberReact Fiber 是什么 React Fiber 初探 react-fiber-resources