Open ChelesteWang opened 2 years ago
1)背景
2)实现原理
旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。
Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
在处理UI时,如果一次执行太多工作,可能会导致动画丢帧。
基本上,如果React要同步遍历整个组件树,并为每个组件执行任务,它可能会运行超过16毫秒,这将导致不顺畅的视觉效果。
较新的浏览器(和React Native)实现了有助于解决这个问题的API:requestIdleCallback
,可用于对函数进行排队,这些函数会在浏览器空闲时被调用:
requestIdleCallback((deadline)=>{
console.log(deadline.timeRemaining(), deadline.didTimeout)
});
如果我现在打开控制台并执行上面的代码,Chrome会打印49.9
和false
,它基本上告诉我,我有49.9ms
去做我需要做的任何工作,并且我还没有用完所有分配的时间,否则deadline.didTimeout
将会是true
。
请记住timeRemaining可能在浏览器被分配某些工作后立即更改,因此应该不断检查。
但requestIdleCallback
实际上有点过于严格,并且[执行频次](https://github.com/facebook/react/issues/13206#issuecomment-418923831)
不足以实现流畅的UI渲染,因此React团队必须实现[自己的版本](https://github.com/facebook/react/blob/eeb817785c771362416fd87ea7d2a1a32dde9842/packages/scheduler/src/Scheduler.js#L212-L222)[](https://user-images.githubusercontent.com/1249423/53719626-94dde800-3e99-11e9-8108-1679cbf06185.png)。
现在,如果我们将React对组件执行的所有活动放入函数performWork
, 并使用requestIdleCallback
来安排工作,我们的代码可能如下所示:
requestIdleCallback((deadline) => {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
nextComponent = performWork(nextComponent);
}
});
我们对一个组件执行工作,然后返回要处理的下一个组件。
这是可以做到的,但前提是我们不能同步地处理整个组件树。
因此,我们需要一种方法将渲染工作分解为增量单元。
为了解决这个问题,React必须重新实现遍历树的算法,从依赖于内置堆栈的同步递归模型,变为具有链表和指针的异步模型。
walk(a1);
function walk(instance) {
doWork(instance);
const children = instance.render();
children.forEach(walk);
}
function doWork(o) {
console.log(o.name);
}
递归方法直观,非常适合遍历树。
但是正如我们发现的,它有局限性:最大的一点就是我们无法分解工作为增量单元。
我们不能暂停特定组件的工作并在稍后恢复。
通过这种方法,React只能不断迭代直到它处理完所有组件,并且堆栈为空。
[Sebastian Markbåge](https://github.com/sebmarkbage)在[Fiber Principles: Contributing To Fiber
](https://github.com/facebook/react/issues/7942)
概括了该算法的要点。
要实现该算法,我们需要一个包含3个字段的数据结构:
在React新的协调算法的上下文中,包含这些字段的数据结构称为Fiber。
下图展示了通过链表链接的对象的层级结构和它们之间的连接类型:
function walk(o) {
let root = o;
let current = o;
while (true) {
let child = doWork(current);
if (child) {
current = child;
continue;
}
if (current === root) {
return;
}
while (!current.sibling) {
if (!current.return || current.return === root) {
return;
}
current = current.return;
}
current = current.sibling;
}
}
它看起来像浏览器中的一个调用堆栈。
我们现在通过保持对current
节点(充当顶部堆栈帧)的引用来控制堆栈:
function walk(o) {
let root = o;
let current = o;
while (true) {
... current = child;
... current = current.return;
... current = current.sibling;
}
}
我们可以随时停止遍历并稍后恢复。
对于React来说,有两类瓶颈需要解决:
CPU的瓶颈,如大计算量的操作导致页面卡顿
IO的瓶颈,如请求服务端数据时的等待时间
其中CPU的瓶颈通过并发特性的优先级中断机制解决。
IO的瓶颈则交给Suspense解决。
react fiber 移除 Effect List 依赖 https://github.com/facebook/react/pull/19673/files
let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;
function createSetter(cursor) {
return function setterWithCursor(newVal) {
state[cursor] = newVal;
};
}
// This is the pseudocode for the useState helper
export function useState(initVal) {
if (firstRun) {
state.push(initVal);
setters.push(createSetter(cursor));
firstRun = false;
}
const setter = setters[cursor];
const value = state[cursor];
cursor++;
return [value, setter];
}
// Our component code that uses hooks
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
const [lastName, setLastName] = useState("Yardley"); // cursor: 1
return (
<div>
<Button onClick={() => setFirstName("Richard")}>Richard</Button>
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
</div>
);
}
// This is sort of simulating Reacts rendering cycle
function MyComponent() {
cursor = 0; // resetting the cursor
return <RenderFunctionComponent />; // render
}
console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']
// click the 'Fred' button
console.log(state); // After-click: ['Fred', 'Yardley']
react因为先天的不足——无法精确更新,所以需要react fiber把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力; react fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;
React 原理学习
JSX -> CE (CreateElement)JSX parser TypeScript Babel React , 或者 React 17 中提出了新的React-JSX JSX-runtime
React-Dom 将创建 DOM 节点递归挂载到 DOM 树上面
现在我们的 React 是没有状态存在的
移除现有的 DOM 节点。
重新渲染一切。
Patch 算法增量更新渲染
调和阶段(Reconciler): 官方解释。React 会自顶向下通过递归,遍历新数据生成新的 Virtual DOM,然后通过 Diff 算法,找到需要变更的元素(Patch),放到更新队列里面去。
渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的API,实际更新渲染对应元素。宿主环境,比如 DOM、Native、WebGL 等。
什么是 Fiber
Fiber 是对 React element 的拷贝,是一份描述 Diff 的工作模拟了函数调用关系
Current 是当前 node , work in progress node 是 copy on write 体现了 immutable 拷贝修改后替换原始数据,计算阶段可中断,提交阶段不可中断。
Element 与渲染无关,只是页面的数据描述,描述组件信息与层级关系
fiber 与渲染有关,fiber 树描述 DIff 等工作调用关系即更新路径,有 react 组件的 render 处理顺序,类似于一棵树转二叉树,线索树指针指向父级,children 直接子节点 ,sibling 相邻节点
Fiber 是如何工作的
ReactDOM.render()
和setState
的时候开始创建更新。Render 是如何执行的
分解任务,加入任务队列,逐一修改,执行 commit 提交
https://zhuanlan.zhihu.com/p/137234573
如何进行 Dom diff
什么是 Reconciler
一个调和器(Reconciler)将3部分个功能结合起来:
Fiber (VirtualDOM): 对组件层级结构的描述
Reconciliation: 计算DOM DIFF
HostConfig :对于React渲染需要的具体端(DOM/React Native等)的封装
改变属性的能力
插入元素的能力
替换元素的能力