2018- 司徒正美-React Fiber 架构
React Fiber 架构
性能优化是一个系统性的工程,如果只看到局部,引入算法,当然是越快越好
; 但从整体来看,在关键点引入缓存,可以秒杀N 多算法,或另辟蹊径,探索事件的本质,可能用户要的并不是快……
痛点
主要有如下几个:
- 组件不能返回数组,最见的场合是
UL 元素下只能使用LI ,TR 元素下只能使用TD 或TH ,这时这里有一个组件循环生成LI 或TD 列表时,我们并不想再放一个DIV ,这会破坏HTML 的语义。 - 弹窗问题,之前一直使用不稳定的
unstable_renderSubtreeIntoContainer 。弹窗是依赖原来DOM 树的上下文,因此这个API 第一个参数是组件实例,通过它得到对应虚拟DOM ,然后一级级往上找,得到上下文。它的其他参数也很好用,但这个方法一直没有转正。。。 - 异常处理,我们想知道哪个组件出错,虽然有了
React DevTool ,但是太深的组件树查找起来还是很吃力。希望有个方法告诉我出错位置,并且出错时能让我有机会进行一些修复工作 HOC 的流行带来两个问题,毕竟是社区兴起的方案,没有考虑到ref 与context 的向下传递。- 组件的性能优化全凭人肉,并且主要集中在
SCU ,希望框架能干些事情,即使不用SCU ,性能也能上去。
解决进度
16.0 让组件支持返回任何数组类型,从而解决数组问题; 推出createPortal API , 解决弹窗问题; 推出componentDidCatch 新钩子, 划分出错误组件与边界组件, 每个边界组件能修复下方组件错误一次, 再次出错,转交更上层的边界组件来处理,解决异常处理问题。16.2 推出Fragment 组件,可以看作是数组的一种语法糖。16.3 推出createRef 与forwardRef 解决Ref 在HOC 中的传递问题,推出new Context API ,解决HOC 的context 传递问题(主要是SCU 作崇)- 而性能问题,从
16.0 开始一直由一些内部机制来保证,涉及到批量更新及基于时间分片的限量更新。
一个小实验
我们可以通过以下实验来窥探
function randomHexColor() {
return (
"#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)
);
}
setTimeout(function () {
var k = 0;
var root = document.getElementById("root");
for (var i = 0; i < 10000; i++) {
k += new Date() - 0;
var el = document.createElement("div");
el.innerHTML = k;
root.appendChild(el);
el.style.cssText = `background:${randomHexColor()};height:40px`;
}
}, 1000);
这是一个拥有

我们再改进一下,分派次插入节点,每次只操作
function randomHexColor() {
return (
"#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)
);
}
var root = document.getElementById("root");
setTimeout(function () {
function loop(n) {
var k = 0;
console.log(n);
for (var i = 0; i < 100; i++) {
k += new Date() - 0;
var el = document.createElement("div");
el.innerHTML = k;
root.appendChild(el);
el.style.cssText = `background:${randomHexColor()};height:40px`;
}
if (n) {
setTimeout(function () {
loop(n - 1);
}, 40);
}
}
loop(100);
}, 1000);

究其原因是因为浏览器是单线程,它将
浏览器的运作流程
渲染
-> tasks -> 渲染-> tasks -> 渲染-> tasks -> ….
这些

总结一句,就是让浏览器休息好,浏览器就能跑得更快。
如何让代码断开重连
<div>
<Foo>
<Bar />
</Foo>
</div>
但标签化是天然套嵌的结构,意味着它会最终编译成递归执行的代码。因此
链表是对异步友好的。链表在循环时不用每次都进入递归函数,重新生成什么执行上下文,变量对象,激活对象,性能当然比递归好。
因此
ReactDOM.render(<A />, node1);
ReactDOM.render(<B />, node2);
//node1与node2不存在包含关系,那么这页面就有两棵虚拟DOM树
如果仔细阅读源码,虚拟DOM层
,它只负责描述结构与逻辑内部组件层
,它们负责组件的更新底层渲染层
, 不同的显示介质有不同的渲染方法,比如说浏览器端,它使用元素节点,文本节点,在
虚拟

如何决定每次更新的数量
在
因此我们需要将我们的更新逻辑分成两个阶段,第一个阶段是将虚拟
为了让读者能直观了解
首先是一些简单的方法:
var queue = [];
ReacDOM.render = function (root, container) {
queue.push(root);
updateFiberAndView();
};
function getVdomFormQueue() {
return queue.shift();
}
function Fiber(vnode) {
for (var i in vnode) {
this[i] = vnode[i];
}
this.uuid = Math.random();
}
//我们简单的Fiber目前来看,只比vdom多了一个uuid属性
function toFiber(vnode) {
if (!vnode.uuid) {
return new Fiber(vnode);
}
return vnode;
}
function updateFiberAndView() {
var now = new Date() - 0;
var deadline = new Date() + 100;
updateView(); //更新视图,这会耗时,因此需要check时间
if (new Date() < deadline) {
var vdom = getVdomFormQueue();
var fiber = vdom,
firstFiber;
var hasVisited = {};
do {
//深度优先遍历
var fiber = toFiber(fiber); //A处
if (!firstFiber) {
fibstFiber = fiber;
}
if (!hasVisited[fiber.uuid]) {
hasVisited[fiber.uuid] = 1;
//根据fiber.type实例化组件或者创建真实DOM
//这会耗时,因此需要check时间
updateComponentOrElement(fiber);
if (fiber.child) {
//向下转换
if (newDate - 0 > deadline) {
queue.push(fiber.child); //时间不够,放入栈
break;
}
fiber = fiber.child;
continue; //让逻辑跑回A处,不断转换child, child.child, child.child.child
}
}
//如果组件没有children,那么就向右找
if (fiber.sibling) {
fiber = fiber.sibling;
continue; //让逻辑跑回A处
}
// 向上找
fiber = fiber.return;
if (fiber === fibstFiber || !fiber) {
break;
}
} while (1);
}
if (queue.length) {
setTimeout(updateFiberAndView, 40);
}
}
里面有一个
function updateComponentOrElement(fiber) {
var { type, stateNode, props } = fiber;
if (!stateNode) {
if (typeof type === "string") {
fiber.stateNode = document.createElement(type);
} else {
var context = {}; //暂时免去这个获取细节
fiber.stateNode = new type(props, context);
}
}
if (stateNode.render) {
//执行componentWillMount等钩子
children = stateNode.render();
} else {
children = fiber.childen;
}
var prev = null; //这里只是mount的实现,update时还需要一个oldChildren, 进行key匹配,重复利用已有节点
for (var i = 0, n = children.length; i < n; i++) {
var child = children[i];
child.return = fiber;
if (!prev) {
fiber.child = child;
} else {
prev.sibling = child;
}
prev = child;
}
}
因此这样
如何调度时间才能保证流畅
刚才的
浏览器本身也不断进化中,随着页面由简单的展示转向
下面是一些自救措施:
- requestAnimationFrame
- requestIdleCallback
- web worker
- IntersectionObserver
我们依次称为浏览器层面的帧数控制调用,闲时调用,多线程调用, 进入可视区调用。
刚才说
我们看

它的第一个参数是一个回调,回调有一个参数对象,对象有一个new Date - deadline
,并且它是一个高精度数据, 比毫秒更准确, 至少浏览器到底安排了多少时间给更新
于是我们的
function updateFiberAndView(dl) {
updateView(); //更新视图,这会耗时,因此需要check时间
if (dl.timeRemaining() > 1) {
var vdom = getVdomFormQueue();
var fiber = vdom,
firstFiber;
var hasVisited = {};
do {
//深度优先遍历
var fiber = toFiber(fiber); //A处
if (!firstFiber) {
fibstFiber = fiber;
}
if (!hasVisited[fiber.uuid]) {
hasVisited[fiber.uuid] = 1;
//根据fiber.type实例化组件或者创建真实DOM
//这会耗时,因此需要check时间
updateComponentOrElement(fiber);
if (fiber.child) {
//向下转换
if (dl.timeRemaining() > 1) {
queue.push(fiber.child); //时间不够,放入栈
break;
}
fiber = fiber.child;
continue; //让逻辑跑回A处,不断转换child, child.child, child.child.child
}
}
//....略
} while (1);
}
if (queue.length) {
requetIdleCallback(updateFiberAndView, {
timeout: new Date() + 100,
});
}
}
到这里,
批量更新
但
这个东西怎么实现呢?就是搞一个全局的开关,如果打开了,就让
var isBatching = false
function batchedUpdates(callback, event) {
let keepbook = isBatching;
isBatching = true;
try {
return callback(event);
} finally {
isBatching = keepbook;
if (!isBatching) {
requetIdleCallback(updateFiberAndView, {
timeout:new Date + 1
}
}
}
};
function updateView(){
if(isBatching){
return
}
//更新视图
}
事实上,当然没有这么简单,考虑到大家看不懂
https://github.com/RubyLouvre/anu/blob/master/packages/fiber/scheduleWork.js#L94-L113
可以说,setState是对单个组件的合并渲染,batchedUpdates是对多个组件的合并渲染
。合并渲染是
为什么使用深度优化遍历
这涉及一个很经典的消息通信问题。如果是父子通信,我们可以通过
当它遇到一个有
当它遇到一个有
组件总是从
如果子组件没有
在
还有一个隐患,它可能被
基于这些问题,终于

相同的情况还有
我们知道,虚拟
这种独立的栈机制有效地解决了内部方法的参数冗余问题。
但有一个问题,当第一次渲染完毕后,
当我们批量更新时,可能有多少不连续的子组件被更新了,其中两个组件之间的某个组件使用了
为什么要对生命周期钩子大换血
一些迷你
有些迷你
其实基于算法的优化是一种绝望的优化,就类似玛雅文明因为找不到铜矿一直停留于石器时代,诞生了伟大的工匠精神把石器打磨得美伦美奂。

之所以这么说,因为
在我们的代码里面,休息
就是检测时间然后断开
在早期版本中,
在即使到来的异步更新中,
在进入
如果出错呢,在

任务系统
每个
每个
怎么添加任务呢?
fiber.effectTag |= Update;
怎么保证不会重复添加相同的任务?
fiber.effectTag &= ~DidCapture;
在
if (fiber.effectTag & Update) {
/*操作属性*/
}

顺便说一下
https://github.com/RubyLouvre/anu/blob/master/packages/fiber/commitWork.js
无论是位操作还是素数,我们只要保证某个
此外,任务系统还有另一个存在意义,保证一些任务优先执行,某些任务是在另一些任务之前。我们称之为任务分拣。这就像快递的仓库管理一样,有了归类才好进行优化。比如说,元素虚拟
Fiber 的连体婴结构
连体婴是一个可怕的名词,想想就不舒服,因为事实上
可以说,
有关连体婴结构的思考,可以参看我另一篇文章《从错误边界到回滚到
中间件系统
说起中间件系统,大家可能对

早在

简单地说,一个
这个东西有什么用呢? 最少有两个用处,在更新
总结
但是和所有人一样,我最初学习