React-Fiber介绍及组织结构
最近几个项目都是React Native
,所以对React
的执行机制越来越好奇,比如为什么React Native
也要先import React
?为什么看起来声明周期没什么差别,但是渲染的结果(标签)却完全不同?怀着这些疑问,决定还是再看一下代码。之前草草撸过一遍基本操作,对结构和流程有个大概轮廓,但都只是粗浅的了解。尤其对于16.x以来的Fiber
,还是一片广阔的未知,正好趁着这个机会,一起看一下。
React版本:react-16.9.0
主要工具:VS code,chrome
调度(reconciliation
)方式
我知道,reconciliation
有的叫“协调算法”,有的叫“一致性处理”,大概意思都是协调组件的集合和渲染,我这里翻译成“调度”可能也不是很确切,意会即可。
两篇文章供参考,这部分概念较多,所以很多内容来源于此:
-> React Fiber Architecture
-> The how and why on React’s usage of linked list in Fiber to walk the component’s tree
它干什么用?
简单来说,它负责把用户输入的React组件,组织成一棵树并保存到内存,然后通过渲染器(renderer
)转化为目标平台可用的app
节点结构。以及当有更新(通常是setState
)过来的时候,会生成一棵新树。这时候就需要某种算法来对比两棵树,然后决定采取什么策略来更新最终渲染的app
。
具体来说,调度方式包括了树的差异(diff)计算,生命周期函数的调用,以及使用不同的渲染器(如react-dom
或react-native
)更新节点的任务。【更多介绍】
基本上,react的调度方式在不同平台是一样的,不同的是渲染器(如浏览器DOM渲染器和RN渲染器)。它将这两个过程完全分开。所以从原理上来说,我们也可以自己实现一个renderer
(比如代码库里用于测试的react-noop-renderer
)。但是由于React
没有对开发者开放过多的API,所以如果想这么做,可能要啃一些代码。
它和Fiber
有什么关系?
react 16
把默认的调度方式从stack
切换到了Fiber
。
在这部分开始前,我们要先知道什么是调用栈,以及有什么痛点。
调用栈(call stack)用来跟踪程序的执行,比如在代码中打个断点,debug
就能看到函数调用流程。当一个函数执行时,对应的stack frame
会压入栈,代表函数所进行的工作。
当处理UI时,通常会同时处理很多的工作,对React
来说,嵌套越深的App
就意味着更深的调用栈(如下面react-15-stable
的图),这就可能产生动画掉帧的问题。而且,栈里的一些工作可能并不是必须的,可能会被后来的update
冲掉。
现代浏览器(以及RN
)实现了解决这个问题的API:requestIdleCallback
用来在空闲时调度低优先级的function
;requestAnimationFrame
用来调度高优先级的function
。但是由于这些API可供使用的时间都是碎片化的,所以为了使用它们,就需要把渲染任务分解成渐进式的小单元。如果依旧使用之前的stack
机制,无法实现这个功能。
之前是什么样子呢?
在React 16之前,默认的stack
使用了堆栈结构,当render
过程中,发现一个子节点,就push
到child
里,接着向下递归直到没有子节点,由此生成了一个自顶向下的树形结构。这棵树是依赖于内置堆栈的同步递归模型(synchronous recursive model)。啥叫同步递归模型?举个例子,平时debug
打断点的时候,能看到自调用函数向上的完整的调用栈(call stack)。
缺点很明显,当来了一个data
更新,会自上而下生成一棵新的树,然后开始两棵树的差异(diff
)对比,这时的工作是同步的,不太可能处理到某个节点,暂停去做另外的工作,而后转回来继续处理。而想保存整个调用栈里的各级状态,需要做大量额外工作。如果在这个同步任务执行时,有用户输入等事件进入,由于主线程正忙着处理这个任务,就会导致卡顿或者失去响应。
简单来说,同步顺序执行直至栈空。
所以Fiber应运而生。它专为React
组件设计,每个fiber
都可以看作一个虚拟的stack frame
。这些stack frames
都被放在内存里,供随时调用。
Fiber
使用了稍微复杂的结构来处理:每个组件都对应一个Fiber
节点,其中包含祖先-兄弟-孩子
三个指针,由此组成链表结构,来实现这棵树。另外,用一个FiberRootNode
来保存整个链表当前的状态,以属性current
以及其他状态位指示当前处理的位置。
由于使用了Fiber
,得以随时知道整棵树执行到哪个节点,以及state的状态,这使得更多功能的实现成为可能:并发模式,错误边界处理等。
虽然Fiber
改变了React
节点树的构建和diff
机制,但是更上层的算法(戳 这里)逻辑基本没大变化,它基于两个最基础的假设:
1、不同的组件类型会生成不同的子树。此时不会去对比子树,而是重新生成一棵新的来取代。
2、列表(lists
)的对比通过keys
来进行,因此keys
需要“稳定,可预测,且唯一”。
Fiber
和之前的调度方式的直观差别?
以一个简单的组件来说明。为了直观,只列出最后的结构,省略组件的声明。
1 2 3 4 5 |
<App> <Foo> <span>value</span> </Foo> </App> |
下面的瀑布图来自chrome devtools - performance
。
react-15-stable
react-16.9.0
用16.9代码调试的时候,发现无论怎么添加代码逻辑,都是同步执行,没有看到并发的影子。这是因为React 16.9
还未正式引进并发模式,估计正式启用要到React 17
了。
如果有兴趣,可以手动启用试试效果,不过由于unstable
,不建议在生产环境开启。
1 2 |
// Whole app (not final API) ReactDOM.unstable_createRoot(domNode).render(<App />); // v16.11 更改为ReactDOM.createRoot |
Fiber组织方式
还是以上面例子,最终生成的树结构
HostRoot
为头部节点的current
(原始FiberNode
节点)树,一棵是workInProgress
(工作节点)树,两棵树通过各节点alternate
属性关联。
惯例需要先走代码验证一下。
1 2 3 |
render legacyRenderSubtreeIntoContainer root = legacyCreateRootFromDOMContainer |
这里两个函数被标记了legacy
,说明这部分代码是遗留的,也就是说执行流程与之前比没变化,跳过。
首先会创建一个FiberRootNode
,可以理解为一个容器,用于保存当前工作现场,方便pause/resume。
接着是一个叫HostRoot
的FiberNode
,tag
被标记为3,type
是null。相当于链表头。
创建之后即放入FiberRootNode
的current
。
1 2 |
FiberRootNode.current = HostRoot; HostRoot.stateNode = FiberRootNode; |
1 2 |
renderRoot -prepareFreshStack // 用于生成空的栈结构 |
这一步主要是workInProgressRoot
的相关属性设置,并从HostRoot
创建第一个workInProgress
(一个FiberNode
类型的节点)。
1 2 3 |
// 用alternate字段,关联HostRoot和workInProgress workInProgress.alternate = current; current.alternate = workInProgress; |
有了HostRoot
,就可以在此之上构建整棵树了。整个过程作为一次update
。一次update
即包含data
变更(通常是setState
)到最后render
的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Initial mount should not be batched. unbatchedUpdates(function () { updateContainer(children, fiberRoot, parentComponent, callback); }); -updateContainer -scheduleRootUpdate -createUpdate // <strong>(1)</strong> -enqueueUpdate(FiberNode, update) // <strong>(2)</strong> -scheduleWork(FiberNode, expirationTime) // <strong>(3)</strong> =scheduleUpdateOnFiber -renderRoot // (!)初次渲染时是同步的所以是workLoopSync,后续React版本会使用非同步的workLoop // 区别在于会在performUnitOfWork之后判断是否需要处理优先任务(shouldYield) -workLoopSync |
workLoopSync
这个函数只有3行
1 2 3 |
while (workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress); } |
performUnitOfWork
会通过return
第一个子节点,达到遍历的效果。而兄弟节点处理放在completeUnitOfWork
里(这就是为什么组件根节点可以用数组放多个)。
1 2 3 4 |
-beginWork -updateHostRoot -reconcileChildren -workInProgress.child = reconcileChildFibers |
reconcileChildFibers
这里会按照子节点类型不同,生成不同type
的FiberNode
,当type
是字符串时,child
会为null
,表示其为叶子节点,也是前面workLoopSync
中遍历的终点。
整个执行流程:
1 2 3 4 5 6 |
beginWork... (HostRoot) beginWork... <App>(IndeterminateComponent) beginWork... <Foo>(IndeterminateComponent) beginWork... <span>(HostComponent) completeUnitOfWork... <span>(HostComponent) // 向上遍历设置父级effect,并处理兄弟节点(sibling),直到父级return为null commitRoot... (HostRoot) |
附:React
代码debug
方式
虽然React
管放文档关于代码的部分比较详细(在 这里),但是直接拿来看还是一脸懵逼。一个办法是,跑测试用例,直接debug
也是理解代码的一个比较好的方式。
1、执行create-react-app
;
2、从github
上checkout
一份代码下来,然后npm run build
编译react
库。
进入build
目录。分别cd react
,cd react-dom
,执行yarn link
。创建快捷方式。
进入create-react-app
的根目录,执行yarn link react react-dom
。
准备工作执行完,就可以在App.js
里打断点,开始debug
。在源代码中打断点,步进,调试并观察。
Comments: 0