react调和过程和react的更新机制

Reconciliation(diff)算法

调和的目的

在用户无感知的情况下将数据的更新体现到UI上。

触发调和过程的方式
  1. ReactDom.render()函数 和ReactNativeRenderer.render()函数

  2. setState()、forceUpdate()、componentWillMount 、componentWillReceiveProp 中直接修改了state

  3. hooks 中的useReducer 和 useState 返回的钩子函数

调和的工作原理
render 阶段

构建 Fiber 对象,构建链表,在链表中标记要执行的 DOM 操作 ,可中断。

 const fiber = {
stateNode,// dom节点实例
child,// 当前节点所关联的子节点
sibling,// 当前节点所关联的兄弟节点
return// 当前节点所关联的父节点
}
  1. 支持暂停,终止以及恢复之前的渲染任务

  2. 支持增量渲染,fiberreact中的渲染任务拆分到每一帧

  3. 通过fiber赋予了不同任务的优先级

    当数据发生改变引起页面组件进行更新,进入diff算法:

// diff对比过程其实也是在通过链表进行递归
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  // 只要还有节点单元,一直进行对比
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
​
​
function performUnitOfWork(unitOfWork) {
  // 获取当前fiber节点
  var current = unitOfWork.alternate;
  setCurrentFiber(unitOfWork);
  // 创建next节点,等会会设置next为下一个要对比的fiber节点
  var next;
​
  if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
    // 设置fiber节点的开始时间
    startProfilerTimer(unitOfWork);
    // 获取当前fiber节点的child,将其设置为next
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
  }
​
  resetCurrentFiber();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
​
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    // 将next赋予给workInProgress,于是while循环会持续进行
    workInProgress = next;
  }
}
​
//在workLoopSync方法中可以看到while (workInProgress !== null)的判断,只要fiber节点不为空,就一直递归调用performUnitOfWork方法。
​
//而在performUnitOfWork中可以看到前文我们说的链表的概念,react通过next = 当前节点child的操作,只要子节点仍存在,就不断更新next并赋予给workInProgress,所以也验证了前文所说,即便任务被暂停,react也能通过next继续先前的工作。

Reconciliation协调阶段

// current -- 旧有的fiber节点信息
// workInProgress -- 也是旧有的fiber节点信息,结构与current有少许不同
// nextChildren -- 之前调用Component(props, secondArg)得到的虚拟dom子节点
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  // 通过current我们能知道此时是初次渲染,还是更新
  if (current === null) {
    // 挂载fiber节点
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    // diff fiber节点
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}
​
// reconcileChildFibers方法会判断新的节点是什么类型,比如当前我们传递的是虚拟dom div,它是个对象,所以会继续调用placeSingleChild方法,根据递归的特性,等会还会对比div的props
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
  // 判断传递的新虚拟dom是不是对象
  var isObject = typeof newChild === 'object' && newChild !== null;
  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 是对象,且是虚拟dom类型,继续调用
        return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
    }
  }
    // 删除部分无用代码
  if (isArray$1(newChild)) { // 若有子节点
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
  }
}
​
// 参数与上个方法的参数注解相同,按值传递
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
  // 获取新虚拟dom的key
  var key = element.key;
  // 旧有的div fiber节点
  var child = currentFirstChild;
    // 判断旧有fiber存不存在,一定是存在才能diff,否则就是走fiber创建初始化了
  while (child !== null) {
    // TODO: If key === null and child.key === null, then this only applies to
    // the first item in the list.
    if (child.key === key) {
      switch (child.tag) {
                // 删除部分无用逻辑
        default:
          {
            if (child.elementType === element.type || ( 
             isCompatibleFamilyForHotReloading(child, element) )) {
              deleteRemainingChildren(returnFiber, child.sibling);
                            // 根据新的虚拟dom的props来更新旧有div fiber节点
              var _existing3 = useFiber(child, element.props);
                            // 更新完成后重新设置ref以及父节点
              _existing3.ref = coerceRef(returnFiber, child, element);
              _existing3.return = returnFiber;
              return _existing3;
            }
            break;
          }
      }
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // 如果key不相等,直接在父节点上把自己整个都删掉
      deleteChild(returnFiber, child);
    }
    // 将兄弟节点赋予给child,继续while遍历
    child = child.sibling;
  }
    // 如果不存在旧的fiber节点,那说明是挂载,因此否则走fiber的初始化
  // 这里的初始化我删掉了
}
​
// 1判断是否存在旧有的fiber节点,如果不存在说明没必要diff,直接走fiber新建挂载逻辑。
// 2有child说明有旧有fiber,那就对比key,如果不相等,直接运行deleteChild(returnFiber, child),也就是从div节点的旧有父节点上,将整个div都删除掉,div的子节点都不需要比了,这也验证了react的逐级比较,父不同,子一律都不比较视为不同。
// 3若key相同,那就比较新旧fiber的type(标签类型),如果type不相同,跟key不相同一样,调用了deleteRemainingChildren(returnFiber, child)方法,直接从div的旧有父节点上将自己整个删除。
// 4若key type都相同,那只能说明是props变了,因此调用var _existing3 = useFiber(child, element.props)方法,根据新的props来更新旧有的div fiber节点。
​
function useFiber(fiber, pendingProps) {
    // 使用旧有的fiber节点以及新的props来创建一个新的clone fiber
  var clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}
​
​

commit 阶段

根据构建好的链表进行 DOM 操作,不可中断。

function performSyncWorkOnRoot(root){
  // 删除意义不大的代码
  var finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // 提交root节点
  commitRoot(root); // Before exiting, make sure there's a callback scheduled for the next
  // pending level.
  ensureRootIsScheduled(root, now());
  return null;
}
​
function commitRoot(root) { // 这里会对当前任务进行优先级判断,再决定后续处理
  // 断点发现这里的优先级是99,最高优先级
  var renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority$1(ImmediatePriority$1, commitRootImpl.bind(null, root, renderPriorityLevel));
  return null;
}
​
​

fiber是一种数据结构,使用父子关系以及next的妙用,以链表形式模拟了传统调用栈。 fiber是一种调度让出机制,只在有剩余时间的情况下运行。 fiber实现了增量渲染,在浏览器允许的情况下一点点拼凑出最终渲染效果。 fiber实现了并发,为任务赋予不同优先级,保证了一有时间总是做最高优先级的事,而不是先来先占位死板的去执行。 fiber有协调与提交两个阶段,协调包含了fiber创建与diff更新,此过程可暂停。而提交必须同步执行,保证渲染不卡顿。

而通过fiber的协调阶段,我们了解了diff的对比过程,如果将fiber的结构理解成一棵树,那么这个过程本质上还是深度遍历,其顺序为父—父的第一个孩子—孩子的每一个兄弟。

通过源码,我们了解到react的diff是同层比较,最先比较key,如果key不相同,那么不用比较剩余节点直接删除,这也强调了key的重要性,其次会比较元素的type以及props。而且这个比较过程其实是拿旧的fiber与新的虚拟dom在比,而不是fiber与fiber或者虚拟dom与虚拟dom比较,其实也不难理解,如果key与type都相同,那说明这个fiber只用做简单的替换,而不是完整重新创建,站在性能角度这确实更有优势

React中的4种优先级

UI产生交互的根本原因是各种事件,这也就意味着事件与更新有着直接关系。不同事件产生的更新,它们的优先级是有差异的,所以更新优先级的根源在于事件的优先级。一个更新的产生可直接导致react生成一个更新任务,最终这个任务被Scheduler调度。

  • 事件优先级:按照用户事件的交互紧急程度,划分的优先级

  • 更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)

  • 任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级

  • 调度优先级:Scheduler依据React更新任务生成一个调度任务,这个调度任务所持有的优先级

前三者属于React的优先级机制,第四个属于Scheduler的优先级机制,Scheduler内部有自己的优先级机制

事件优先级

React按照事件的紧急程度,把它们划分成三个等级:

  • 离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。

  • 用户阻塞事件(UserBlockingEvent):drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。

  • 连续事件(ContinuousEvent):canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。

事件优先级是在注册阶段被确定的,在向root上注册事件时,会根据事件的类别,创建不同优先级的事件监听(listener----dispatchDiscreteEvent、dispatchUserBlockingUpdate、dispatchEvent这三个中的一个),最终将它绑定到root上去。以某种优先级去执行事件处理函数其实要借助Scheduler中提供的runWithPriority函数来实现

​
function dispatchUserBlockingUpdate(  domEventName,  eventSystemFlags,  container,  nativeEvent,) {
  ...
  runWithPriority(    UserBlockingPriority,    dispatchEvent.bind(      null,      domEventName,      eventSystemFlags,      container,      nativeEvent,    ),  );
  ...
}

这么做可以将事件优先级记录到Scheduler中,相当于告诉Scheduler:你帮我记录一下当前事件派发的优先级,等React那边创建更新对象(即update)计算更新优先级时直接从你这拿就好了。

​
function unstable_runWithPriority(priorityLevel, eventHandler) {  switch (priorityLevel) {    case ImmediatePriority:    case UserBlockingPriority:    case NormalPriority:    case LowPriority:    case IdlePriority:      break;    default:      priorityLevel = NormalPriority;  }
  var previousPriorityLevel = currentPriorityLevel;  // 记录优先级到Scheduler内部的变量里  currentPriorityLevel = priorityLevel;
  try {    return eventHandler();  } finally {    currentPriorityLevel = previousPriorityLevel;  }}

更新优先级

以setState为例,事件的执行会导致setState执行,而setState本质上是调用enqueueSetState,生成一个update对象,这时候会计算它的更新优先级,即update.lane:它首先找出Scheduler中记录的优先级:schedulerPriority,然后计算更新优先级:lane

const classComponentUpdater = {  enqueueSetState(inst, payload, callback) {    ...
    // 依据事件优先级创建update的优先级    const lane = requestUpdateLane(fiber, suspenseConfig);
    const update = createUpdate(eventTime, lane, suspenseConfig);    update.payload = payload;    enqueueUpdate(fiber, update);
    // 开始调度   
                                                                           scheduleUpdateOnFiber(fiber, lane, eventTime);    ...  
      }
   };
​
export function requestUpdateLane(
  fiber: Fiber,
  suspenseConfig: SuspenseConfig | null,
): Lane {
 
 
  ...
  // 根据记录下的事件优先级,获取任务调度优先级
  const schedulerPriority = getCurrentPriorityLevel();
 
 
  let lane;
  if (
    (executionContext & DiscreteEventContext) !== NoContext &&
    schedulerPriority === UserBlockingSchedulerPriority
  ) {
    // 如果事件优先级是用户阻塞级别,则直接用InputDiscreteLanePriority去计算更新优先级
    lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
  } else {
    // 依据事件的优先级去计算schedulerLanePriority
    const schedulerLanePriority = schedulerPriorityToLanePriority(
      schedulerPriority,
    );
    ...
    // 根据事件优先级计算得来的schedulerLanePriority,去计算更新优先级
    lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  }
  return lane;
}
getCurrentPriorityLevel负责读取记录在Scheduler中的优先级:
function unstable_getCurrentPriorityLevel() {
  return currentPriorityLevel;
​

update对象创建完成后意味着需要对页面进行更新,会调用scheduleUpdateOnFiber进入调度,而真正开始调度之前会计算本次产生的更新任务的任务优先级,目的是与已有任务的任务优先级去做比较,便于做出多任务的调度决策

任务优先级

一个update会被一个React的更新任务执行掉,任务优先级被用来区分多个更新任务的紧急程度,它由更新优先级计算而来,举例来说:

假设产生一前一后两个update,它们持有各自的更新优先级,也会被各自的更新任务执行。经过优先级计算,如果后者的任务优先级高于前者的任务优先级,那么会让Scheduler取消前者的任务调度;如果后者的任务优先级等于前者的任务优先级,后者不会导致前者被取消,而是会复用前者的更新任务,将两个同等优先级的更新收敛到一次任务中;如果后者的任务优先级低于前者的任务优先级,同样不会导致前者的任务被取消,而是在前者更新完成后,再次用Scheduler对后者发起一次任务调度。

这是任务优先级存在的意义,保证高优先级任务及时响应,收敛同等优先级的任务调度

调度优先级

调度优先级由任务优先级计算得出,一旦任务被调度,那么它就会进入Scheduler,在Scheduler中,这个任务会被包装一下,生成一个属于Scheduler自己的task,这个task持有的优先级就是调度优先级。

在Scheduler中,分别用过期任务队列和未过期任务的队列去管理它内部的task,过期任务的队列中的task根据过期时间去排序,最早过期的排在前面,便于被最先处理。而过期时间是由调度优先级计算的出的,不同的调度优先级对应的过期时间不同。

事件优先级由事件本身决定,更新优先级由事件计算得出,然后放到root.pendingLanes,任务优先级来自root.pendingLanes中最紧急的那些lanes对应的优先级,调度优先级根据任务优先级获取。几种优先级环环相扣,保证了高优任务的优先执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值