Open jsonz1993 opened 5 years ago
前面我们已经讲了React渲染过程 JSX到ReactElement 再到 ReactRoot的创建、fiberTreeRoot的创建以及确定任务优先级等。 接下来就是看React怎么生成整一棵 fiberTree,以及怎么把fiberTree对应到浏览器的DomTree。
beginWork 应该是第一个具体操作到fiber的方法,会根据我们 workInProgress.tag 来调用对应的操作方法来创建一个fiber节点,绑定到fiber tree上面,然后返回下一个工作单元(nextUnitOfWork)回到 workLoop 继续循环这个过程,直到整个fiber tree都创建完毕。
beginWork
workInProgress.tag
nextUnitOfWork
workLoop
我们举两个例子来看具体的fiber操作做了什么东西,如果workInProgress是 HostRoot ,即我们一直说的fiberTreeRoot,这时候会调用updateHostRoot处理,如果是ReactClass的类型,就会调用updateClassComponent处理。
HostRoot
fiberTreeRoot
updateHostRoot
ReactClass
updateClassComponent
比如我们现在 workInProgress.tag === 3,意味着这是一个 HostRoot 类型的fiber。这时候我们调用updateHostRoot方法。
还记得之前在 第一阶段scheduleRootUpdate 的时候,已经给fiber.updateQueue推了一个update任务payload: { element: T组件ReactElement }。
scheduleRootUpdate
fiber.updateQueue
payload: { element: T组件ReactElement }
这里会先调用 processUpdateQueue 处理 fiber.updateQueue。
processUpdateQueue
然后调用 reconcileChildren 创建一个对应的fiber。把fiber挂在fiber tree上面,具体的操作是把fiber.return指向workInProgress, 再把workInProgress.child指向创建的fiber,这就形成一棵最简单的fiber tree。
reconcileChildren
处理完之后把workInProgress.child返回到workLoop,完成一次 unitOfWork。
workInProgress.child
unitOfWork
processUpdateQueue最主要的任务就是处理update任务,一般处理最多的情况是setState的时候推进来的update任务。也就是说,我们平时 this.setState 之所以不能立刻拿到变更后的状态,是因为setState不是同步的,这样可以大大提高渲染性能,不会每一次setState就执行一次更新操作。
this.setState
updateQueue其实也是一个单向链表,每一个 update.next 都指向下一个 update任务。至于为什么React里面,大量用了单向链表而不是数组,我个人觉得是因为单向链表比数组的性能更优,虽然我们平时工作可能没什么影响,不过对于一个大型框架来说,一点点的优化都有很大的提升。具体可以看这一篇 单向链表和数组的比较
调用 getStateFromUpdate 获取 nextState,getStateFromUpdate里面会分 replaceState、CaptureUpdate、UpdateState、ForceUpdate 几种情况处理,然后返回最新的state,这一块后面可以结合setState拎出来单独说。
getStateFromUpdate
nextState
setState
执行完之后对 update.next 执行一样的逻辑,直到整条 updateQueue 处理完。
update.next
updateQueue
最后把最新的state更新到对应的地方,如 workInProgress(fiber).memoizedState (我们组件内部用到的 this.state) 。
workInProgress(fiber).memoizedState
这里面会调用到 getDerivedStateFromProps、UNSAFE_componentWillMount、UNSAFE_componentWillReceiveProps、shouldComponentUpdate等生命周期函数
getDerivedStateFromProps
UNSAFE_componentWillMount
UNSAFE_componentWillReceiveProps
shouldComponentUpdate
react会根据是否是第一次渲染该组件分两种情况处理。
ReactElement
type
workInProgress.stateNode
workInProgress.stateNode = new workInProgress.type(props, context)
classComponent
UNSAFE_componentWillMount(componentWillMount)
shouldUpdate=true
UNSAFE_componentWillReceiveProps(componentWillReceiveProps)
shouldUpdate = false
执行完上面的生命周期之后,调用instance(classComponent).render获取返回的ReactElement,基于这个ReactElement创建一个fiber挂载到 workInProgress.child,本次的work就算完成,接下来就是把 workInProgress.child 作为 nextUnitOfWork 继续下一次的 workLoop。
instance(classComponent).render
基本上整棵fiber tree就是用这种递归方法创建出来的。
performUnitOfWork
workInProgress.child === null
completeUnitOfWork
fiber.tag
HostText
document.createTextNode
fiber.stateNode
workInProgress.siblings
preformUnitOfWork
next
workInProgress.return
return
fiber tree 就是根据child, siblings, return(parent) 这种顺序逐步递归完成整颗fiber tree,并把fiber对应的dom节点绑定到stateNode属性
当我们的fiber tree创建完成之后,就会退出workLoop,开始准备commit的阶段。也就是说前面做了很多工作,调度,diff,创建fiber,创建dom节点等等,都是基于firberTree。而接下来的commit操作是将我们前面做的这些工作都对应到浏览器的domTree上。
commit
首先我们会重新设置一些全局开关,如isCommiting= isWorking= true,设置这些开关是为了放置重复执行。 然后开始处理fiber tree上的effect,将 firstEffect和nextEffect都设置为root.finishedWork.firstEffect。作为第一个处理的effect,然后 nextEffect = nextEffect.next来指定下一个 effect任务(也是单向链表)。
effect
firstEffect
nextEffect
root.finishedWork.firstEffect
nextEffect = nextEffect.next
接下来会好几次对整条 effect 链进行处理的操作
第一次循环:调用生命周期函数getSnapshotBeforeUpdate,并把结果缓存在instance.__reactInternalSnapshotBeforeUpdate。 这里的 getSnapshotBeforeUpdate 是React16.3.0的feature
getSnapshotBeforeUpdate
instance.__reactInternalSnapshotBeforeUpdate
第二次循环:调用commitAllHostEffects,会先对所有的ref设置为null,之所以要这么做是因为怕后面生命周期调用的时候,ref的引用和最新的fiber tree不一致。然后根据effect.effectTag来做对应的处理,有(Placement || Update || Deletion)几种情况。
commitAllHostEffects
effect.effectTag
比如Placement,会先获取 hostParentFiber,既我们的 ReactRoot ,然后执行 ReactRoot.stateNode.containerInfo.appendChild(HostComponent.stateNode) 完成domTree的挂载。也就是这一步我们已经完成了fiberTree到浏览器domTree的操作
Placement
hostParentFiber
ReactRoot
ReactRoot.stateNode.containerInfo.appendChild(HostComponent.stateNode)
commitRoot之所以分几次遍历effect链表,而不是遍历一次直接执行所有逻辑,个人觉得一方面是方便记录每个环节花费的时间,另一方面一方面是边界错误处理、上下文会比较受控。
第三次循环: 挂载完dom之后,就开始执行我们的生命周期函数。大家都知道一般我们挂载之后就会执行一次componentDidMount,那React是怎么控制只执行一次的呢?
componentDidMount
React根据effect.alternate这个值来判断我们这个fiber是不是第一次mount,如果是的话就执行componentDidMount,如果不是就执行componentDidUpdate。执行完之后如果updateQueue队列不为空,意味着这些生命周期函数里面又有一些更新任务,如调用了 this.setState,这时候就会回到processUpdateQueue处理工作。
effect.alternate
componentDidUpdate
到这一步,我们整个组件的生命周期函数就算全部调用完成。也就是说我们现在的fiber tree算是暂时的最终态,调用组件上的 ref 函数,把fiber.stateNode当做参数处理ref的引用。
ref
这些都处理完就会调用 ReactRoot上面的callback,在本例是console.log(args)。
console.log(args)
全部effect执行完,意味着我们当前ReactRoot的render的工作已经告一段落。修改isCommitting, isWorking, isRendering这些开关为false。
isCommitting, isWorking, isRendering
实际上这时候会返回到performWork执行 findHighestPriorityRoot,寻找下一个最高优先级的ReactRoot来继续上述的Render操作,不过目前还不清楚这种多ReactRoot的场景是什么情况。
findHighestPriorityRoot
在ReactDOM.render执行的最后会返回RootInstance,整个ReactDOM.render就算执行完成。
ReactDOM.render
RootInstance
到这里,Reac基本的工作原理过了一遍。 虽然很多经典的地方没有详细说,但是对整个工作机制都有一定的了解,后面如果工作遇到和React问题,大概能知道是哪个环节出的问题。或者对哪一块比较感兴趣想要去看,也容易定位到。
前面我们已经讲了React渲染过程 JSX到ReactElement 再到 ReactRoot的创建、fiberTreeRoot的创建以及确定任务优先级等。 接下来就是看React怎么生成整一棵 fiberTree,以及怎么把fiberTree对应到浏览器的DomTree。
第四阶段 递归创建 fiber tree
beginWork
beginWork
应该是第一个具体操作到fiber的方法,会根据我们workInProgress.tag
来调用对应的操作方法来创建一个fiber节点,绑定到fiber tree上面,然后返回下一个工作单元(nextUnitOfWork
)回到workLoop
继续循环这个过程,直到整个fiber tree都创建完毕。我们举两个例子来看具体的fiber操作做了什么东西,如果workInProgress是
HostRoot
,即我们一直说的fiberTreeRoot
,这时候会调用updateHostRoot
处理,如果是ReactClass
的类型,就会调用updateClassComponent
处理。updateHostRoot HostRoot类型 FiberTreeROOT
比如我们现在 workInProgress.tag === 3,意味着这是一个
HostRoot
类型的fiber。这时候我们调用updateHostRoot
方法。还记得之前在 第一阶段
scheduleRootUpdate
的时候,已经给fiber.updateQueue
推了一个update任务payload: { element: T组件ReactElement }
。这里会先调用
processUpdateQueue
处理fiber.updateQueue
。然后调用
reconcileChildren
创建一个对应的fiber。把fiber挂在fiber tree上面,具体的操作是把fiber.return指向workInProgress, 再把workInProgress.child指向创建的fiber,这就形成一棵最简单的fiber tree。处理完之后把
workInProgress.child
返回到workLoop,完成一次unitOfWork
。processUpdateQueue 处理更新队列 (setState的异步处理核心)
processUpdateQueue最主要的任务就是处理update任务,一般处理最多的情况是setState的时候推进来的update任务。也就是说,我们平时
this.setState
之所以不能立刻拿到变更后的状态,是因为setState不是同步的,这样可以大大提高渲染性能,不会每一次setState就执行一次更新操作。updateQueue其实也是一个单向链表,每一个 update.next 都指向下一个 update任务。至于为什么React里面,大量用了单向链表而不是数组,我个人觉得是因为单向链表比数组的性能更优,虽然我们平时工作可能没什么影响,不过对于一个大型框架来说,一点点的优化都有很大的提升。具体可以看这一篇 单向链表和数组的比较
调用
getStateFromUpdate
获取nextState
,getStateFromUpdate
里面会分 replaceState、CaptureUpdate、UpdateState、ForceUpdate 几种情况处理,然后返回最新的state,这一块后面可以结合setState
拎出来单独说。执行完之后对
update.next
执行一样的逻辑,直到整条updateQueue
处理完。最后把最新的state更新到对应的地方,如
workInProgress(fiber).memoizedState
(我们组件内部用到的 this.state) 。updateClassComponent 创建React.Component类型的fiber
这里面会调用到
getDerivedStateFromProps
、UNSAFE_componentWillMount
、UNSAFE_componentWillReceiveProps
、shouldComponentUpdate
等生命周期函数react会根据是否是第一次渲染该组件分两种情况处理。
第一种情况: 第一次渲染的情况
ReactElement
的时候,如果发现是一个Class,就会把它绑定给type
属性,忘记的同学可以看这一篇 React源码系列(二): 从jsx到createElement 。这里如果是第一次渲染,就会实例化一个组件绑定到workInProgress.stateNode
,既workInProgress.stateNode = new workInProgress.type(props, context)
。classComponent
的生命周期函数 __getDerivedStateFromProps
、UNSAFE_componentWillMount(componentWillMount)
__ 。shouldUpdate=true
。第二种情况: 更新的情况
UNSAFE_componentWillReceiveProps(componentWillReceiveProps)
__ 获取最新的props和stateshouldUpdate = false
。getDerivedStateFromProps
更新state。再调用shouldComponentUpdate
来确定是否需要更新,要更新的话就调用生命周期函数UNSAFE_componentWillMount(componentWillMount)
。finishClassComponent
执行完上面的生命周期之后,调用
instance(classComponent).render
获取返回的ReactElement
,基于这个ReactElement
创建一个fiber挂载到workInProgress.child
,本次的work就算完成,接下来就是把workInProgress.child
作为nextUnitOfWork
继续下一次的 workLoop。基本上整棵fiber tree就是用这种递归方法创建出来的。
completeUnitOfWork: 会根据生成的fiber创建对应的dom,挂载到fiber.stateNode
performUnitOfWork
之后发现workInProgress.child === null
,意味着我们已经到了一个组件的最深处,也就是已经没有子级了。completeUnitOfWork
。 根据fiber.tag
对当前的fiber进行对应的dom层面的操作 ,例如 tag === 6,意味着这个fiber为HostText
类型,就调用document.createTextNode
创建对应的dom节点,再把这个dom节点绑定到fiber.stateNode
,就算完成一个fiber的dom层面操作。workInProgress.siblings
是否为空,不为空就把他return出去继续下一次的preformUnitOfWork
。workInProgress.child
为空,workInProgress.siblings
也为空。意味着当前fiber已经当前fiber的所有兄弟fiber都处理完了(可以参考domTree比较好理解),这时候就将next
设置为workInProgress.return
,因为return
已经是一个fiber了,所以我们不需要返回到 workLoop,而是回到completeUnitOfWork
的第二步进行fiber的dom层面处理。fiber tree 就是根据child, siblings, return(parent) 这种顺序逐步递归完成整颗fiber tree,并把fiber对应的dom节点绑定到stateNode属性
到这里fiber tree需要的东西基本上准备完了,我们理一下fiber。整一个fiber tree创建的过程,都是可以被中断的,这也是异步说法的由来。
第五阶段 Commit
当我们的fiber tree创建完成之后,就会退出
workLoop
,开始准备commit
的阶段。也就是说前面做了很多工作,调度,diff,创建fiber,创建dom节点等等,都是基于firberTree。而接下来的commit
操作是将我们前面做的这些工作都对应到浏览器的domTree上。commitRoot
首先我们会重新设置一些全局开关,如isCommiting= isWorking= true,设置这些开关是为了放置重复执行。 然后开始处理fiber tree上的
effect
,将firstEffect
和nextEffect
都设置为root.finishedWork.firstEffect
。作为第一个处理的effect
,然后nextEffect = nextEffect.next
来指定下一个effect
任务(也是单向链表)。接下来会好几次对整条 effect 链进行处理的操作
getSnapshotBeforeUpdate
第一次循环:调用生命周期函数
getSnapshotBeforeUpdate
,并把结果缓存在instance.__reactInternalSnapshotBeforeUpdate
。 这里的getSnapshotBeforeUpdate
是React16.3.0的featuresetRef(null) && perform effectTag
第二次循环:调用
commitAllHostEffects
,会先对所有的ref设置为null,之所以要这么做是因为怕后面生命周期调用的时候,ref的引用和最新的fiber tree不一致。然后根据effect.effectTag
来做对应的处理,有(Placement || Update || Deletion)几种情况。比如
Placement
,会先获取hostParentFiber
,既我们的ReactRoot
,然后执行ReactRoot.stateNode.containerInfo.appendChild(HostComponent.stateNode)
完成domTree的挂载。也就是这一步我们已经完成了fiberTree到浏览器domTree的操作commitRoot之所以分几次遍历effect链表,而不是遍历一次直接执行所有逻辑,个人觉得一方面是方便记录每个环节花费的时间,另一方面一方面是边界错误处理、上下文会比较受控。
commitAllLifeCycles
第三次循环: 挂载完dom之后,就开始执行我们的生命周期函数。大家都知道一般我们挂载之后就会执行一次
componentDidMount
,那React是怎么控制只执行一次的呢?React根据
effect.alternate
这个值来判断我们这个fiber是不是第一次mount,如果是的话就执行componentDidMount
,如果不是就执行componentDidUpdate
。执行完之后如果updateQueue
队列不为空,意味着这些生命周期函数里面又有一些更新任务,如调用了this.setState
,这时候就会回到processUpdateQueue
处理工作。到这一步,我们整个组件的生命周期函数就算全部调用完成。也就是说我们现在的fiber tree算是暂时的最终态,调用组件上的 ref 函数,把fiber.stateNode当做参数处理
ref
的引用。这些都处理完就会调用 ReactRoot上面的callback,在本例是
console.log(args)
。全部effect执行完,意味着我们当前ReactRoot的render的工作已经告一段落。修改
isCommitting, isWorking, isRendering
这些开关为false。实际上这时候会返回到performWork执行
findHighestPriorityRoot
,寻找下一个最高优先级的ReactRoot来继续上述的Render操作,不过目前还不清楚这种多ReactRoot的场景是什么情况。在
ReactDOM.render
执行的最后会返回RootInstance
,整个ReactDOM.render就算执行完成。结语
到这里,Reac基本的工作原理过了一遍。 虽然很多经典的地方没有详细说,但是对整个工作机制都有一定的了解,后面如果工作遇到和React问题,大概能知道是哪个环节出的问题。或者对哪一块比较感兴趣想要去看,也容易定位到。