最近几个项目都是
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 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
。基本上,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的状态,这使得更多功能的实现成为可能:并发模式,错误边界处理等。1、不同的组件类型会生成不同的子树。此时不会去对比子树,而是重新生成一棵新的来取代。
2、列表(
2、列表(
lists
)的对比通过keys
来进行,因此keys
需要“稳定,可预测,且唯一”。Fiber
和之前的调度方式的直观差别?以一个简单的组件来说明。为了直观,只列出最后的结构,省略组件的声明。
value
下面的瀑布图来自
chrome devtools - performance
。react-15-stable


stack flow
react-16.9.0


fiber flow

fiber flow
用16.9代码调试的时候,发现无论怎么添加代码逻辑,都是同步执行,没有看到并发的影子。这是因为
React 16.9
还未正式引进并发模式,估计正式启用要到React 17
了。
unstable api
如果有兴趣,可以手动启用试试效果,不过由于
unstable
,不建议在生产环境开启。// Whole app (not final API)
ReactDOM.unstable_createRoot(domNode).render(); // v16.11 更改为ReactDOM.createRoot
Fiber组织方式
还是以上面例子,最终生成的树结构
fiber structure
FiberNode生成的树结构
由于核心算法没变,所以还是可以看作维护了两棵树:一棵以
HostRoot
为头部节点的current
(原始FiberNode
节点)树,一棵是workInProgress
(工作节点)树,两棵树通过各节点alternate
属性关联。惯例需要先走代码验证一下。
render
legacyRenderSubtreeIntoContainer
root = legacyCreateRootFromDOMContainer
这里两个函数被标记了
legacy
,说明这部分代码是遗留的,也就是说执行流程与之前比没变化,跳过。首先会创建一个
FiberRootNode
,可以理解为一个容器,用于保存当前工作现场,方便pause/resume。接着是一个叫
创建之后即放入
HostRoot
的FiberNode
,tag
被标记为3,type
是null。相当于链表头。创建之后即放入
FiberRootNode
的current
。FiberRootNode.current = HostRoot;
HostRoot.stateNode = FiberRootNode;
renderRoot
-prepareFreshStack // 用于生成空的栈结构
这一步主要是
workInProgressRoot
的相关属性设置,并从HostRoot
创建第一个workInProgress
(一个FiberNode
类型的节点)。// 用alternate字段,关联HostRoot和workInProgress
workInProgress.alternate = current;
current.alternate = workInProgress;
有了
HostRoot
,就可以在此之上构建整棵树了。整个过程作为一次update
。一次update
即包含data
变更(通常是setState
)到最后render
的过程。// Initial mount should not be batched.
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
-updateContainer
-scheduleRootUpdate
-createUpdate // (1)
-enqueueUpdate(FiberNode, update) // (2)
-scheduleWork(FiberNode, expirationTime) // (3)
=scheduleUpdateOnFiber
-renderRoot
// (!)初次渲染时是同步的所以是workLoopSync,后续React版本会使用非同步的workLoop
// 区别在于会在performUnitOfWork之后判断是否需要处理优先任务(shouldYield)
-workLoopSync
workLoopSync
这个函数只有3行while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
performUnitOfWork
会通过return
第一个子节点,达到遍历的效果。而兄弟节点处理放在completeUnitOfWork
里(这就是为什么组件根节点可以用数组放多个)。-beginWork
-updateHostRoot
-reconcileChildren
-workInProgress.child = reconcileChildFibers
reconcileChildFibers
这里会按照子节点类型不同,生成不同type
的FiberNode
,当type
是字符串时,child
会为null
,表示其为叶子节点,也是前面workLoopSync
中遍历的终点。整个执行流程:
beginWork... (HostRoot)
beginWork... (IndeterminateComponent)
beginWork... (IndeterminateComponent)
beginWork... (HostComponent)
completeUnitOfWork... (HostComponent) // 向上遍历设置父级effect,并处理兄弟节点(sibling),直到父级return为null
commitRoot... (HostRoot)
附:React
代码debug
方式
1、执行
2、从
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
。在源代码中打断点,步进,调试并观察。