本文做者:葛星html
React 实现了使用 Virtual DOM 来描述 UI 的方式,经过对比两棵树的差别最小化的更新 DOM,这样使得用户的代码变的傻瓜,可是同时也来带了一些问题。这个核心的问题就在于 diff 计算并不是是免费的,在元素较多的状况下,整个 diff 计算的过程可能会持续很⻓时间,形成动画丢帧或者很难响应用户的操做,形成用户体验降低。前端
为何会出现这个问题,主要是由于下面两个缘由:react
上面两个缘由缺一不可,由于若是 JS 执行, UI 不会阻塞 ,其实用户也不会有所感知。下面让咱们看下比较常见的性能优化手段。git
通常咱们会采用下面的方式来优化性能github
对函数使用防抖的方式进行优化。这种方式将 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>
);
}
}
复制代码
经过 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>
);
}
}
复制代码
这种方式有下面三个须要注意的点:markdown
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 的 API, 这个 API 可让浏览器在空闲时期的时候执行脚本,大概如下面的方式使用:
requestIdleCallback((deadline) => {
if (deadline.timeRemaining() > 0) {
} else {
requestIdleCallback(otherTasks);
}
});
复制代码
上面的例子主要是说若是浏览器在当前帧没有空闲时间了,则开启另外一个空闲期调用。(注:大概在 2018 年的时候, Facebook 抛弃了 requestIdleCallback 的原生 API,讨论)
以前咱们说过 React 的 diff 计算会花费大量的时间,因此咱们思考下若是咱们将 diff 计算放在里面执行是否就能解决体验的问题呢?答案是确定的,可是这会面临下面几个问题:
再看 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 执行性能提高呢,这是由于有其余的优化在其中,好比不须要兼容旧有的浏览器,代码量的缩减等等。
如今咱们来看一看一个 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 的过程当中,为了更好的实现扩展性的需求,衍生出了 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) // 伪代码
}
复制代码
虽然看起来 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。最后,本文仅表明我的观点,若有错误欢迎批评指正。
参考资料
本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!