tech-blog
tech-blog copied to clipboard
React16.2源码解析-页面渲染流程
前言
本文中
workInProgress
指的是正在工作的 fiber
。
"更新队列"指 workInProgress.updateQueue
流程
jsx => element tree => fiber tree => html dom
React
渲染页面
-
schedule work
执行虚拟 DOM
( fiber
树)的更新,有 scheduleWork
, requestWork
, performWork
是三部曲。
-
reconcile work
- 调度阶段(
render/reconciliation
):在这个阶段React
会更新数据生成新的Virtual DOM
,然后通过Diff
算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。(改变pendingState
,pendingProps
,最后构建新的fiber tree
)。在初始时则是生成子fiber
,连接fiber
与siblingFiber
- 渲染阶段(
commit
):这个阶段React
会遍历更新队列,将其所有的变更一次性更新到DOM
上。并触发响应生命周期函数钩子。
- 调度阶段(
流程图
什么是JSX
JSX
是 JavaScript
的一种语法扩展,它很像一种模板语言
JSX
应用实例:
-
JSX
作为表达式
function getElement () {
if (flag) {
return <p>true</p>
}
return <p>false</p>
}
const element = getElement();
- JSX指定属性
const element = <div className="element">element</div>
生命周期
componentWillReceiveProps在props改变时执行
JSX的编译方式
在 react
中,通过 babel
可以将 JSX
编译成 JavaScript
代码
比如 JSX
表达式
const element = (
<div className="app">
hi
</div>
)
编译成 JavaScript
const element = React.createElement('div', {
className: 'app'
}, 'hi')
child
以参数形式一个个传入 React.createElement
(而不是以数组形式)
编译得到 element
的流程如下:
在得到首个 element
后,便能执行 ReactDOM.render
函数,并将 element,container,callback
传入
1. schedule work
阶段
1.1 renderSubtreeIntoContainer
渲染一个元素到 DOM
,将 container
中原有的子元素删除。比如下列代码,会将原有的 p
元素删除,而用 <div>child</div>
代替
// html
<div id="app">
<p>child</p>
</div>
ReactDOM.render(<div>child</div>, document.querySelector('#app'))
待新的 root
构建好后,将之与 container
关联: root = container._reactRootContainer = newRoot
1.2 createContainer
创建 fiber root
var root = {
current: {
alternate: null,
tag,
key,
type,
stateNode: root,
child,
return,
index,
ref,
pendingProps,
memoizedProps,
updateQueue,
memoizedState,
effectTag,
nextEffect
firstEffect,
lastEffect,
expirationTime
},
containerInfo: containerInfo,
pendingChildren: null,
remainingExpirationTime: NoWork,
isReadyForCommit: false,
finishedWork: null,
context: null,
pendingContext: null,
hydrate: hydrate,
nextScheduledRoot: null
};
1.3 scheduleTopLevelUpdate
创建 update
对象,初始的 fiber root
的 updateQueue
中,partialState
是 element
组成的对象
var update = {
expirationTime: expirationTime,
partialState: { element: element },
callback: callback,
isReplace: false,
isForced: false,
nextCallback: null,
next: null
}
1.4 insertUpdateIntoFiber
将 update
对象植入 current fiber
中。挂载在 first
和 last
属性上
root.current.updateQueue = {
baseState: null,
expirationTime: NoWork,
first: { // update对象
expirationTime: expirationTime,
partialState: { element: element },
callback: callback,
isReplace: false,
isForced: false,
nextCallback: null,
next: null
},
last: { // update对象
expirationTime: expirationTime,
partialState: { element: element },
callback: callback,
isReplace: false,
isForced: false,
nextCallback: null,
next: null
},
callbackList: null,
hasForceUpdate: false,
isInitialized: false,
isProcessing: false
}
1.5 scheduleWorkImpl
用一个 while
循环沿着 parent
位置向上遍历 fiber
,直到 root.current
,并且更新每个 fiber
的 expirationTime
然后再 requestWork
1.6 requestWork
当调度器有 update
工作的时候,就会调用 requestWork
。在其中改变 firstScheduledRoot
, lastScheduledRoot
等被调度的 FiberRoot
。
函数中需要判断的 isBatchingUpdates
,在事件触发后会改变。以下代码是事件触发后会执行,所以在 requestWork
时,isBatchingUpdates
为 true
function batchedUpdates(fn, a) {
var previousIsBatchingUpdates = isBatchingUpdates; // 批量处理嘛
isBatchingUpdates = true;
try {
return fn(a); // // 此过程中可能改变state所以需要再performWork
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performWork(Sync, null);
}
}
}
1.7 performWork
找到优先级最高的 work
,并激活它,此后开始构建 fiber
树
1.8 performWorkOnRoot
开始渲染,isRendering = true
区分同步渲染还是异步渲染
在 performWorkOnRoot
方法中有两个重要的步骤。1. renderRoot
。2. commitRoot
。
2. reconcile work
阶段
详情见:Reconciliation
2.1 renderRoot
开始 work
,设置 workloop
函数中所用到的 nextUnitOfWork
(复制 nextRoot.current
得到) ,然后执行 workloop
循环函数,得到更新后的 fiber
树。
如下图:创建 workInProgress
(复制 current fiber
(这也是 current.alternate
和 current
不全等的原因)),并将 workInProgress
的 alternate
指向 current fiber
,在之后使用 workInProgress
的时候,都是用它的替代品( current.alternate
)来作文章
nextUnitOfWork = createWorkInProgress(nextRoot.current, null, expirationTime);
// 结果如下
var current = root.current;
nextUnitOfWork = {
alternate: current,
stateNode: root,
tag,
key,
child,
sibling,
return,
effectTag
....
}
所以在 reconcileChildren
阶段,改变了 workInProgress.child
,但是 current.child
不会改变。也就是 workInProgress
是 current
的复制品,所以 workInProgress
的属性改变并不会导致 current
属性改变。
经过 renderRoot
后,得到 root.current.alternate
var finishedWork = root.current.alternate = {
alternate: current,
child: {
alternate: null,
child: {
alternate: null,
child: null
},
sibling: {
alternate: null,
child: {
alternate: null,
child: null,
sibling: null
}
}
}
}
2.1.1 workloop
以深度遍历方式,激活所有期望 work
,对每个 work
都执行 performUnitOfWork
function workLopp () {
while (nextUnitOfWork !== null) { // 激活所有期望任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
实现深度遍历:
遍历逻辑如下:
对1中提到的 workInProgress
或者说 nextUnitOfWork
开始工作,对下面层级的子 fiber
进行更新。
2.1.2 performUnitOfWork
performUnitOfWork
中有两个重要函数,beginWork
和 completeUnitOfWork
。
var next = beginWork(current, workInProgress, nextRenderExpirationTime);
if (next === null) { // 表示workInProgress为叶节点,无next了,子节点只是text了。
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
return next;
2.1.2.1 beginWork
期间执行生命周期函数componentWillMount
,componentWillReceiveProps
,componentWillUpdate
。在 updateClassComponent
中执行 render
根据 workInProgress
的 tag
( component
类型)来做出相应的更新。将更新后得到的 child
(可为 null
)返回,置为 child
属性,此时 alternate
仍未 null
在 bailoutOnAlreadyFinishedWork
函数中,可以改变 workInProgress.child
及workInProgress.child.alternate
childFiber.alternate
改变是在 cloneChildFibers
过程,将本来为 null
的 alternate
改为指向 childFiber
有如下 update
操作:
- mountIndeterminateComponent
- updateFunctionalComponent
- updateClassComponent
- 通过判断
workInProgress.alternate
是否为null
,来判断是否是第一次处理 该workInProgress
。否就执行2,是则跳到3。待2、3执行完后跳到5- 执行
updateClassInstance
来更新instance
实例。如果不能渲染,则不改变实例。在此函数中还需要处理workInProgress.updateQueue
。另外需要调用更新前的生命周期函数:componentWillReceiveProps
,componentWillUpdate
。另外需要替换新旧props
和state
,在更新完后,通过在此步中设置的effectTag
来调用生命周期函数:componentDidUpdate
。最后更新instance
上的props,state,refs
等属性- 执行
constructClassInstance
,更具工作任务的type
来创建instance
实例(在fiber
架构中有描述)。并给instance
对象属性赋值,并关联instance
和workInProgress
- 执行
mountClassInstance
,给instance
添加props,state,refs
等属性。调用生命周期函数componentWillMount
,并在钩子函数执行完后检查updateQueue
。改变effectTag
,在mount
后执行componentDidMount
钩子。- 执行
finishClassComponent
,调用instance.render()
,得到element
,并将此element和workInProgress
传入reconcileChildFibers
中进行调和,如果element
不为null
,根据$$typeof
类型创建childFiber
,将childFiber
挂载在workInProgress
的child
属性上,最后返回child
- updateHostRoot
如果 workInProgress
更新队列(如1.4中的updateQueue
)不为 null
,则处理更新队列。在处理过程中将 callback
回调函数推进更新队列的 callbackList
,并将 update
对象链的 partialState
合并。
- 从需要更新的对象开始。
var update = updateQueue.first
- 往
update = update.next
走,直到update.next
为null
- 将更新对象
update
经过getStateFromUpdate
处理,返回partialState
- 将
partialState
与上一个state
进行assign
合并- 处理完后将
workInProgress.updateQueue
重置为null
设置 workInProgress.effectTag
,标记 commit
阶段的操作(插入、更新、插入并更新、删除)
给 workInProgress
挂载 child fiber
,并返回该 child
。在处理 child
的过程中,需要创建 fiber
,有这几种方式 createFiberFromFragment,createFiberFromFragment,useFiber
如果workInProgress 更新队列(如1.4中的updateQueue)为,则返回 null。则返回 workInProgress.child
- updateHostComponent
此方法为更新形如 div、p
标签之类的 html
组件。故在 reconcileChildren
时传入的是 props.children
。如果 props.children
是文本字符串而不是 element
,则改变 workInProgress
的 effectTag
以便在 commit
阶段重置内容,并且传入 reconcileChildFibers的newChild
为 null
,并且最后返回 null
。
如果 props.children
为数组,则在 reconcileChildrenArray
中按序给每个元素进行调和,用 sibling
将每个 fiber
连接起来。最后返回第一个 child
。
在更新时 performWork
的过程中,若新旧 props
没有改变,则直接返回 workInProgress.child
- updateHostText
- updateCallComponent
- updatePortalComponent
- updateFragment
2.1.2.2 completeUnitOfWork
如果 beginWork
返回的 child
为 null
,则证明 workInProgress
已没有孩子 fiber
了,这时就需要去找 workInProgress
的下一个可遍历 fiber
节点。可以是返回 sibling fiber
,没有的话是找父级的 sibling fiber
并返回。
在此函数的 while
循环中,如果 workInProgress.tag
大于 PerformedWork
所对应的值,设置 returnFiber
的 effect
串,在下一次循环时,将此 fiber
的 effect
串又传递给returnFiber
-
completeWork
- 将
pendingProps
挂到memoizedProps
- 根据
workInProgress
的tag
来标记commit
阶段如何操作,如改变effectTag
- 改变
effect
串,如firstEffect,lastEffect,nextEffect
- 可以创建
instance
实例,并添加子节点 - 期间绑定事件
- 执行
diff
算法
- 将
2.2 commitRoot
finishedWork = renderRoot(root, expirationTime);
commitRoot(finishedWork);
在 renderRoot
后构建了一个新的 fiber
树,但此树只是 root.current
的替代品,还没有真正的应用到 root.current
上。
但是当 commitRoot
后,将 root.current
改变为 finishedWork
。这时 root.current.alternate === finishedWork.alternate
。为最初的 currentFiber
对象.
在 commitRoot
中,将已经连接起来的 effect
串从头开始执行,对于 effect fiber
,根据其 effectTag
,来标记其操作方式。
当渲染 dom
时,由组件为单位,从里到外渲染。
2.2.1 commitAllHostEffects
详情见: 视图改变
- commitPlacement
向 parent
插入真实 dom
节点
2.2.2 commitAllLifeCycles
- commitLifeCycles
根据 effect fiber
的 effectTag
和 alternate
来决定钩子函数如何执行。可执行 componentDidMount
、componentDidUpdate
var finishedWork = nextEffect;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.props = finishedWork.memoizedProps;
instance.state = finishedWork.memoizedState;
instance.componentDidMount(); // 执行componentDidMount钩子函数
} else {
var prevProps = current.memoizedProps;
var prevState = current.memoizedState;
instance.props = finishedWork.memoizedProps;
instance.state = finishedWork.memoizedState;
instance.componentDidUpdate(prevProps, prevState);
}
}
关于 currentFiber.alternate
的改变
下列示范代码,详细请看源码的 createWorkInProgress
var current = {
alternate: null,
b: 1,
c: 2
}
var workInProgress = { // 复制current,创建workInProgress
alternate: null,
b: 1,
c: 2
}
workInProgress.alternate = current;
current.alternate = workInProgress;
current = {
alternate: { // workInProgress
alternate: current,
b: 1,
c: 2
},
b: 1,
c: 2
}
return workInProgress
能问下,思维导图用的是什么软件吗?
@luke93h xmind
好的,谢谢啊~
想问一下,那个 fiber 的时间分片是怎么体现在源码中的啊?源码中大量的 expirationTime、updateTime、next..Time 之类的时间用处在哪?还有各种的 startTimer、stopTimer 也不太理解是干嘛用的哎
箭头反了