本文作者:葛星
背景
React 实现了应用 Virtual DOM 来形容 UI 的形式,通过比照两棵树的差别最小化的更新 DOM,这样使得用户的代码变的傻瓜,然而同时也来带了一些问题。这个外围的问题就在于 diff 计算并非是收费的,在元素较多的状况下,整个 diff 计算的过程可能会继续很⻓工夫,造成动画丢帧或者很难响应用户的操作,造成用户体验降落。
为什么会呈现这个问题,次要是因为上面两个起因:
- React < 15 的版本始终采纳 Stack Reconciler 的形式进行 UI 渲染(之所以叫 Stack Reconciler 是绝对于
Fiber Reconciler 而言) , 而 Stack Reconciler 的实现是采纳了递归的形式,咱们晓得递归是无奈被打断,每当有须要更新的时候,React 会从须要更新的节点开始始终执行 diff,这会耗费大量的工夫。 - 浏览器是多线程的,蕴含渲染线程和 JS 线程,而渲染线程和 JS 线程是互斥的,所以当 JS 线程占据大量工夫的时候,UI 的响应也会被 block 住。
下面两个起因缺一不可,因为如果 JS 执行,UI 不会阻塞,其实用户也不会有所感知。上面让咱们看下比拟常见的性能优化伎俩。
常见的性能优化伎俩
个别咱们会采纳上面的形式来优化性能
防抖
对函数应用防抖的形式进行优化。这种形式将 UI 的更新推延到用户输出结束。这样用户在输出的时候就不会感觉到卡顿。
class App extends Component {onChange = () => {if (this.timeout) {clearTimeout(this.timeout);
}
this.timeout = setTimeout(() =>
this.setState({ds: [],
}),
200
);
};
render() {
return (
<div>
<input onChange={this.onChange} />
<list ds={this.state.ds} />
</div>
);
}
}
应用 PureComponent || shouldComponentUpdate
通过 shouldComponentUpdate 或者 PureComponent 的形式进行优化。这种形式通过浅比照前后两次的 props 和 state 让 React 跳过不必要的 diff 计算。
class App extends Component {shouldComponentUpdate(nextProps, nextState) {
return (!shallowEqual(nextProps, this.props) ||
!shallowEqual(nextState, this.state)
);
}
render() {
return (
<div>
<input onChange={this.onChange} />
<list ds={this.state.ds} />
</div>
);
}
}
这种形式有上面三个须要留神的点:
a. 只能采纳 浅比拟 的形式,这样更深层次的对象更新的时候无奈比拟,而如果采纳深比拟的形式,如果你比拟对象的工夫比 React diff 的工夫还要久,得失相当。
b. 对象的援用关系,在对于 state 的赋值的时候,次要留神 对象的援用关系,比方上面的代码就会让这个组件无奈更新
class App extends PureComponent {
state = {record: {},
};
componentDidMount() {const { record} = this.state;
record.name = "demo";
this.setState({record,});
}
render() {return <>{this.state.record.name}</>;
}
}
c. 函数的执行值产生扭转。这种状况在于函数外面用到了 props 和 state 之外的变量,这些变量可能产生了扭转
class App extends PureComponent {cellRender = (value, index, record) => {return record.name + this.name;};
render() {return <List cellRender={this.cellRender} />;
}
}
对象劫持
通过相似于 Vue@2.x 和 Mobx 的形式实现察看对象来进行部分更新。这种形式要求用户在应用的时候防止应用 setState 办法。
@inject("color")
@observer
class Btn extends React.Component {render() {
return (<button style={{ color: this.props.color}}>{this.props.text}</button>
);
}
}
<Provider color="red">
<MessageList>
<Btn />
</MessageList>
</Provider>;
对于这个例子,color 变动的时候, 只有 Button 会从新渲染。
其实对于 80% 的状况,下面的三种形式曾经满足这些场景的性能优化,然而下面所说的都是在利用层面的优化,其实对于开发者提出了肯定的要求,有什么形式能够在底层进行一些优化呢?
RequestIdleCallback
十分庆幸的是浏览器推出了requestIdleCallback 的 API, 这个 API 能够让浏览器在闲暇期间的时候执行脚本,大略以上面的形式应用:
requestIdleCallback((deadline) => {if (deadline.timeRemaining() > 0) { } else {requestIdleCallback(otherTasks);
}
});
下面的例子次要是说如果浏览器在以后帧没有闲暇工夫了,则开启另一个闲暇期调用。(注:大略在 2018 年的时候,Facebook 摈弃了 requestIdleCallback 的原生 API,探讨)
之前咱们说过 React 的 diff 计算会破费大量的工夫,所以咱们思考下如果咱们将 diff 计算放在外面执行是否就能解决体验的问题呢? 答案是必定的,然而这会面临上面几个问题:
- 因为每次闲暇的工夫无限,所以要求程序在执行 diff 的时候须要将以后状态保留下来,期待下次闲暇的时候再次调用。这里就波及到可中断,可复原。
- 程序须要有优先级的概念。简略的来说就是须要标记哪些工作是高优先级的,哪些工作是低优先级的,这样才有调度的根据。
所以 React Fiber 就是基于优先级的调度策略 。看下面两个问题,最重要的局部其实是能够 中断和复原,如何实现中断和复原?
斐波那契数列的 Fiber
再看 React 的 Fiber 之前咱们先来钻研下怎么应用 Fiber 的思维形式来改写斐波那契数列,在计算机科学里,有这样一句话“任何递归的程序都能够应用循环实现”。为了让程序能够中断,递归的程序必须改写为循环。
递归下斐波那契数列写法:
function fib(n) {if (n <= 2) {return 1;} else {return fib(n - 1) + fib(n - 2);
}
}
如果咱们采纳 Fiber 的思路将其改写为循环,就须要开展程序,保留执行的两头态,这里的两头态咱们定义为上面的构造,尽管这个例子并不能和 React Fiber 的对等。
function fib(n) {let fiber = { arg: n, returnAddr: null, a: 0};
// 标记循环
rec: while (true) {
// 当开展齐全后,开始计算
if (fiber.arg <= 2) {
let sum = 1;
// 寻找父级
while (fiber.returnAddr) {
fiber = fiber.returnAddr;
if (fiber.a === 0) {
fiber.a = sum;
fiber = {arg: fiber.arg - 2, returnAddr: fiber, a: 0};
continue rec;
}
sum += fiber.a;
}
return sum;
} else {
// 先开展
fiber = {arg: fiber.arg - 1, returnAddr: fiber, a: 0};
}
}
}
实际上 React Fiber 正是受到了下面的启发,咱们能够看到因为 Fiber 的思路对执行程序进行了开展,大略相似于上面的构造,和程序执行的堆栈十分类似,这段代码的意思是先像右边一样开展整个构造,当 fiber
的入参小于 2 的时候,再一直的寻找父级晓得没有父节点,最初失去 sum
值。
左侧是开展的构造,右侧是向上重叠的调用栈示意图
所以 Fiber 比 Stack 的形式要花费更多的内存占用和执行性能。这个例子有更直观的展现。然而为什么 React 基于 Fiber 的思路会让 JS 执行性能晋升呢,这是因为有其余的优化在其中,比方不须要兼容旧有的浏览器,代码量的缩减等等。
React Fiber 的构造
当初咱们来看一看一个 Fiber Node 的构造,如下图所示,一个十分典型的链表的构造,这种设计形式理论也受下面开展堆栈形式的启发,而绝对于 15 版本而言,减少了很多属性。
{
tag, // 标记一些非凡的组件类型,比方 Fragment,ContextProvider 等
type, // 组件的节点的实在的形容,比方 div, Button 等
key, // key 和 15 一样,如果 key 统一,下次这个节点能够被复用
child, // 节点的孩子
sibling, // 节点的兄弟节点
return, // 实际上就是该节点的父级节点
pendingProps, // 开始的时候设置 pendingProps
memoizedProps, // 完结的时候设置 memoizedProps, 如果两者雷同的话,间接复用之前的 stateNode
pendingWorkPriority, // 以后节点的优先级,
stateNode, // 以后节点关联的组件的 instance
effectTag // 标记以后的 fiber 须要被操作的类型,比方删除,更新等等
...
}
咱们能够采纳下面相似遍历开展的斐波那契数列一样遍历 Fiber Node 的 root,其实就是一个比较简单的链表遍历办法。
Fiber 的衍生产物 Custom Renderer
在施行 Fiber 的过程中,为了更好的实现扩展性的需要,衍生出了 React Reconciler 这个独立的包,咱们能够通过这个玩意自定义一个 Custom Renderer。它定义了一系列标准化的接口,使咱们不用关怀 Fiber 外部是如何工作的,就能够通过虚构 DOM 的形式驱动宿主环境。
一个较为残缺的摸索 Custom Renderer 的例子
启动形式
上面一个标准化的 Custom Renderer 的启动代码,咱们只须要实现 HostConfig 的局部就能够应用 React Reconclier 的调度能力:
import Reconciler from 'react-reconclier';
const HostConfig = {};
const CustomRenderer = Reconciler(HostConfig)
let root;
const render = function(children, container) {if(!root) {root = CustomRenderer.createContainer(container);
}
CustomRenderer.updateContainer(children, root);
}
render(<App/>, doucment.querySelector('#root')
HostConfig 中最外围的办法是 createInstance
,为 type 类型创立一个实例,如果宿主环境是 Web,能够间接调用 createElement
办法
createInstance(type,props,rootContainerInstance,hostContext) {
// 转换 props
return document.createElement(
type,
props,
);
}
跨端实现
衍生一下,当初跨端的计划,基本上这种运行时的计划都能够利用 CustomRenderer 的思路,来实现一码多端。举个简略的例子,假如了我写了上面的代码
function App() {return <Button />;}
Button 具体应该应用什么对应的实现渲染,能够在 createInstance
里做个拦挡,当然也能够对不同的端实现不同的 Renderer。上面一个伪代码
Mobile Renderer
import {MobileButton} from 'xxx';
createInstance(type,props,rootContainerInstance,hostContext) {
const components = {Button: MobileButton}
return new components[type](props) // 伪代码
}
API 设计的问题
尽管看起来 CustomRenderer 很好,实际上在整个 API 的设计上,为了 Web 做了一些斗争。比方独自为文本设计的 shouldSetTextContent
,createTextInstance
办法,基本上是因为 Web 对某些元素文本操作的起因,没有方法应用对立的 document.createElement
,而必须应用document.createTextNode
,其实在很多其余的渲染场景下都不须要独自实现这些办法或者间接返回 false
React DOM 的实现
export function shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
type === 'option' ||
type === 'noscript' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
props.dangerouslySetInnerHTML.__html != null)
);
}
其余的一些 Renderer
export function shouldSetTextContent() {return false;}
小结
本文次要探寻下 React Fiber 想要解决的问题,包含 Fiber 架构受到的一些启发,及在施行了 Fiber 架构后的衍生产物 Custom Renderer 的利用,心愿有更多的场景能够利用到 Custom Renderer 的能力, 这里提供一些社区常见的 Custom Renderer。最初,本文仅代表个人观点,如有谬误欢送批评指正。
参考资料
ReactFiber
CallStack
requestIdleCallback
React Reconclier
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!