Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jsonz1993 opened this issue Nov 14, 2018 · 0 comments
Open

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

jsonz1993 opened this issue Nov 14, 2018 · 0 comments

Comments

@jsonz1993
Copy link
Owner

前面我们已经讲了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问题,大概能知道是哪个环节出的问题。或者对哪一块比较感兴趣想要去看,也容易定位到。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant