深入jsx学习
- 在react16的时候,jsx会被babel转化为React.createElement,craeteElement返回一个对象。该对象使用$$typeof显示标识为React Element,这点可以通过react提供的isValidElement来判断该对象是否是React Element。
- 在17的时候,jsx可以不再被转为React.createElement,也就是不用显示引入React就可以转为浏览器可以运行的代码,因为createELement在调优方面不可以做更多的事情
详情:https://zh-hans.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html - Jsx与React组件
我们先打印下函数组件与类组件
class MineClass extends React.Component {
render() {
return <p>KaSong</p>
}
}
console.log('我是类组件', MineClass);
console.log('这是Element:', <MineClass/>);
function MineFun() {
return <p>KaSong</p>;
}
console.log('我是函数组建', MineFun);
console.log('这是Element:', <MineFun/>);
并且他们都是Funcition的实例,
console.log(MineFun instanceof Function);
console.log(MineClass instanceof Function);
所以我们无法通过类型区分这两者区别,react则在类组件的原型上提供了isReactComponent来判断是否是类组件。
console.log((MineFun.prototype as any).isReactComponent);
console.log((MineClass.prototype as any).isReactComponent);
- JSX与Fiber节点
jsx是一种描述组件的数据结构,他不包含Scedule Reconclier Renderer所需的信息,如sibling,alternate指针,tag,等等。这些信息都包含在fiber节点。
mount时,Reconclier通过jsx描述的组件内容生成对应的fiber节点。
update时,Reconclier通过jsx与alternate指针指向的上一个fiber节点,生成对应的fiber节点。并根据更新操作对fiber打上标记。
Reconclier阶段
Reconclier阶段是创建fiber节点并将fiber节点连接成fiber树的阶段。
这个阶段开始于
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
与之前实现react多了一步,就是判断是否是同步的调用。shuoldYield是判断当前浏览器是否有多余时间可以执行。
- workLoopConcurrent会被一直调用,直到任务都运行完毕。
- workInProgress代表当前已创建的workInProgress fiber。
- 执行performUnitOfWork会处理当前的节点,并且返回下一个fiber节点赋值给wrokInProgress,再将该节点与上一个创建完的节点连接起来形成fiber树。
- fiberReconclier主要完成可中断的递归操作。所以Reconclier的主要操作分为两部,递跟归。
- performUnitOfWork=>beginwork=>completework=>commit(Renderer阶段)
递阶段就是beginwork
- beginwork会对传入的fiber节点进行处理,创建子fiebr。
- 从双缓存机制看,第一次mount的时候,beginwork会根据fiber.tag类型创建不同的节点。update的时候会尽可能的复用alternate指向的在current fiber的当前节点。
- 然后,对于函数组件类组件等等,都会进入reconclierChildren(核心)方法去处理。这个方法对于mount的组件会创建子fiber节点,调用mountChildFibers方法,对于update的组件,会进行diff算法生成新的
- 这两个方法的区别就是reconclierChildFibers会为生成的fiber节点打上effectTag标记,用来标记该节点将执行的操作。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
它是通过current来判断是否是mount,因为如果mount过,current!==null
无论走哪个逻辑,他都会生成一个fiber节点并且赋值给wrokInProgress.child,作为beginwork的返回值,并且作为下次执行performUnitOfWork的传参。
- effecttag
render阶段是在内存中运行的,当完成后就会交给Renderer去执行dom操作,也就是调用commit方法,而具体的操作类型就报存在effecttag中。 - 通知renderer将fiber渲染上dom需要满足两个条件,一个是fiber的stateNode存在,即当前节点的dom要创建完毕,第二则是fiber节点的effecttag是Placement effectTag。
- 解决这两个问题在于completework这个方法完成,也就是归的流程了。
归阶段就是completework
- 类似于beginwork,completework也是针对不同的fiber.tag进行不同的处理。
- 这里主要看hostComponent也就是对原声dom处理的方法。对于hostComponent,同样需要判断是mount还是updated。首先看updated方法:
if (current !== null && workInProgress.stateNode != null) { // update的情况 // ...省略 }
- 当update的时候,fiber节点已经有对应的stateNode,所以只需要处理props就行,包括回调函数的注册,处理style,children props等等,主要就是调用了updareHostComponent方法对porps进行处理
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
- mount的时候
这里的逻辑主要包括三点
1 创建dom赋给stateNode
2 将子孙dom插入到刚创建完的dom,解决上面说的第一个stateNode必须存在的问题。
3 与update逻辑中的updateHostComponent类似的处理props的过程
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
因为completework是归阶段执行的函数,所以最先执行的是第一个子节点都完成的节点,因为appendAllChildren方法会将子dom挂载到当前创建的dom上,到最后执行rootfiber的时候,已经有一颗没有挂载的dom树完成了,最后只需挂载一次到真实dom即可。
- 至此,render阶段的工作快要完成了,我们在fiber树的每一个fiber节点打上了effecttage,然后交给commit阶段进行渲染,但是在commit的时候需要继续重新遍历fiber树吗,效率明显是低下的,所以在completework函数的上层函数,也就是completeUnitOfWork函数
completeUnitOfWork主要是用来建立起一个单链表,使利用每个fIber的firstEffect,LastEffect以及NextEffect来建立一个单链表。
在归阶段,所有完成的节点都会执行compleUnitOfWork函数, - 该函数第一步骤就是将继续扩展单链表,将父亲的firsEffect指向currentFiber的firstEffect,将lastEffect指向currentFiber的lastEffect,中级还做了一些指针处理的操作。
- 第二阶段则是判断当前currentFiber的effecttag是否存在,不存在则不需要做任何处理,存在的话,再将自己也连接到单链表上去。
最后会形成一个以rootFiber.firstEffect为起点的单链表,在commit阶段只需要遍历该链表即可。
Renderer阶段
Render阶段的工作被称为commit阶段,其作用就是将Reconclier给予的需要变化的fiber(有effecttag)进行渲染更新。
一共有三个步骤:
- before mutation阶段(执行DOM操作前)
- mutation (执行dom)
- layout阶段(执行dom之后)
before mutation
- before mutation阶段的代码很短,整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。
- 主要关注commitBeforeMutation的逻辑
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...focus blur相关
}
const effectTag = nextEffect.effectTag;
// 调用getSnapshotBeforeUpdate
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
主要做了三个事情,
- 处理dom渲染删除前的blur focus
- 调用getSnapshotBeforeUpdate,commitBeforeMutationEffectOnFibe这个方法会调用getSnapshotBeforeUpdate.
- 调度useEffect。
- 从react16开始,很多生命周期如componentWillXX都加上了UNSAFE_,标志不安全,因为现在render阶段是可以中断的,对应的组件的像componentWillXX是在render阶段调用的,所以可能会触发多次。而getSnapshotBeforeUpdate是在commit阶段调用的,commit阶段是同步的,故不存在该问题。
- 调度useEffect
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
flushPassiveEffects就是触发useEffect的方法,他是作为回调函数被异步调度的。
completework的时候我们知道,fiber的创建和更新都会带上effecttag标记,而含有useEffecr或者useLayoutEffect的函数组件也会对其fiber打上effecttag标记。
- useEffect异步调用分为三步:
- before mutation阶段在scheduleCallback中调度flushPassiveEffects,即注册回调函数。
- layout阶段之后将effectList赋值给rootWithPendingPassiveEffects,layoit后将需要执行的effectList赋值。
- scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects。回调时机到,遍历effectList执行调用。 调用是在layout之后,也就是dom挂载后。
- 为什么需要异步调用呢?
就是为了防止同步执行时阻塞浏览器渲染。 - 所以before mutation阶段会遍历effectlists,然后依次执行focus,blur。调用getSnapshotBeforeUpdate,调度useEffect。
mutation阶段
类似before mutation阶段,mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根据 ContentReset effectTag重置文字节点
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 并 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitMutationEffects会遍历effectlists,做三个操作:
根据ContentReset effectTag重置文字节点
重置ref
根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)
Placement
当effecttag为Placement,表示fiberj节点需要插入到dom当中,调用commitPlacement方法,该方法的工作主要分为三步:
- 获取父级DOM节点。其中finishedWork为传入的Fiber节点。
const parentFiber = getHostParentFiber(finishedWork);
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;
- 获取Fiber节点的DOM兄弟节点
const before = getHostSibling(finishedWork);
- 根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。
值得注意的是,getHostSibling这个方法比较耗时,同一个父节点下依次执行多个操作,时间复杂度是指数级。因为fiber节点跟dom节点并不是一一对应的,从fiber节点去找dom节点可能需要跨层级。
如:
function App() {
return (
<div>
<p></p>
<Item/>
</div>
)
}
function Item() {
return <li><li>;
}
fiber树是
child child child
rootFiber -----> App -----> div -----> p
| sibling child
| -------> Item -----> li
dom树是
// DOM树
#root ---> div ---> p
|
---> li
p 的兄弟dom是li,在fiber上确实p的兄弟fiber Item的子fiber li。
Update effect
fiber节点需要更新,只需要关注函数组件和类组件即可。
- 当fiber.tag为FunctionComponent,会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数!!。
- 当fiber.tag为HostComponent,会调用commitUpdate,最终会在updateDOMProperties 中将render阶段 completeWork 中为Fiber节点赋值的updateQueue对应的内容渲染在页面上
函数组件update时遍历执行effectlist的useLayoutEffect的销毁函数。类组件会将updateQueue对应的内容进行渲染。
Deletion effect
当fiber阶段的effecttag为deletion effect时,表示将该节点从对应的dom上卸载。执行的方法commitDeletion。
该方法会执行三个操作:
- 递归调用Fiber节点及其子孙Fiber节点中fiber.tag为ClassComponent的componentWillUnmount (opens new window)生命周期钩子,从页面移除Fiber节点对应DOM节点
- 解绑ref
- 调度useEffect的销毁函数
layout
layout阶段是在dom渲染完毕后执行的,该阶段调用的hook以及生命周期可以直接访问到dom。
与前两个阶段相同,layout也是遍历effectlists调用对应的函数。具体执行commitLayoutEffects,`function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 调用生命周期钩子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 赋值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
一共做了两件事,
- 调用commitLayoutEffectOnFIber方法,调用生命周期钩子和hook
- 调用commitAttachref(nextEffect)方法去赋值ref
先看commitLayoutEffectOnFiber,
- 对于类组件,会根据current !== null判断调用componentDidMount还是componentDidUpdate生命周期。如果this.setState({},()=>{})第二个参数有回调函数,也会在这个阶段调用。
- 对于函数组件,会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数
switch (finishedWork.tag) {
// 以下都是FunctionComponent及相关类型
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 调度useEffect的销毁函数与回调函数
schedulePassiveEffects(finishedWork);
return;
}
结合mutation阶段,执行effect tag为deletion tag的fiber节点,会调用useLayoutEffect的销毁函数,到现在调用useLayoutEffect的回调函数,是同步执行的。而useEffect则需要在before mutation阶段先调度,在layout阶段完成后再异步执行,调用before mutation注册的回调函数,。
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
而这也是useLayoutEffect与useEffect的区别,对于rootFiber,即如果ReactDOM.render有第三个参数,也会在这里执行。
ReactDOM.render(<App />, document.querySelector("#root"), function() {
console.log("i am mount~");
});
- commitAttachRef就是对ref的赋值操作。
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
// 获取DOM实例
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (typeof ref === "function") {
// 如果ref是函数形式,调用回调函数
ref(instanceToUse);
} else {
// 如果ref是ref实例形式,赋值ref.current
ref.current = instanceToUse;
}
}
}
获取dom实例,赋值给ref。
至此,整个layout阶段就结束了,而currentFiber树和workInProgress树的切换是在什么时候呢?
root.current = finishedWork;
也就是这行代码在什么时候执行,他用来切换fiber树。答案是在mutation阶段之后,layout阶段之前
- 我们知道,layout阶段会执行componentDidUpdate,componentDidMount生命周期,比如componentDidUpdate执行的时候,他对应的状态应该是最新的了,也就是在layout阶段currentFiber树就必须是新创建的workInProgress fiber树。
- 而mutation阶段执行effecttag为deletion tag的时候会调用componentWillUnmount,这时候该生命周期获取的状态依旧是老的state,也就是这时候currentFIber树还没更新。
综上所述,fiber树的切换就是在mutation阶段之后,layout阶段之前。
总结
架构篇学习了fiber节点被构建为fiber树的经过
- 从Schedule->Reconclier阶段,也就是render阶段,因为是可中断的操作,所以分为了递操作跟归操作。递操作主要调用了beforeWork函数,创建fiber节点,建立与子fiber的联系。归操作主要调用了completeWork函数,首先完成的节点会执行completeWork函数,该函数会对应创建dom,赋值给fiber节点的stateNode,并且将子孙节点的dom也插入到自己身上。再者会根据firstEffect和nextEffect和lasteEffect,执行completeUnifOfWork函数,形成一个由rootFiber.firstEffect为头的,所有带有effecttag的fiber节点为链表内容的单链表,effectlists,交给commit阶段去执行。
- commit阶段对应Renderer阶段,该阶段不可中断,是将fiber树渲染到ui的阶段,分为三个before moutation, mutation, layout,分别对应dom树挂载前,挂载时,挂载后。都会遍历effectlist执行对应的操作。
- before mutation阶段
主要是处理DOM节点渲染/删除后的 autoFocus、blur逻辑
调用getSnapshotBeforeUpdate生命周期钩子
调度useEffect(只是调度,在layout执行,通过回调函数) - mutation阶段
根据不同的effecttag执行不同的操作,主要是placement effect,update effect,deletion effect - update effect会执行useLayoutEffect的销毁函数,将render阶段completework赋值的updateQueue渲染到页面。
- deletion effect会调用componentWillUnmount函数,解绑ref,以及调用useEffect的销毁函数,
- layoutj阶段是在dom渲染完成后执行的,主要的操作是调用生命周期和hooks,比如执行componentDidMount或者componentDidUpdate函数,调度useEffect的执行或者销毁函数,调用useLayoutEffect执行函数。useEffect函数是在beforemutatiion先调度,在layout完成异步执行,useLayoutEffec是在mutation销毁,在layout执行,整个过程是同步的,这也是跟useEffect的区别。
- 再者就是对ref的赋值操作。
- fiber树的切换是在mutation阶段之后,layout阶段之前,因为两个阶段执行的一些hooks和生命周期所对应的状态不同,比如mutation的componentWillUnmount,和layout阶段的componentDidUpdate。
学习文章的地址
https://react.iamkasong.com/renderer/layout.html#commitlayouteffects