React源码系列之九:React的更新机制

906 阅读7分钟

前言

本次React源码参考版本为17.0.3。这是React源码系列第九篇,建议初看源码的同学从第一篇开始看起,这样更有连贯性,下面有源码系列链接。

终于写到这里了,这算是17版本的终结篇了,后面一篇算是未来篇。

热身准备

系列文章前面讲了渲染,其实主要讲的是初始化,那在后续的交互中会触发视图的更新,React是怎么做的呢?这篇文章带你揭秘React的更新机制。

触发更新的几种场景

用过React的了解触发更新主要是组件的props或者state发生了变化,可能导致他们变化的有几种场景:

  • 初次渲染(比如在生命周期中setState);
  • 定时任务(定时任务中有setState);
  • 事件回调(回调中有setState);
  • context变化(Context.Providervalue);

我们日常开发中,触发React更新主要还是通过setState的形式,接下来,我们就从setState开始探索React的更新机制。

setState之后

在系列文章的React源码系列之三:hooks之useState,useReducer有讲过,当React执行setState实际上是触发了一个dispatchAction,基于setState的入参创建了一个update,进入更新调度scheduleUpdateOnFiber

scheduleUpdateOnFiber

在这里,会对当前任务队列中的任务基于当前setState触发时间做判断,如果任务时间比currentTime小,即是一个过期任务,需要在下次更新时立即执行。

markStarvedLanesAsExpired(root, currentTime);

获取当前setState任务的优先级,这一块代码判断就比较复杂了,笔者也没有深入研究

var nextLanes = getNextLanes(root, lanes);

var newCallbackPriority = returnNextLanesPriority();

在做完优先级判断后,根据优先级创建一个回调任务存储在root.callbackNode中,下面主要基于优先级区分同步任务,异步任务还是批量更新任务,以及执行的优先级。

if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else if (newCallbackPriority === SyncBatchedLanePriority) {
    newCallbackNode = scheduleCallback(ImmediatePriority$1, performSyncWorkOnRoot.bind(null, root));
  } else {
    var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
    newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
  }

scheduleSyncCallbackscheduleCallback中,最后都会调用unstable_scheduleCallback,在这个函数中,会基于优先级计算得出过期时间创建任务

switch (priorityLevel) {
    case ImmediatePriority:
      timeout = -1;
      break;

    case UserBlockingPriority:
      timeout = 250;
      break;

    case IdlePriority:
      timeout = 1073741830;
      break;

    case LowPriority:
      timeout = 10000;
      break;

    case NormalPriority:
    default:
      timeout = 5000;
      break;
}

var expirationTime = startTime + timeout;

过期时间久的说明任务优先级比较低,可以放在后面执行,优先级高的任务,过期时间会比当前时间小,这样直接就是过期任务,React会在下次执行任务时立即执行。

任务创建好了,那到底什么时候执行呢?下面这段代码给了我们答案,当任务创建完了,React会通过MessageChannel创建一个宏任务,并且立即port2.postMessage去告知port1执行performWorkUntilDeadlineperformWorkUntilDeadline会执行传入的回调函数。

我们暂时不考虑优先级和调度时机的问题,把重心放到它在回调中会执行的函数,我们可以注意到,在这三种任务中,最终传入回调的都是performSyncWorkOnRoot.bind(null, root),也就是说,不管什么时候执行,最终都是执行的performSyncWorkOnRoot,那我们就知道了下步更新的方向了:performSyncWorkOnRoot

  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  // 基于MessageChannel实现异步调度更新, 如果不支持MessageChannel会使用setTimeout
  requestHostCallback = function (callback) {
    scheduledHostCallback = callback;

    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };

performSyncWorkOnRoot

创建rootFiber

if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    // 初始化workInProgressRoot, workInProgress, mount创建, update拷贝
    prepareFreshStack(root, lanes);   
    startWorkOnPendingInteractions(root, lanes);
}

prepareFreshStack中,如果有currentfiber树,React会判断为更新阶段,它会从curret上拷贝一份作为workInprogressrootFiber

有了rootFiber,接下来就是递归遍历这个rootFiber的子节点,找出和currentfiber树的不同,然后更新,一样要走beginWorkcompleteWorkcommitWork这些渲染时的阶段。

beginWork

和初始化渲染阶段执行的是一个函数,但是会在一些逻辑判断中有所不同。

React判断是初始化渲染阶段还是更新阶段,一个重要的参考是current === null

React在渲染的beginWork阶段有下面这样一个判断

if (oldProps !== newProps || hasContextChanged() || (workInProgress.type !== current.type )) {
  didReceiveUpdate = true;
}

我们可以留意到workInProgress.type !== current.type这个判断条件,这就是在判断元素类型,更新前后的类型是否一致。如果不一致didReceiveUpdate = true;。再看下面代码当didReceiveUpdate = true;时会做什么

if (current !== null && !didReceiveUpdate) {
  bailoutHooks(current, workInProgress, renderLanes);
  // clone current fiber并返回
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);

这段代码告诉了我们答案, 当current !== null && !didReceiveUpdate时,会直接复制current的值并返回, 反之, 当didReceiveUpdate = true;时,不会进入这个判断,会走下面的重新

假设有个<App />的函数根组件,在beginWork阶段会执行这个函数组件,通过useState拿到新的setState的值来支持后面的更新。

函数组件执行完毕后,就会开始递归遍历函数组件returnReact元素,这里就涉及到我们上篇文章React源码系列之八:React的diff算法了,有兴趣的可以去看看,这里就略过了。

completeWork

completeWork阶段,更新阶段主要是对新老节点的props进行diff,在上篇文章也有介绍。在初始化渲染阶段主要执行appendAllChildren

如果在更新阶段有新加的React元素还是会执行appendAllChildren,将fiber节点上新增的子节点加入到dom中。

commitWork

commitWorkmutation阶段,也就是渲染DOM的时候,会将completeWorkdiff出来的改动更新的DOM中,此时就是真正的渲染页面了。

新增,删除DOM就不说了,都是按照正常的渲染流程。我们来看下DOM更新

function updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag) {
  
  for (var i = 0; i < updatePayload.length; i += 2) {
    var propKey = updatePayload[i];
    var propValue = updatePayload[i + 1];

    if (propKey === STYLE) {
      setValueForStyles(domElement, propValue);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

React根据completeWorkdiff出来props来更新DOM,区分props的类型为style, dangerouslySetInnerHTML, chidren和其他。

  • style:就是DOM的style
  • dangerouslySetInnerHTMLReact提供的api,用来写DOM节点的(富文本中有用到,慎用),从源码能看到其实使用的就是innerHtml
  • childrenchildren默认为文本,直接渲染;
  • 其他prop:主要通过setAttributeremoveAttribute进行处理;

总结

来总结下setState导致的更新:

  • 首先通过dispatchAction建一个update存储在对应的fiber节点中,然后开始调度更新;
  • 在调度更新时会判断当前应用中是否有更新任务,然后判断本次setState任务的优先级来决定更新时机;
  • setState任务开始执行时,会向初始化渲染一样经历beginWorkcompleteWorkcommitWork
  • beginWorkcompleteWork中,通过diff确定要更新的元素;
  • commitWork中将diff出来的更新应用到DOM中,渲染到页面;

至此,我们系列文章从Reactfiber,到初始化渲染,到hook,到合成事件,diff算法,更新机制都做了剖析,肯定有不全面和不对的地方,而且笔者主要分析的是函数组件和客户端渲染,对整个只能说对React的运行机制有了大概的理解,希望各位看官也能有所收获。

看完这篇文章, 我们可以弄明白下面这几个问题:

  1. React的更新机制是怎样的?

系列文章安排:

  1. React源码系列之一:Fiber
  2. React源码系列之二:React的渲染机制
  3. React源码系列之三:hooks之useState,useReducer
  4. React源码系列之四:hooks之useEffect
  5. React源码系列之五:hooks之useCallback,useMemo
  6. React源码系列之六:hooks之useContext
  7. React源码系列之七:React的合成事件
  8. React源码系列之八:React的diff算法
  9. React源码系列之九:React的更新机制;
  10. React源码系列之十:Concurrent Mode;

参考:

React官方文档

github