jsonz1993 / react-source-learn

react16 源码阅读学习记录
151 stars 24 forks source link

React源码系列(四): Fiber Tree && commit #4

Open jsonz1993 opened 5 years ago

jsonz1993 commented 5 years ago

前面我们已经讲了React渲染过程 JSX到ReactElement 再到 ReactRoot的创建、fiberTreeRoot的创建以及确定任务优先级等接下来就是看React怎么生成整一棵 fiberTree,以及怎么把fiberTree对应到浏览器的DomTree。

第四阶段 递归创建 fiber tree

beginWork

begin-work

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 获取 nextStategetStateFromUpdate里面会分 replaceState、CaptureUpdate、UpdateState、ForceUpdate 几种情况处理,然后返回最新的state,这一块后面可以结合setState拎出来单独说。

执行完之后对 update.next 执行一样的逻辑,直到整条 updateQueue 处理完。

最后把最新的state更新到对应的地方,如 workInProgress(fiber).memoizedState (我们组件内部用到的 this.state)

processupdatequeue

updateClassComponent 创建React.Component类型的fiber

这里面会调用到 getDerivedStateFromPropsUNSAFE_componentWillMountUNSAFE_componentWillReceivePropsshouldComponentUpdate等生命周期函数

react会根据是否是第一次渲染该组件分两种情况处理。

第一种情况: 第一次渲染的情况

  1. 我们在一开始创建ReactElement的时候,如果发现是一个Class,就会把它绑定给 type属性,忘记的同学可以看这一篇 React源码系列(二): 从jsx到createElement 。这里如果是第一次渲染,就会实例化一个组件绑定到 workInProgress.stateNode,既 workInProgress.stateNode = new workInProgress.type(props, context)
  2. 调用 classComponent 的生命周期函数 __getDerivedStateFromPropsUNSAFE_componentWillMount(componentWillMount)__ 。
  3. 因为这是第一次渲染,所以直接设置 shouldUpdate=true

第二种情况: 更新的情况

  1. 调用生命周期函数 __UNSAFE_componentWillReceiveProps(componentWillReceiveProps) __ 获取最新的props和state
  2. 判断组件的 新旧 props、state是否一致,是否有调用强制更新的方法classComponent.forceUpdate 如果没有改变,直接返回false,即 shouldUpdate = false
  3. 如果发现有props、state变动的话,调用生命周期函数 getDerivedStateFromProps 更新state。再调用shouldComponentUpdate来确定是否需要更新,要更新的话就调用生命周期函数UNSAFE_componentWillMount(componentWillMount)

updateclasscomponent

finishClassComponent

执行完上面的生命周期之后,调用instance(classComponent).render获取返回的ReactElement,基于这个ReactElement创建一个fiber挂载到 workInProgress.child,本次的work就算完成,接下来就是把 workInProgress.child 作为 nextUnitOfWork 继续下一次的 workLoop。

基本上整棵fiber tree就是用这种递归方法创建出来的。

completeUnitOfWork: 会根据生成的fiber创建对应的dom,挂载到fiber.stateNode

  1. 当我们执行 performUnitOfWork 之后发现 workInProgress.child === null,意味着我们已经到了一个组件的最深处,也就是已经没有子级了。
  2. 这这时候会调用 completeUnitOfWork根据 fiber.tag 对当前的fiber进行对应的dom层面的操作 ,例如 tag === 6,意味着这个fiber为HostText类型,就调用document.createTextNode创建对应的dom节点,再把这个dom节点绑定到fiber.stateNode,就算完成一个fiber的dom层面操作。
  3. 处理完之后就会看 workInProgress.siblings 是否为空,不为空就把他return出去继续下一次的 preformUnitOfWork
  4. 如果 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创建的过程,都是可以被中断的,这也是异步说法的由来。

fibertree fiber

第五阶段 Commit

当我们的fiber tree创建完成之后,就会退出workLoop,开始准备commit的阶段。也就是说前面做了很多工作,调度,diff,创建fiber,创建dom节点等等,都是基于firberTree。而接下来的commit操作是将我们前面做的这些工作都对应到浏览器的domTree上。

commitRoot

首先我们会重新设置一些全局开关,如isCommiting= isWorking= true,设置这些开关是为了放置重复执行。 然后开始处理fiber tree上的effect,将 firstEffectnextEffect都设置为root.finishedWork.firstEffect。作为第一个处理的effect,然后 nextEffect = nextEffect.next来指定下一个 effect任务(也是单向链表)。

接下来会好几次对整条 effect 链进行处理的操作

image

getSnapshotBeforeUpdate

第一次循环:调用生命周期函数getSnapshotBeforeUpdate,并把结果缓存在instance.__reactInternalSnapshotBeforeUpdate。 这里的 getSnapshotBeforeUpdate 是React16.3.0的feature

setRef(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就算执行完成。

publicrootinstance

fifth

结语

到这里,Reac基本的工作原理过了一遍。 虽然很多经典的地方没有详细说,但是对整个工作机制都有一定的了解,后面如果工作遇到和React问题,大概能知道是哪个环节出的问题。或者对哪一块比较感兴趣想要去看,也容易定位到。