当前位置:   article > 正文

React源码解析————Render阶段(一)_react render阶段

react render阶段

2021SC@SDUSC

2021SC@SDUSC

Fiber

在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。为此React 16之后就有了scheduler进行时间片的调度,给每个task一定的时间,如果在这个时间内没执行完,也要交出执行权给浏览器进行绘制和重排,所以异步可中断的更新需要一定的数据结构在内存中来保存dom的信息,这个数据结构就是Fiber(虚拟dom)。

Fiber数据结构

Fiber自带属性如下(已添加注解):

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  //保存节点的信息
  this.tag = tag;//对应组件的类型
  this.key = key;//key属性
  this.elementType = null;//元素类型
  this.type = null;//func或者class
  this.stateNode = null;//真实dom节点

  //连接成fiber树
  this.return = null;//指向父节点
  this.child = null;//指向child
  this.sibling = null;//指向兄弟节点
  this.index = 0;

  this.ref = null;

  //用来计算state
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;
    
	//effect相关
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;

  //优先级相关的属性
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  //current和workInProgress的指针
  this.alternate = null;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

Fiber双缓存

Fiber可以保存真实的dom,真实dom对应在内存中的Fiber节点会形成Fiber树,在第一次渲染之后,React 最终得到一个 Fiber 树,它反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current Fiber(当前树) 。当 React 开始处理更新时,它会构建一个所谓的 workInProgress Fiber(工作过程树) ,它反映了要刷新到屏幕的未来状态。这两棵树会通过alternate相互连接,因为每个 Fiber上都有个alternate属性,也指向一个 Fiber,创建 WorkInProgress 节点时优先取alternate,没有的话就创建一个。
举个简单的例子吧(例子来源于网络,如果作者读到请联系我,我会加上出处):

function App() {
  return (
    <div>
      xiao
      <p>chen</p>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById("root"));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

生成rootFiber
在这里插入图片描述

1.在mount时:会创建fiberRoot和rootFiber,然后根据jsx对象创建Fiber节点,节点连接成current Fiber树。
在这里插入图片描述

2.在update时:会根据新的状态形成的jsx(ClassComponent的render或者FuncComponent的返回值)和current Fiber对比形成一颗叫workInProgress的Fiber树(diff算法),然后将fiberRoot的current指向workInProgress树,此时workInProgress就变成了current Fiber。
注:
1.fiberRoot:指整个应用的根节点,只存在一个
2.rootFiber:ReactDOM.render或者ReactDOM.unstable_createRoot创建出来的应用的节点,可以存在多个。
我们现在知道了存在current Fiber和workInProgress Fiber两颗Fiber树,Fiber双缓存指的就是,在经过reconcile(diff)形成了新的workInProgress Fiber然后将workInProgress Fiber切换成current Fiber应用到真实dom中,存在双Fiber的好处是在内存中形成视图的描述,在最后应用到dom中,减少了对dom的操作。通俗来说:你可以将 workInProgress 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉。

补充内容:

对于JSX:JSX是JavaScript XML,是React提供的语法糖。 通过它我们就可以很方便的在js代码中书写html片段。
作用:React中用JSX来创建虚拟DOM(元素)。注意:JSX不是字符串,也不是HTML/XML标签,本质上还是javascript;
因此它不包含schedule,reconcile,render所需的相关信息,而组件在更新中的优先级,state,被打上的用于render的标记,这些内容都包含在fiber节点中。所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。

render流程预览

render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。
performSyncWorkOnRoot:

// This is the entry point for synchronous tasks that don't go
// through Scheduler
function performSyncWorkOnRoot(root) {
  invariant(
    (executionContext & (RenderContext | CommitContext)) === NoContext,
    'Should not already be working.',
  );

  flushPassiveEffects();

  let lanes;
  let exitStatus;
  if (
    root === workInProgressRoot &&
    includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)
  ) {
    // There's a partial tree, and at least one of its lanes has expired. Finish
    // rendering it before rendering the rest of the expired work.
    lanes = workInProgressRootRenderLanes;
    exitStatus = renderRootSync(root, lanes);
    if (
      includesSomeLane(
        workInProgressRootIncludedLanes,
        workInProgressRootUpdatedLanes,
      )
    ) {
      // The render included lanes that were updated during the render phase.
      // For example, when unhiding a hidden tree, we include all the lanes
      // that were previously skipped when the tree was hidden. That set of
      // lanes is a superset of the lanes we started rendering with.
      //
      // Note that this only happens when part of the tree is rendered
      // concurrently. If the whole tree is rendered synchronously, then there
      // are no interleaved events.
      lanes = getNextLanes(root, lanes);
      exitStatus = renderRootSync(root, lanes);
    }
  } else {
    lanes = getNextLanes(root, NoLanes);
    exitStatus = renderRootSync(root, lanes);
  }

  if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
    executionContext |= RetryAfterError;

    // If an error occurred during hydration,
    // discard server response and fall back to client side render.
    if (root.hydrate) {
      root.hydrate = false;
      clearContainer(root.containerInfo);
    }

    // If something threw an error, try rendering one more time. We'll render
    // synchronously to block concurrent data mutations, and we'll includes
    // all pending updates are included. If it still fails after the second
    // attempt, we'll give up and commit the resulting tree.
    lanes = getLanesToRetrySynchronouslyOnError(root);
    if (lanes !== NoLanes) {
      exitStatus = renderRootSync(root, lanes);
    }
  }

  if (exitStatus === RootFatalErrored) {
    const fatalError = workInProgressRootFatalError;
    prepareFreshStack(root, NoLanes);
    markRootSuspended(root, lanes);
    ensureRootIsScheduled(root, now());
    throw fatalError;
  }

  // We now have a consistent tree. Because this is a sync render, we
  // will commit it even if something suspended.
  const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  commitRoot(root);

  // Before exiting, make sure there's a callback scheduled for the next
  // pending level.
  ensureRootIsScheduled(root, now());

  return null;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

performConcurrentWorkOnRoot

// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
  // Since we know we're in a React event, we can clear the current
  // event time. The next update will compute a new event time.
  currentEventTime = NoTimestamp;
  currentEventWipLanes = NoLanes;
  currentEventPendingLanes = NoLanes;

  invariant(
    (executionContext & (RenderContext | CommitContext)) === NoContext,
    'Should not already be working.',
  );

  // Flush any pending passive effects before deciding which lanes to work on,
  // in case they schedule additional work.
  flushPassiveEffects();

  // Determine the next expiration time to work on, using the fields stored
  // on the root.
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  if (lanes === NoLanes) {
    return null;
  }

  // TODO: We only check `didTimeout` defensively, to account for a Scheduler
  // bug where `shouldYield` sometimes returns `true` even if `didTimeout` is
  // true, which leads to an infinite loop. Once the bug in Scheduler is
  // fixed, we can remove this, since we track expiration ourselves.
  if (didTimeout) {
    // Something expired. Flush synchronously until there's no expired
    // work left.
    markRootExpired(root, lanes);
    // This will schedule a synchronous callback.
    ensureRootIsScheduled(root, now());
    return null;
  }

  const originalCallbackNode = root.callbackNode;

  let exitStatus = renderRootConcurrent(root, lanes);

  if (
    includesSomeLane(
      workInProgressRootIncludedLanes,
      workInProgressRootUpdatedLanes,
    )
  ) {
    // The render included lanes that were updated during the render phase.
    // For example, when unhiding a hidden tree, we include all the lanes
    // that were previously skipped when the tree was hidden. That set of
    // lanes is a superset of the lanes we started rendering with.
    //
    // So we'll throw out the current work and restart.
    prepareFreshStack(root, NoLanes);
  } else if (exitStatus !== RootIncomplete) {
    if (exitStatus === RootErrored) {
      executionContext |= RetryAfterError;

      // If an error occurred during hydration,
      // discard server response and fall back to client side render.
      if (root.hydrate) {
        root.hydrate = false;
        clearContainer(root.containerInfo);
      }

      // If something threw an error, try rendering one more time. We'll render
      // synchronously to block concurrent data mutations, and we'll includes
      // all pending updates are included. If it still fails after the second
      // attempt, we'll give up and commit the resulting tree.
      lanes = getLanesToRetrySynchronouslyOnError(root);
      if (lanes !== NoLanes) {
        exitStatus = renderRootSync(root, lanes);
      }
    }

    if (exitStatus === RootFatalErrored) {
      const fatalError = workInProgressRootFatalError;
      prepareFreshStack(root, NoLanes);
      markRootSuspended(root, lanes);
      ensureRootIsScheduled(root, now());
      throw fatalError;
    }

    // We now have a consistent tree. The next step is either to commit it,
    // or, if something suspended, wait to commit it after a timeout.
    const finishedWork: Fiber = (root.current.alternate: any);
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    finishConcurrentRender(root, finishedWork, exitStatus, lanes);
  }

  ensureRootIsScheduled(root, now());
  if (root.callbackNode === originalCallbackNode) {
    // The task node scheduled for this root is the same one that's
    // currently executed. Need to return a continuation.
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104

我们现在暂时还不需要理解这两个方法,只需要知道在这两个方法中会调用如下两个方法:

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
这里的参数workInProgress代表当前已创建的workInProgress fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树
我们知道Fiber Reconciler是从Stack Reconciler重构而来(React 15及以前的 Reconciler 在React 16之后被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑, Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行),通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。
(一)“递”阶段
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法。
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
(二)“归”阶段
在“归”阶段会调用completeWork处理Fiber节点。
当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。
如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。

关于beginwork 和 completeWork的讲解我们放到下一篇去讲解

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/383439
推荐阅读
相关标签
  

闽ICP备14008679号