react fiber 的运行机制
前言
1. 不同的元素类型决定不同的任务类型
2. react 的元素类型有 class component , function component ,Dom nodes, portal等等
要理解 fiber 调度算法,首先要了解实现该算法的数据结构:
jsx -> react element :
{
$$typeof:Symbol(react.element),
key:'',
props:{},
ref:null,
type:''
}
react element -> fiber node:
react element 转换成对应类型的 fiber node ,fiber node 的类型描述了
要执行了的任务的类型,可以把fiber node看成是一个任务执行单元。
在后面的更新中,react 会复用fiber node 仅仅通过对应的react element 上的数据
更新必要的属性。如果对应的react element在render函数中被删除,与其对应的fiber node
也会通过key属性来删除。
react 为每一个element创建了一个fiber node,所以也会得到一个对应的fiber tree。
以链表的方式通过child(子) sibling(兄弟) return(子节点指向父节点) 等指针连接。
current and work in progress trees (当前和正在进行中的tree)
fiber tree 反映的是当前的页面的状态。当react 开始更新时,会创建一个 workInProgress
tree 反映的是将要被渲染到页面上的状态。每一个 fiber node 会创建一个替换的节点,可以看做是
workInProgress tree,这个节点用来自对应的react element 上的数据创建。一旦更新过程进行完并且
所有相关的工作完成,react将会有一颗替换的树准备推向页面。一旦 workInProgress tree被渲染到了页
面上,它就会变成current tree。
react 的核心原则之一是保持一致性。react 总是一次性更新 workInProgress , 不会出现只更新部分dom的情况。
workInProgress 可以看成是页面更新的一个原型,对用户并不可见,所以 react 能够首先处理所有的组件,并一次性的把
这些变更提现在组件上。
fiber tree 上的节点的 alternate 指向 workInprogress 中对应的节点,反过来也是一样。
副作用
可以认为 react 组件通过state 和props 来计算UI 表示。除此之外的dom 操作或者生命周期函数可以被认为是副作用。
因为这些操作会影响其他的组件并且不能在render的时候执行。执行副作用也是一种任务,每个fiber节点 effectTag 来
关联副作用。因此副作用在fiber中定义了在实例更新之后要处理的任务。对应原生的dom节点来说这个任务由新增,删除和
更新节点组成。对于类组件,对应的是更新ref 和调用 componentDidMount或者componentDidUpdate生命周期函数。此外
还有其他的复作用对应了其他的fibre类型。
副作用任务执行
为了快速迭代计算,react 构造了一个线性的有副作用的fiber node链表。相比树形结构,同时不需要花时间在没有副作用的节点上。
链接通过 nextEffect 字段链接下一个节点。
{
stateNode: new HTMLSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: {children: 0},
memoizedProps: {children: 0},
tag: 5,
effectTag: 0,
nextEffect: null
}
stateNode字段指向组件实例。
type:react element (组件)或者dom元素。
tag:定义了fiber 节点的类型
updateQueue:状态更新队列
memoizedState:之前用来创建输出的状态。当正在更新时,它反映了当前在页面上的状态
memoizedProps:上一次渲染用来创建输出的属性
pendingProps:从新数据中更新的属性,将要应用到dom元素或者子元素中
key:在数组中唯一标识该元素,用来帮助react找到变更的元素。
算法:
更新过程分为两个阶段:render 和 commit
在render阶段,通过setState或者react.render(初始化),找到要更新的UI上的内容。
render的阶段的output 是标记了副作用的fiber tree。副作用描述了要在commit阶段
完成的任务。在commit 阶段,react 会把在render阶段的输出应用到实例,同时会遍历
副作用列表把dom 更新或者其他的改变反映到页面上。
在render阶段的任务是可以异步执行的,react 能够根据可用的时间处理一个或者多个
fiber节点,然后暂停并保存结果来执行一些高优先级的事件,执行完后从暂停的位置继续
执行。在某些情况下,可能需要丢掉已经做过的工作重新从顶部开始。能够暂停的是因为在
render 阶段做的工作对用户是不可见的。
相反,接下来的commit阶段总是同步的。这是因为在这个阶段的任务会导致用户界面的改变,例如dom更新,这是为什么react需要在一次更新中完成所用的任务。
调用生命周期函数是react 任务中的一种。一些生命周期函数在 render阶段调用,其他的一些方法在commit 阶段调用。
render 阶段:
componentWillMount (遗弃)
componentWillReceiveProps(遗弃)
getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate
render
render 阶段不会产生副作用,react 可以异步处理组件更新(还可能在多个线程中处理)。然而标记了 unsafe的方法经常误解和误用。开发者可能会把一些有副作用的操作放在这些回调方法中,这可能会导致一些问题在新的一步渲染阶段。
Commit 阶段:
getSnapshotBeforeUpdate
componentDidMount
componentDidUpdate
componentWillUnmount
因为这些方法在同步的commit阶段,这些回调函数可能会包含副作用并且操作dom。
Render阶段:
React的调度算法总是从最顶部的HostRoot fiber节点用renderRoot函数。react 会跳过已经处理的fiber节点直到找到未完成的任务。例如,如果调用setState在很深的组件的树中,react 会重顶部重新开始但是会快速跳过父亲节点直到到达调用setState方法的组件。
任务循环的只要不步骤:
所有的fiber节点都在任务循环中处理。
function workLoop(isYieldy){
If(!isYieldy){
While(nextUnitOfWork !== null){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}else{…}
}
nextUnitOfWork 保存了指向有任务要做的fiber节点。当react 遍历fiber树时,react 用这个变量来识别有未完成任务的fiber节点。nextUnitWork可能保存了下一个fiber节点或者null。当指向null,react 会退出任务循环并且为提交变更做准备。
有4个主要的函数用来遍历fiber树或者初始化或者完成任务。
1. performUnitWork
2. beginWork
3. completeUnitOfWork
4. completeWork
performUnitOfWork 从workInProgressTree接受一个fiber节点,调用beginWork函数来开始工作。这个函数会开始所有需要在fiber中执行的活动。beginWork总是返回一个指针指向下一个要处理的子节点或者null。
如果这里有下一个子节点,会在任务循环中赋值给nextUnitofWork变量。然而,如果没有子节点,react 知道到达了分支的边界,所以会完成当前的节点任务。一旦节点任务完成,react需要执行兄弟节点并且会返回到父亲节点。当workInProgress 节点没有子节点,react会进入这个循环函数。
在commit阶段,这个阶段以completeRoot这个函数开始。当react进入到这个阶段,react中有2棵树和副作用链表。一棵树代表了当前渲染到页面上的状态。然后在render阶段会创建一棵替换的树,即workInProgress树,代表了需要映射到页面上的状态。workInProgress树和fiber树一样也是通过child 和 sibling指针进行关联。副作用链表是render阶段的结果。整个render阶段的目标是找出需要插入,更新,或者删除的节点,同时找到需要调用生命周期函数的组件。这也是在
Commit节点需要遍历的节点的集合。
Commit阶段的步骤:
1. 在标记了Snapshot的节点上调用getSnapshotBeforeUpdate生命周期函数
2. 在标记了Deletion副作用的节点上调用componentWillUnmount生命周期函数。
3. 执行所有节点的插入,更新,删除操作。
4. 设置finishedWork树作为当前的树。
5. 在标记了Placement的节点上调用componentDidmount生命周期函数。
6. 在标记了更新副作用的节点上调用componentDidUpdate生命周期函数。
在调用完预变更方法getSnapshotBeforeUpdate,react会提交所有的副作用。分为两个阶段,第一个阶段:执行所有的dom更新操作以及ref的卸载,然后react会把任务执行完的到fiberRoot的任务完成的树标记为workInProgress树作为当前的树。在第二阶段,react会调用所有的其他的生命周期函数和ref回调函数。这些方法作为独立的阶段因此替换,更新和删除操作已经被调用。
参考:https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/