react源码学习2(架构)

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码学习2(架构)相关的知识,希望对你有一定的参考价值。

深入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)进行渲染更新。
一共有三个步骤:

  1. before mutation阶段(执行DOM操作前)
  2. mutation (执行dom)
  3. 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;
  }
}

主要做了三个事情,

  1. 处理dom渲染删除前的blur focus
  2. 调用getSnapshotBeforeUpdate,commitBeforeMutationEffectOnFibe这个方法会调用getSnapshotBeforeUpdate.
  3. 调度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异步调用分为三步:
  1. before mutation阶段在scheduleCallback中调度flushPassiveEffects,即注册回调函数。
  2. layout阶段之后将effectList赋值给rootWithPendingPassiveEffects,layoit后将需要执行的effectList赋值。
  3. 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方法,该方法的工作主要分为三步:

  1. 获取父级DOM节点。其中finishedWork为传入的Fiber节点。
const parentFiber = getHostParentFiber(finishedWork);
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;
  1. 获取Fiber节点的DOM兄弟节点
const before = getHostSibling(finishedWork);
  1. 根据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;

一共做了两件事,

  1. 调用commitLayoutEffectOnFIber方法,调用生命周期钩子和hook
  2. 调用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

以上是关于react源码学习2(架构)的主要内容,如果未能解决你的问题,请参考以下文章

react源码学习-架构篇-render阶段

自顶而下学习react源码 架构篇 render阶段

react源码学习-架构篇-commit阶段

react源码解析18事件系统

react源码解析19.手写迷你版react

自顶而下学习react源码 架构篇 commit阶段