Skip to content

前言

在前面的文章中,我们分析初次渲染的具体流程

接下来我们将着重于分析各种触发渲染更新的操作、更新时的diff流程、更新时联动hooks刷新的逻辑

文章内容概述

  1. 分析useReducer的相关源码,了解任务的创建以及更新相关流程
  2. 分析useState的相关源码,了解任务的创建以及更新相关流程
  3. useState为基础,对整个更新流程进行简单的分析
  4. 更新流程中的diff算法进行简单描述,侧重于各种flags的标记以及对应的更新方法
  5. 分析其它常见的useXXX的相关源码

workInProgress全局变量的赋值情况??很多地方都有current以及workInProgress,它们的关系是怎么样的?

文章要解决的问题

  1. updatelanetask之间的关系,它们是如何配合进行调度更新的?

有多种更新?元素更新?function更新?还有state更新??


1. useReducer

javascript
const [reducerState, dispatch] = React.useReducer(reducer, {age: 42});

1.1 初始化mountReducer

FunctionComponent类型fiberbeginWork()中,我们会触发

  • mountIndeterminateComponent()
  • renderWithHooks()

renderWithHooks()我们会设置全局变量currentlyRenderingFiber$1为当前的fiber

javascript
function beginWork(current, workInProgress, renderLanes) {
  didReceiveUpdate = false;
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
    }
  }
}
function mountIndeterminateComponent(...) {
  value = renderWithHooks(...);
  //...
}
javascript
function renderWithHooks() {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber$1 = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  //   function App(_ref) {
  //     var _React$useReducer = React.useReducer(reducer, { age: 42 })
  //     return React.createElement(
  //       "div",
  //       ...
  //     );
  //   }
  var children = Component(props, secondArg); // workInProgress.type
  renderLanes = NoLanes;
  currentlyRenderingFiber$1 = null;
  return children;
}

然后触发Component(),也就是FunctionComponent()中实际的内容,全部执行一遍,然后return对应的React.createElement(...)作为fiber赋值给children

根据当前current去切换HooksDispatcherOnMount/HooksDispatcherOnUpdate,对应不同的方法,因此初始化React.useReducer=mountReducer,而更新时React.useReducer=updateReducer

当我们在代码中有React.useReducer()时,会触发mountReducer(),如下面代码所示,在我们示例中,传入的

  • initialArg{age: 42}
  • initundefined

因此我们会根据initialArg初始化对应的值,然后根据赋值hook相关属性,包括

  • memoizedState
  • baseState
  • queue:包括pendinglanesdispatchlastRenderedReducerlastRenderedState
javascript
function useReducer(reducer, initialArg, init) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useReducer(reducer, initialArg, init);
}
function mountReducer(reducer, initialArg, init) {
  var hook = mountWorkInProgressHook();
  var initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg;
  }
  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  var dispatch = (queue.dispatch = dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber$1,
    queue
  ));
  return [hook.memoizedState, dispatch];
}

1.1.1 fiber.memoizedState单链表结构存储hooks

从初始化mountWorkInProgressHook()方法可以知道,hook本身有一个memoizedState属性,fiber本身也有一个memoizedState属性,不同的是

  • hook.memoizedState存储的是state当前的值
  • fiber.memoizedState存储的是当前fiber(比如一个FunctionComponent类型的fiber)中的所有hook的第一个节点
javascript
function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) {
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

最终fiber.memoizedState=代码中第1个useReducer+代码中第2个useReducer+...

注:workInProgressHook表示当前正在初始化的hook,不是workInProgress!!是两个不同的变量

1.1.2 hook 的更新方法初始化

当前currentlyRenderingFiber$1FunctionComponet代表的fiberqueue代表的是当前fiber中其中一个hookqueue

javascript
var queue = {
  pending: null,
  lanes: NoLanes,
  dispatch: null,
  lastRenderedReducer: reducer,
  lastRenderedState: initialState,
};
hook.queue = queue;
var dispatch = (queue.dispatch = dispatchReducerAction.bind(
  null,
  currentlyRenderingFiber$1,
  queue
));

function dispatchReducerAction(fiber, queue, action) {
  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    var eventTime = requestEventTime();
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitionUpdate(root, queue, lane);
  }
}

1.2 dispatch创建更新

当我们在onClick()方法中触发dispatch()的时候,我们会进行reducer的调用,触发更新操作

我们触发了两次dispatch1,因此创建了两个update!!

javascript
function reducer(state, {type}) {
    if (type === "incremented_age") {
        return {
            age: state.age + 1
        };
    }
    throw Error('Unknown action.');
}

const reducerJsx = (
    <React.Fragment>
        <span>reducer现在是:{reducerState.age}</span>
        <div>
            <button onClick={() => {
                dispatch1({type: "incremented_second"});
                dispatch1({type: "incremented_second"});
            }}>
                reducerState点击增加1
            </button>
        </div>
    </React.Fragment>
);

hook的更新方法初始化可以知道,dispatch()实际上就是dispatchReducerAction(),因此涉及到两个问题

  • dispatchReducerAction()执行了什么?
  • 什么时候调用reducer()方法?以及发生了什么?

1.2.1 创建update并触发调度

从下面代码可以知道,主要分为几个步骤:

  • 创建update
  • update放入到队列中:enqueueConcurrentHookUpdate()
  • 处理队列中的updatescheduleUpdateOnFiber()
javascript
function dispatchReducerAction(fiber, queue, action) {
  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    var eventTime = requestEventTime();
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitionUpdate(root, queue, lane);
  }
}

1.2.1.1 传入值fiber、queue、action分析

fiber:当前FunctionComponet代表的fiber

queue:当前hook.queue

action:当前hook所产生更新传入的参数,比如外部调用setState({type: "a"}),那么action={type: a}

1.2.1.2 创建update

使用requestUpdateLane()获取当前fiberlane,然后构建update对象

1.2.1.3 将update放入队列中enqueueConcurrentHookUpdate()

将当前hook创建的update压入concurrentQueues队列中,然后返回HostRoot

这里的queue是上面初始化mountReducer()构建dispatch更新方法时创建的hook.queue,对于同一个hookdispatch更新方法多次调用,拿到的都是同一个fiberqueue,由于示例创建了两个update,这里压入了两次队列

注意:此时的 update 产生的 lane 已经合并到对应的 fiber 数据中

javascript
function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
    var concurrentQueue = queue;
    var concurrentUpdate = update;
    enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
    return getRootForUpdatedFiber(fiber);
}
function enqueueUpdate(fiber, queue, update, lane) {
  concurrentQueues[concurrentQueuesIndex++] = fiber;
  concurrentQueues[concurrentQueuesIndex++] = queue;
  concurrentQueues[concurrentQueuesIndex++] = update;
  concurrentQueues[concurrentQueuesIndex++] = lane;
  concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
  fiber.lanes = mergeLanes(fiber.lanes, lane);
  var alternate = fiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
}
function mergeLanes(a, b) {
  return a | b;
}
function getRootForUpdatedFiber(sourceFiber) {
  var node = sourceFiber;
  var parent = node.return;
  while (parent !== null) {
    node = parent;
    parent = node.return;
  }
  return node.tag === HostRoot ? node.stateNode : null;
}

1.2.1.4 开始调度scheduleUpdateOnFiber()

在我们首次渲染的文章中,我们已经详细分析了scheduleUpdateOnFiber()的流程,就是触发

javascript
function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
  //...
  markRootUpdated(root, lane, eventTime);
  ensureRootIsScheduled(root, eventTime);
}
function ensureRootIsScheduled(root, currentTime) {
  var existingCallbackNode = root.callbackNode;
  markStarvedLanesAsExpired(root, currentTime);
  var nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
  );
  if (nextLanes === NoLanes) {
    //...没有任务直接返回
    return;
  }
  // getHighestPriorityLane = 16
  var newCallbackPriority = getHighestPriorityLane(nextLanes);
  if (existingCallbackNode != null) {
    cancelCallback$1(existingCallbackNode);
  }
  var newCallbackNode;
  var schedulerPriorityLevel;
  switch (lanesToEventPriority(nextLanes)) {
    case DefaultEventPriority:
      schedulerPriorityLevel = NormalPriority;
      break;
    //...
  }
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

1.2.2 调度中处理队列finishQueueingConcurrentUpdates()

从下面三段代码可以看出,renderRootSync()->prepareFreshStack()->finishQueueingConcurrentUpdates()

finishQueueingConcurrentUpdates()做了两件事:

  • concurrentQueues取出queueupdatelane,将queueupdate进行关联!
  • 触发markUpdateLaneFromFiberToRoot()lanerootFiber冒泡

?????后续我们需要根据root.childLanes取出优先级最高的lane,创建对应的task进行

我们从下面可以知道,最终update放入到queue.pending中,如果有多个update(相同hook触发),那么会形成一个循环单链表数据(尾部节点指向头部节点)

javascript
function renderRootSync(root, lanes) {
  var prevExecutionContext = executionContext;
  executionContext |= RenderContext;

  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    //...
    workInProgressTransitions = getTransitionsForLanes(); // enableTransitionTracing=false,返回null
    prepareFreshStack(root, lanes);
  }
  workLoopSync();
  executionContext = prevExecutionContext;

  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;
  return workInProgressRootExitStatus;
}
function prepareFreshStack(root, lanes) {
  //...
  workInProgressRoot = root;
  var rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRootRenderLanes =
    subtreeRenderLanes =
    workInProgressRootIncludedLanes =
      lanes;
  //...
  finishQueueingConcurrentUpdates();
  return rootWorkInProgress;
}

本质就是将某一个 fiber 产生的所有更新操作都整理为一个链表结构,然后赋值到 queue 中,而 queue 就是 fiber.memoizedState 中的某一个属性!

因此本质还是 将 某一个 fiber 产生的所有更新与 fiber 进行互相关联

javascript
function finishQueueingConcurrentUpdates() {
  var endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;
  concurrentlyUpdatedLanes = NoLanes;
  var i = 0;

  while (i < endIndex) {
    var fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var queue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var update = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var lane = concurrentQueues[i];
    concurrentQueues[i++] = null;

    if (queue !== null && update !== null) {
      var pending = queue.pending;
      if (pending === null) {
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      queue.pending = update;
    }
    if (lane !== NoLane) {
      markUpdateLaneFromFiberToRoot(fiber, update, lane);
    }
  }
}
javascript
function markUpdateLaneFromFiberToRoot(sourceFiber, update, lane) {
  // Update the source fiber's lanes
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  var alternate = sourceFiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
  //...
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    }
    //...
    node = parent;
    parent = parent.return;
  }
  //...
}

1.3 触发全量渲染

处理fiber-updateFunctionComponent()

scheduleCallback()触发调度时,会从root开始遍历所有节点触发重新渲染,从而触发FunctionComponentbeginWork()

此时beginWork()触发的是updateFunctionComponent(),从而再次触发renderWithHooks()

javascript
function beginWork(current, workInProgress, renderLanes) {
  didReceiveUpdate = false;
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    case FunctionComponent: {
      var Component = workInProgress.type;
      //...
      return updateFunctionComponent(...);
    }
  }
}
function updateFunctionComponent() {
  nextChildren = renderWithHooks(...);

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

而此时的useReducer()不再是mountReducer,而是updateReducer()

1.3.1 updateReducer()

  • 先使用updateWorkInProgressHook()构建出hook对象,从hook中取出queue
  • queue.pending,也就是update对象(根据finishQueueingConcurrentUpdates()分析)赋值到baseQueue
  • 由于queue.pending拿到的是hook更新(多个更新update操作会形成一个循环单向链表)的最后一个节点,因此baseQueue.next可以拿到头节点,从头节点开始遍历整个链表,不断拿出action(也就是示例中dispatch1({type: "incremented_second"}){type: "incremented_second"}),触发reducrer(上一次reducer计算出来的state,传入的数据)来获取新的state

update===first时,说明已经updatefirst遍历到first

javascript
function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  var current = currentHook;
  var baseQueue = current.baseQueue;
  var pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  if (baseQueue !== null) {
    var first = baseQueue.next;
    var newState = current.baseState;
    var update = first;
    do {
      var action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    queue.lastRenderedState = newState;
  }
  if (baseQueue === null) {
    // 不需要更新
    queue.lanes = NoLanes;
  }
  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

1.3.1.1 updateWorkInProgressHook()

  • 先使用currentlyRenderingFiber$1.memoizedState获取当前渲染tree的头节点,如果该头节点不为空,则不断重用复用以该头节点构建的链表,不断
    • nextWorkInProgressHook = workInProgressHook.next
    • workInProgressHook=nextWorkInProgressHook

注:这里不是一个循环while,很多局部变量用一次就废弃了,比如nextWorkInProgressHook,每次进入updateWorkInProgressHook()都要重新赋值的,因此下面代码对比源码去除了无用的代码赋值

  • currentlyRenderingFiber$1.memoizedState可能为空,因此我们可以复用
    • current=currentlyRenderingFiber$1.alternate
    • 使用current.memoizedState所代表的链表去复制一个新的newHook,然后赋值给currentlyRenderingFiber$1.memoizedStateworkInProgressHook,此时currentHook代表着两棵tree相同位置对应的hook代码(useXXX()
    • 在下一次触发updateWorkInProgressHook()时,如果currentlyRenderingFiber$1.memoizedState所代表的链表还是为空,则继续复用alternate,也就是currentHook.next去复制出新的newHook,然后workInProgressHook.next=newHook

currentlyRenderingFiber$1.memoizedState可能为空发生在第一次更新??因为双缓冲树只是在mount构建了其中的一棵。然后第一个更新,会切换到新的tree,此时memoizedState为空

currentlyRenderingFiber$1.memoizedState不为空则发生在第二次~以后的更新??

经过多次更新尝试,每次currentlyRenderingFiber$1.memoizedState都为空!!每次都需要构建新的newHook,太奇怪了...暂时放下,再找找资料

javascript
function updateWorkInProgressHook() {
  var nextCurrentHook;

  if (currentHook === null) {
    var current = currentlyRenderingFiber$1.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  var nextWorkInProgressHook;

  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    currentHook = nextCurrentHook;
  } else {
    currentHook = nextCurrentHook;
    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }

  return workInProgressHook;
}

上面的代码过于繁琐和难以看懂,我们可以简化为下面代码

因为是更新阶段,一般alternate是存在的(可能存在之前没有hook,更新后又有hook,那么alternate就不存在)

这里考虑一般情况下=>可以简化代码逻辑,容易理解

由于是更新阶段,因此currentlyRenderingFiber$1.alternate必定存在,复用当前alternatememoizedState构建链表数据,主要是 头节点的赋值 + 剩余节点的赋值 两个步骤

涉及到三个全局变量的赋值:

  • currentHook:代表的是当前renderFiber.alternate对应的hook
  • currentlyRenderingFiber.memoizedState:当前renderFiber.alternate复制的单链表
  • workInProgressHook:当前renderFiber对应的hook

fiber.memoizedStatehook.memoizedState是不一样的!!

javascript
function updateWorkInProgressHook() {
	const current = currentlyRenderingFiber.alternate;
    // 更新模式会存在current
    currentlyRenderingFiber.memoizedState = current.memoizedState;
	if (workInProgressHook === null) {
		// 头节点还没赋值
		workInProgressHook = currentlyRenderingFiber.memoizedState;
		currentHook = current.memoizedState;
	} else {
		workInProgressHook = workInProgressHook.next;
		currentHook = currentHook.next;
	}
	return workInProgressHook;
}

2. useState

2.1 初始化mountState

从上面mountReducer()的分析可以知道,我们会从ClassComponentbeginWork()开始触发,然后进行useState()的执行,初始化阶段useState()就是mountState(),与mountReducer()一样

  • 使用mountWorkInProgressHook()构建一个hook对象
  • 然后进行initialState的初始化,因为可能是function,因此执行function()获取初始的state
  • 然后初始化hook.queue以及hook.dispatch方法
javascript
function mountState(initialState) {
  var hook = mountWorkInProgressHook();
  if (typeof initialState === "function") {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  var dispatch = (queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber$1,
    queue
  ));
  return [hook.memoizedState, dispatch];
}

2.2 dispatchSetState创建更新

2.2.1 创建update->压入队列->触发调度

  • 创建update对象
  • 判断是否有上一次更新的值,如果有旧的值,比对两个值,如果没有变化则不会将update加入到队列中,也就是阻止没有改变的值重复进行渲染更新,当然lane也不会更新到root节点中
  • 如果有变化,则加入队列中enqueueConcurrentHookUpdate
  • 然后开始调用scheduleUpdateOnFiber()

上面的流程跟useReducer()相比较,只是多了一步新旧值的比对,其他核心逻辑几乎是一致的!

javascript
function dispatchSetState(fiber, queue, action) {
  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  var alternate = fiber.alternate;
  if (
    fiber.lanes === NoLanes &&
    (alternate === null || alternate.lanes === NoLanes)
  ) {
    var lastRenderedReducer = queue.lastRenderedReducer;
    if (lastRenderedReducer !== null) {
      var currentState = queue.lastRenderedState;
      var eagerState = lastRenderedReducer(currentState, action);
      if (objectIs(eagerState, currentState)) {
        enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
        return;
      }
    }
  }
  var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    var eventTime = requestEventTime();
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitionUpdate(root, queue, lane);
  }
}

2.2.2 调度中处理队列finishQueueingConcurrentUpdates()

javascript
function renderRootSync(root, lanes) {
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    //...
    workInProgressTransitions = getTransitionsForLanes();
    prepareFreshStack(root, lanes);
  }
  workLoopSync();
  return workInProgressRootExitStatus;
}
function prepareFreshStack(root, lanes) {
  //...
  finishQueueingConcurrentUpdates();
  return rootWorkInProgress;
}
javascript
function finishQueueingConcurrentUpdates() {
  var endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;
  concurrentlyUpdatedLanes = NoLanes;
  var i = 0;

  while (i < endIndex) {
    var fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var queue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var update = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var lane = concurrentQueues[i];
    concurrentQueues[i++] = null;

    if (queue !== null && update !== null) {
      var pending = queue.pending;
      if (pending === null) {
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      queue.pending = update;
    }
    if (lane !== NoLane) {
      markUpdateLaneFromFiberToRoot(fiber, update, lane);
    }
  }
}

2.3 触发全量渲染-函数组件beginWork()

经过beginWork()->updateFunctionComponent(),从而再次触发renderWithHooks()

而此时的useState()不再是mountState,而是updateState(),从下面代码可以知道,本质也是updateReducer(),只是为useState()自动设置了一个reducer方法=basicStateReducer

javascript
function updateState(initialState) {
    return updateReducer(basicStateReducer, initialState);
}
function basicStateReducer(state, action) {
    return typeof action === "function" ? action(state) : action;
}

我们传入的reducerbasicStateReducer,然后进行hook.queue.pending->baseQueue,如果baseQueue为空,说明该hook没有更新,那么不触发reducer()执行以及hook.memoziedState的重新赋值!

由于我们在 dispatchSetState() 传入的值为 action,所以这里本质就是判断 action是否为 function,如果不是 function,直接返回传入的值

javascript
function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  var current = currentHook;
  var baseQueue = current.baseQueue;
  var pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  if (baseQueue !== null) {
    var first = baseQueue.next;
    var newState = current.baseState;
    var update = first;
    do {
      var action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    queue.lastRenderedState = newState;
  }
  if (baseQueue === null) {
    // 不需要更新
    queue.lanes = NoLanes;
  }
  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

3. 多种更新类型分析

更新的时候主要分为3种类型:

  • 删除节点
  • 新增节点
  • 移动节点

我们接下来将根据上面3种类型进行分析

比如删除节点,我们在什么阶段进行fiber删除的flag标记的?我们在什么阶段进行删除flag标记对应的dom节点的删除的?

reconcileChildFibers()中,我们需要判断当前是否是初次渲染的阶段,如果是初次渲染,则不用触发对应的删除逻辑

如果是渲染更新,分为两种情况

  • 新的数据是单个元素:触发reconcileSingleElement()
    • 旧的数据是单个元素:根据key+type判断是否可以复用,否则删除
    • 旧的数据是Array:根据key+type找到可以复用的数据,其余都删除
  • 新的数据是Array元素:触发reconcileChildrenArray() => 涉及到多个元素的diff算法
  • 新的数据为空时,直接触发deleteRemainingChildren()删除所有的旧节点数据

3.1 删除逻辑

  • 新的数据是单个元素:触发reconcileSingleElement()
    • 旧的数据是单个元素:根据key+type判断是否可以复用,否则删除
    • 旧的数据是Array:根据key+type找到可以复用的数据,其余都删除
ts
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
  var isUnkeyedTopLevelFragment =
    typeof newChild === "object" &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;

  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }

  if (typeof newChild === "object" && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes
          )
        );
      //...
    }

    if (isArray(newChild)) {
      //...新的数据是Array元素 => 触发`reconcileChildrenArray()`
    }
  }

  //...处理文本

  return deleteRemainingChildren(returnFiber, currentFirstChild); //...新的数据为空,直接删除旧的所有数据
}

当新的children是一个singleElement时,我们会触发reconcileSingleElement()进行处理

  • 如果key不同,则触发deleteChild()直接删除旧的节点数据
  • 如果key相同,type又不相同,说明可以复用的节点数据的类型已经改变,所有旧的数据都无法复用,直接触发deleteRemainingChildren()删除所有的旧节点数据
  • 如果key相同,type相同,则说明可以复用,保留当前的节点,触发deleteRemainingChildren()删除其余的旧节点数据

注:当child.tag === Fragment时,需要提取element.props.children而不是element.props

ts

function reconcileSingleElement(
  returnFiber,
  currentFirstChild, // 旧的数据
  element, // 新的数据
  lanes
) {
  var key = element.key;
  var child = currentFirstChild;

  while (child !== null) {
    if (child.key === key) {
      var elementType = element.type;

      if (elementType === REACT_FRAGMENT_TYPE) {
        if (child.tag === Fragment) {
          // 因为新的element数据是单节点,如果旧的数据也是同样的fragment,那么旧的数据的剩余节点都可以直接删除
          deleteRemainingChildren(returnFiber, child.sibling);
          var existing = useFiber(child, element.props.children);
          existing.return = returnFiber;

          return existing;
        }
      } else {
        if (child.elementType === elementType) {
          deleteRemainingChildren(returnFiber, child.sibling);

          var _existing = useFiber(child, element.props);

          _existing.ref = coerceRef(returnFiber, child, element);
          _existing.return = returnFiber;

          return _existing;
        }
      } // Didn't match.

      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      deleteChild(returnFiber, child);
    }

    child = child.sibling;
  }

  // ...新的数据的fiber创建逻辑
}

3.1.1 fiber标记:deleteChild标记单个子节点删除

获取当前fiber对应的deletions,然后将当前fiber想要删除的childFiber添加到deletions

注意:是将要删除的子fiber添加到父fiber的deletions中!! 并且给当前父fiber打上ChildDeletionflags

ts
function deleteChild(returnFiber, childToDelete) {
  if (!shouldTrackSideEffects) {
    return;
  }
  var deletions = returnFiber.deletions;

  if (deletions === null) {
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

3.1.2 fiber标记:deleteRemainingChildren标记多个节点删除

ts
function deleteRemainingChildren(returnFiber, currentFirstChild) {
  if (!shouldTrackSideEffects) {
    return null;
  }
  var childToDelete = currentFirstChild;

  while (childToDelete !== null) {
    deleteChild(returnFiber, childToDelete);
    childToDelete = childToDelete.sibling;
  }

  return null;
}

3.1.3 fiber标记处理:commit阶段处理ChildDeletion

根据fiber.ChildDeletion进行对应真实DOM的删除

在之前的分析中,我们知道commitMutationEffectsOnFiber()会触发

  • recursivelyTraverseMutationEffects()
  • commitReconciliationEffects()

commitMutationEffectsOnFiber()中高频出现的recursivelyTraverseMutationEffects()是为了

  • 处理当前fiber.deletions,在reconcileChildFibers()中进行fiber.deletions数据的添加(也就是fiber.children中已经被移除的数据)
  • 然后触发fiber.child进行commitMutationEffectsOnFiber()=>fiber.child处理完成,就处理fiber.child.sibling,触发commitMutationEffectsOnFiber()

总结:处理fiber.childrenDeletion集合 + 往下遍历fiber.child + fiber.child.sibling进行递归调用commitMutationEffectsOnFiber()

ts
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
  //...
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      if (flags & Update) {
        commitHookEffectListUnmount(Insertion | HasEffect, ...);
        commitHookEffectListMount(Insertion | HasEffect, finishedWork);
        commitHookEffectListUnmount(Layout | HasEffect, ...);
      }
      return;
    }
    case ClassComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      if (flags & Ref) {
        //...
      }
      return;
    }
    case HostComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      if (flags & Ref) {
        //...
      }
      if (finishedWork.flags & ContentReset) {
        //...
      }
      if (flags & Update) {
        //...
      }
      return;
    }
    case HostText: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      if (flags & Update) {
        //...
      }
      return;
    }
    case HostRoot: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      if (flags & Update) {
        //...
      }
      return;
    }
    case OffscreenComponent: {
      //...
    }
          //...还有多种类型
    default: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      return;
    }
  }
}

而在recursivelyTraverseMutationEffects()中,我们直接获取当前fiber的deletions,也就是下面的parentFiber.deletions的数据,然后直接处理当前fiber的所有需要删除的children

因为这里是深度遍历,会先处理children->children.sibling-> parent,因此我们先把当前fiber的所有需要删除的children处理了,那么就不需要深度遍历需要删除的children了

ts
function recursivelyTraverseMutationEffects(root: FiberRoot, parentFiber: Fiber) {
  var deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (var i = 0; i < deletions.length; i++) {
      var childToDelete = deletions[i];
      commitDeletionEffects(root, parentFiber, childToDelete);
    }
  }

  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;

    while (child !== null) {
      commitMutationEffectsOnFiber(child, root);
      child = child.sibling;
    }
  }
}

从上面代码知道,处理逻辑主要集中在commitDeletionEffects()

  • 先使用一个while()获取目前要删除的fiber的parent的真实DOMhostParent

hostParent是一个全局变量!!!

  • 然后触发commitDeletionEffectsOnFiber()
  • 然后触发detachFiberMutation()
ts
function commitDeletionEffects(root, returnFiber, deletedFiber) {
  var parent = returnFiber;

  findParent: while (parent !== null) {
    switch (parent.tag) {
      case HostComponent: {
        hostParent = parent.stateNode;
        hostParentIsContainer = false;
        break findParent;
      }
      case HostRoot: {
        hostParent = parent.stateNode.containerInfo;
        hostParentIsContainer = true;
        break findParent;
      }
      case HostPortal: {
        hostParent = parent.stateNode.containerInfo;
        hostParentIsContainer = true;
        break findParent;
      }
    }
    parent = parent.return;
  }

  if (hostParent === null) {
    throw new Error(
      "Expected to find a host parent. This error is likely caused by " +
        "a bug in React. Please file an issue."
    );
  }

  commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
  hostParent = null;
  hostParentIsContainer = false;

  detachFiberMutation(deletedFiber);
}

3.1.3.1 commitDeletionEffectsOnFiber()

commitDeletionEffectsOnFiber()中,根据deletedFiber.tag进行了不同类型的繁杂处理

虽然代码量非常多,但是我们仔细观察就会发现,其本质逻辑就是:

  • HostComponent&HostText类型会触发recursivelyTraverseDeletionEffects(),基本没做什么其他处理
  • HostComponent类型会先触发safelyDetachRef,由于没有break,因此后续会触发HostText的处理
  • HostText类型先触发recursivelyTraverseDeletionEffects(),然后触发removeChild()进行原生DOM的removeChild()移除DOM操作
ts
function commitDeletionEffectsOnFiber(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber
) {
  //...
  switch (deletedFiber.tag) {
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
      }
    }
    case HostText: {
      {
        var prevHostParent = hostParent;
        var prevHostParentIsContainer = hostParentIsContainer;
        hostParent = null;
        recursivelyTraverseDeletionEffects(
                finishedRoot,
                nearestMountedAncestor,
                deletedFiber
        );
        hostParent = prevHostParent;
        hostParentIsContainer = prevHostParentIsContainer;

        if (hostParent !== null) {
          // Now that all the child effects have unmounted, we can remove the
          // node from the tree.
          if (hostParentIsContainer) {
            removeChildFromContainer(hostParent, deletedFiber.stateNode);
          } else {
            removeChild(hostParent, deletedFiber.stateNode);
          }
        }
      }

      return;
    }

    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      //...
      recursivelyTraverseDeletionEffects(
              finishedRoot,
              nearestMountedAncestor,
              deletedFiber
      );
      return;
    }

    case ClassComponent: {
      if (!offscreenSubtreeWasHidden) {
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
        var instance = deletedFiber.stateNode;

        if (typeof instance.componentWillUnmount === "function") {
          safelyCallComponentWillUnmount(
                  deletedFiber,
                  nearestMountedAncestor,
                  instance
          );
        }
      }

      recursivelyTraverseDeletionEffects(
              finishedRoot,
              nearestMountedAncestor,
              deletedFiber
      );
      return;
    }

    default: {
      recursivelyTraverseDeletionEffects(
              finishedRoot,
              nearestMountedAncestor,
              deletedFiber
      );
      return;
    }
  }
}

因此recursivelyTraverseDeletionEffects()到底执行了什么呢?

recursivelyTraverseDeletionEffects()的逻辑也非常简单,就是取出要删除的fiber的children,然后触发commitDeletionEffectsOnFiber()

commitDeletionEffectsOnFiber()就是根据不同类型进行处理的方法:

  • 遇到HostComponent&HostText类型时会触发removeChild()进行原生DOM的removeChild()移除DOM操作
  • 遇到非HostComponent&HostText类型时则会继续调用recursivelyTraverseDeletionEffects()取出要删除的fiber的children,然后触发commitDeletionEffectsOnFiber()

如果当前类型是HostComponent,直接删除parentDom.removeChild(childDom)即可

如果当前类型是FunctionComponent,我们需要触发对应的effect,然后拿到fiber.child(因为FunctionComponent这个fiber是不具备DOM的)才是它的DOM,甚至有可能这个fiber.child是一个数组,也就是多个DOM,我们需要遍历所有DOM进行removeChild()

ts
function recursivelyTraverseDeletionEffects(
  finishedRoot,
  nearestMountedAncestor,
  parent
) {
  var child = parent.child;

  while (child !== null) {
    commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
    child = child.sibling;
  }
}

3.1.3.2 detachFiberMutation()

由于当前fiber已经被删除,因此可以直接切断当前fiber与双缓冲树的联系

ts
function detachFiberMutation(fiber) {
  var alternate = fiber.alternate;

  if (alternate !== null) {
    alternate.return = null;
  }

  fiber.return = null;
}

3.2 移动/新增逻辑

节点复用的三个条件:同一层级下 + key相同 + type相同(也就是htmltag相同,都是<div>,都是<span>

新的数据是Array元素:触发reconcileChildrenArray() => 涉及到多个元素的diff算法

ts
function reconcileChildrenArray(...) {
  // 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    //...
  }

  // 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    //...
    return resultingFirstChild;
  }

  // 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
  if (oldFiber === null) {
    //...
  }

  // 3.2.3 新的节点和旧的节点还有,进行diff复用
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    //...
  }

  // 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
  if (shouldTrackSideEffects) {
    existingChildren.forEach(function (child) {
      return deleteChild(returnFiber, child);
    });
  }
  return resultingFirstChild;
}

3.2.1 fiber标记:从左边到右边,从index=0不断递增,比较是否可以直接复用,减少diff的范围

比如

旧:[1, 2, 3, 4]

新:[1, 2, 3, 5, 6]

我们可以比较得到前三位的数据是可以直接复用的,那么我们diff的范围就缩小到

旧:[4]

新:[5, 6]

newIdx = 0开始遍历,我们要缩减的是左边的边界

  • 如果oldFiber.index > newIdx,说明能够左边的边界已经不能缩减了,那么我们就将oldFiber设置为null,最终得到的newFiber肯定为null,那么最终会触发break,同时使用oldFiber = nextOldFiber恢复数据

比如旧的[1, 2, 3, 4]和新的[1, 3, 3, 4],当旧的的3oldFiber.index=2,新的3newIdx=1

  • 如果oldFiber.index <= newIdx,说明还可以缩减左边的边界,继续单链表的下一个数据nextOldFiber = oldFiber.sibling
  • 使用updateSlot()检测旧的fiber是否被复用

updateSlot()先进行key的判断,如果key不一样,则肯定无法直接复用,直接返回null => 可能目前数据是移动了,还可以被其他节点复用,不删除旧的fiber!!!!

如果key一样,说明是原来对应的数据,也不可能被其他节点复用了,只是可能<tag>变了,那么就直接创建新的fiber,然后删除掉旧的fiber!!!

  • 如果旧的fiber能够被复用(keytag都相同),则直接更新旧的fiber => newFiber
  • 如果旧的fiber存在但是key相同,说明是原来对应的数据,如果旧的fiber为NULL/旧的fiber的tag不一致,直接创建新的fiber => newFiber,然后后续逻辑删除旧的fiber
  • 如果旧的fiber存在但是key不同,说明不是原来对应的数据 => 返回newFiber=null => 中断左边边界的缩减

下面的小节会展开对updateSlot()源码的详细分析

  • 如果newFiber=null,说明无法缩减左边的边界了,直接break当前的循环
  • 如果newFiber不为null,说明是oldFibernewFiberkey相同,是原来对应的数据,可能可以复用
    • 通过oldFiber && newFiber.alternate === null,说明还是不能复用,是直接创建了新的fiber => 删除旧的fiber
    • 否则就是可以复用 => 通过lastPlacedIndex判断是否是move/insertion,然后打上newFiber.flags |= Placement
  • 最终构建单链表结构:resultingFirstChild代表头节点,previousNewFiber代表前节点,可以不断previousNewFiber.sibling = newFiber
ts
function reconcileChildrenArray() {
  // 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    var newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes
    );

    if (newFiber === null) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }

    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  // 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    //...
    return resultingFirstChild;
  }

  // 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
  if (oldFiber === null) {
    //...
  }

  // 3.2.3 新的节点和旧的节点还有,进行diff复用
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    //...
  }

  // 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
  if (shouldTrackSideEffects) {
    existingChildren.forEach(function (child) {
      return deleteChild(returnFiber, child);
    });
  }
  return resultingFirstChild;
}

3.2.1.1 updateSlot

主要分为3种情况进行处理:文本格式、Object格式(包括各种FunctionComponent、ClassComponent等等)、Array格式(Fragment)

先进行key的判断,如果key不一样,则肯定无法直接复用,直接返回null => 可能目前数据是移动了,还可以被其他节点复用,不删除旧的fiber!!!! 如果key一样,说明是原来对应的数据,也不可能被其他节点复用了,只是可能<tag>变了,那么就直接创建新的fiber,然后删除掉旧的fiber!!!

  • 先处理newChild是否是文本格式,由于文本数据没有key,因此旧的数据存在key时,说明是非文本数据->文本数据,无法复用,直接返回null
  • 然后处理newChildobject格式,只有key相同才有可能复用,一定不能复用时直接返回null,然后进入updateTextNode()尝试复用,如果无法复用则直接返回新创建的fiber
  • 然后处理newChildarray格式,新数据没有key,如果旧数据存在key,则一定不能复用,一定不能复用时直接返回null,然后进入updateFragment()尝试复用,如果无法复用则直接返回新创建的fiber
ts
function updateSlot(returnFiber, oldFiber, newChild, lanes) {
  var key = oldFiber !== null ? oldFiber.key : null;
  if (
    (typeof newChild === "string" && newChild !== "") ||
    typeof newChild === "number"
  ) {
    if (key !== null) {
      return null;
    }

    return updateTextNode(returnFiber, oldFiber, "" + newChild, lanes);
  }

  if (typeof newChild === "object" && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        if (newChild.key === key) {
          return updateElement(returnFiber, oldFiber, newChild, lanes);
        } else {
          return null;
        }
      }
      case REACT_PORTAL_TYPE: {}
      case REACT_LAZY_TYPE: {}
    }

    if (isArray(newChild) || getIteratorFn(newChild)) {
      if (key !== null) {
        return null;
      }
      return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
    }

    throwOnInvalidObjectType(returnFiber, newChild);
  }

  return null;
}

3.2.1.2 updateTextNode()

  • 如果当前旧的fiber为空或者当前fiber不是文本数据,直接创建新的fiber文本数据
  • 如果当前旧的fiber可以被复用,使用useFiber()复用fiber并且更新数据,返回新的fiber文本数据
ts
function updateTextNode(returnFiber, current, textContent, lanes) {
  if (current === null || current.tag !== HostText) {
    // Insert
    var created = createFiberFromText(textContent, returnFiber.mode, lanes);
    created.return = returnFiber;
    return created;
  } else {
    // Update
    var existing = useFiber(current, textContent);
    existing.return = returnFiber;
    return existing;
  }
}

3.2.1.3 updateFragment()

跟上面updateTextNode()逻辑基本一致

  • 如果为空或者当前的<tag>已经改变,则创建新的fiber,只是使用的方法从createFiberFromText()->createFiberFromFragment()
  • 如果可以复用,则使用useFiber()进行数据的更新
ts
function updateFragment(returnFiber, current, fragment, lanes, key) {
  if (current === null || current.tag !== Fragment) {
    // Insert
    var created = createFiberFromFragment(...);
    created.return = returnFiber;
    return created;
  } else {
    // Update
    var existing = useFiber(current, fragment);
    existing.return = returnFiber;
    return existing;
  }
}

3.2.1.4 updateElement()

  • 如果当前新的数据是fragment,取出element.props.children数组数据去触发updateFragment()
  • 检查旧的fiber和新的fiber的type是否相同,相同则进行复用,更新旧的fiber数据
  • 如果type不相同或者当前旧的fiber为空,则直接新建新的fiber数据
ts
function updateElement(returnFiber, current, element, lanes) {
  var elementType = element.type;

  if (elementType === REACT_FRAGMENT_TYPE) {
    return updateFragment(
      returnFiber,
      current,
      element.props.children,
      lanes,
      element.key
    );
  }

  if (current !== null) {
    if (
      current.elementType === elementType ||
      (typeof elementType === "object" &&
        elementType !== null &&
        elementType.$$typeof === REACT_LAZY_TYPE &&
        resolveLazy(elementType) === current.type)
    ) {
      // Move based on index
      var existing = useFiber(current, element.props);
      existing.ref = coerceRef(returnFiber, current, element);
      existing.return = returnFiber;

      return existing;
    }
  } // Insert

  var created = createFiberFromElement(element, returnFiber.mode, lanes);
  created.ref = coerceRef(returnFiber, current, element);
  created.return = returnFiber;
  return created;
}

3.2.1.5 placeChild()

如果是初次渲染,即shouldTrackSideEffects=false,那么直接返回lastPlacedIndex即可

如果不是初次渲染,那么我们需要根据判断是move还是insert数据,如果是move,还需要根据lastPlaceIndex进行位置判断

ts
function placeChild(newFiber, lastPlacedIndex, newIndex) {
  newFiber.index = newIndex;

  if (!shouldTrackSideEffects) {
    newFiber.flags |= Forked;
    return lastPlacedIndex;
  }

  var current = newFiber.alternate;

  if (current !== null) {
    var oldIndex = current.index;

    if (oldIndex < lastPlacedIndex) {
      // This is a move.
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      // This item can stay in place.
      return oldIndex;
    }
  } else {
    // This is an insertion.
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}

上面的代码可以使用下图流程辅助理解 Image

3.2.2 fiber标记:经过上面的流程,如果出现旧的节点已经没有/新的节点已经没有,那么只需要进行新增剩余节点/删除剩余节点

不需要再进行复杂的diff判断是否可以进行复用

当左边边界缩减后,新的节点已经遍历完成,旧的节点还有

那么就进行剩余旧节点的删除工作,直接触发deleteRemainingChildren()删除旧的fiber

当左边边界缩减后,新的节点还没遍历完成,旧的节点已经没有

那么就进行剩余新节点的创建工作

  • 使用createChild()创建新的fiber数据
  • 使用placeChild()进行标记:由于newFiber.alternate为空,因此直接newFiber.flags |= Placement,不考虑lastPlacedIndex
ts
function reconcileChildrenArray() {
  // 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    //
  }

  // 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    //...
    return resultingFirstChild;
  }

  // 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (_newFiber === null) {
        continue;
      }

      lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);

      // 构建一个新的单链表结构,不停将当前fiber后移到下一个fiber
      if (previousNewFiber === null) {
        resultingFirstChild = _newFiber;
      } else {
        previousNewFiber.sibling = _newFiber;
      }
      previousNewFiber = _newFiber;
    }
    return resultingFirstChild;
  }

  // 3.2.3 新的节点和旧的节点还有,进行diff复用
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    //...
  }

  // 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
  if (shouldTrackSideEffects) {
    existingChildren.forEach(function (child) {
      return deleteChild(returnFiber, child);
    });
  }
  return resultingFirstChild;
}

3.2.3 fiber标记:经过上面的流程,旧的节点/新的节点都还有,进行复杂的diff

哪些可以复用?哪些需要新增?哪些需要删除?

从下面代码可以看出

  • 使用mapRemainingChildren()构建oldFiber的Map数据
  • 遍历剩余的newFiber数据,从mapRemainingChildren()找到可以复用的fiber进行数据更新,否则直接创建新的Fiber数据
  • 删除已经复用的fiber数据,从existingChildren这个Map集合中删除已经复用的oldFiber数据
  • 使用placeChild()判断是move还是insert数据,如果是move,还需要根据lastPlaceIndex进行位置判断是否需要newFiber.flags |= Placement
  • 最终构建出新的fiber的单链表结构进行返回

注:lastPlacedIndex是指目前遍历到的元素标记Placement的最大index!不是指目前遍历的index!

ts
function reconcileChildrenArray() {
  // 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    //
  }
  // 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    //...
    return resultingFirstChild;
  }
  // 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
  if (oldFiber === null) {
    //...
  }

  // 3.2.3 新的节点和旧的节点还有,进行diff复用
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    var _newFiber2 = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes
    );
    if (_newFiber2 !== null) {
      if (shouldTrackSideEffects) {
        if (_newFiber2.alternate !== null) {
          existingChildren.delete(
            _newFiber2.key === null ? newIdx : _newFiber2.key
          );
        }
      }
      lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        resultingFirstChild = _newFiber2;
      } else {
        previousNewFiber.sibling = _newFiber2;
      }

      previousNewFiber = _newFiber2;
    }
  }

  // 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
  if (shouldTrackSideEffects) {
    existingChildren.forEach(function (child) {
      return deleteChild(returnFiber, child);
    });
  }
  return resultingFirstChild;
}

3.2.4 fiber标记:经过diff后,新节点已经遍历完成,旧的节点还有

如果diff完成后,旧的节点还有剩余,也就是existingChildren这个Map数据还残留着数据,则直接删除目前的所有旧Fiber数据

ts
function reconcileChildrenArray() {
  // 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    //
  }
  // 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    //...
    return resultingFirstChild;
  }
  // 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
  if (oldFiber === null) {
    //...
  }
  // 3.2.3 新的节点和旧的节点还有,进行diff复用
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  for (; newIdx < newChildren.length; newIdx++) {
    //...
  }
  // 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
  if (shouldTrackSideEffects) {
    existingChildren.forEach(function (child) {
      return deleteChild(returnFiber, child);
    });
  }
  return resultingFirstChild;
}

3.2.5 fiber标记处理:commit阶段处理Placement

  • 通过getHostParentFiber()找到parentFiberHostComponent/HostRoot/HostPortal的parent,否则一直parent=parent.return
  • 通过getHostSibling()寻找具备stateNode真实DOM + 没有Placement标记的fiber,然后返回其node.stateNode
  • 使用insertOrAppendPlacementNode()处理各种类型fiber的DOM的插入操作,包括: FunctionComponent、HostComponent等等的插入操作

insertOrAppendPlacementNode()可以参考下面的分析

ts
function commitPlacement(finishedWork) {
  var parentFiber = getHostParentFiber(finishedWork);

  switch (parentFiber.tag) {
    case HostComponent: {
      var parent = parentFiber.stateNode;
      var before = getHostSibling(finishedWork);
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }

    case HostRoot:
    case HostPortal: {
      var _parent = parentFiber.stateNode.containerInfo;
      var _before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
      break;
    }
  }
}

注:insertOrAppendPlacementNode()可以参考文章首次渲染流程分析(二)4.3.1 insertOrAppendPlacementNodeIntoContainer(),下面的分析摘自4.3.1

nodeFunctionComponent,它是不具备DOM的!!!因此isHost=false,触发了第三个条件的代码

我们会直接取node.child,也就是FunctionComponent中顶层元素<div>,然后触发insertOrAppendPlacementNodeIntoContainer(),这个时候nodeHostComponent,具备DOM,因此可以执行插入操作,也就是#root.appendChild(<div/>)

处理完node.child还不够,我们还得处理下node.child.sibling,因此可能存在着FunctionComponent的顶层元素是一个<React.Fragment>的情况,它也是一个不具备DOM的类型,我们需要#root.appendChild(Fragment的childDOM)

ts
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
  var tag = node.tag;
  var isHost = tag === HostComponent || tag === HostText;

  if (isHost) {
    var stateNode = node.stateNode;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal);
  else {
    var child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      var sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

3.3 React18与Vue3的diff算法简单对比

下面Vue分析摘自以前的文章:https://segmentfault.com/a/1190000042974066

Vue 3.2.30的源码可以知道,源码中有对patchKeyedChildren()方法进行了核心步骤的注释,摘出核心步骤的注册如下面代码块所示,可以分为5个步骤:

  • 步骤1:从头->尾,处理相同的前置元素
  • 步骤2:从尾->头,处理相同的后置元素
  • 步骤3:旧vnode已经处理完毕,但是新vnode还有元素,处理新增元素,直接进行mount
  • 步骤4:旧vnode还有剩余,但是新vnode已经处理完毕,处理已经废弃元素,直接进行unmount
  • 步骤5:最复杂的情况,处理相同的前置元素+处理相同的后置元素后,剩下的元素有新增、废弃、乱序的情况,需要复杂处理

React 18.3.1的主要diff流程如下所示

  • 步骤1:从左边到右边,从index=0不断递增,比较是否可以直接复用,减少diff的范围
  • 步骤2:新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
  • 步骤3:旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
  • 步骤4:新的节点和旧的节点还有,进行diff复用
  • 步骤5:新的节点已经新增/移动完毕,剩下的旧节点应该删除

在上面两个框架的流程概述中,我们可以发现,Vue增加了从尾->头,处理相同的后置元素的处理步骤,然后主要区别就在于新增、废弃、乱序的处理

3.3.1 Vue3中新增、废弃、乱序的处理

下面步骤摘自以前的文章:https://segmentfault.com/a/1190000042974066

  • 步骤5.1:为newChildren建立索引
  • 步骤5.2:移除废弃的旧vnode + 更新能复用的旧vnode + newIndexToOldIndexMap和move的构建为下一步骤做准备
    • 逻辑1:移除废弃的旧vnode
    • 逻辑2:更新能复用的旧vnode
    • 逻辑3:newIndexToOldIndexMap和move的构建为步骤5.3做准备
  • 步骤5.3:移动/新增处理
    • 新增:判断目前新vnode是之前没有过的新vnode
    • 移动:判断目前新vnode对应的可复用的旧vnode是否需要移动位置
      • 流程1: 构建最长递增子序列
      • 流程2: 根据increasingNewIndexSequence进行节点的移动

increasingNewIndexSequence最长递增子序列的作用:获取旧的children在新的children的相对位置顺序仍然递增的最长子序列,减少move的次数,提升性能

ts
const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR;
j = increasingNewIndexSequence.length - 1;
for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i;
    const nextChild = c2[nextIndex];
    const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
    if (newIndexToOldIndexMap[i] === 0) {
        patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
    } else if (moved) {
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, 2 /* REORDER */);
        } else {
            j--;
        }
    }
}
const move = (vnode, container, anchor, moveType, parentSuspense = null) => {
    const { el, type, transition, children, shapeFlag } = vnode;
    //...多种类型的数据进行不同的move处理,包括组件、Comment、Static
    if (needTransition) {
      //...
    } else {
        hostInsert(el, container, anchor);
    }
};
  • 如果increasingNewIndexSequence.length=0,即构建不出来最长递增子序列的时候,按照上面代码块的逻辑,我们从末尾开始,使用当前新vnode后面的index作为参照,不停将旧vnode移动插入到c2[nextIndex+1]的前面
  • 如果increasingNewIndexSequence.length>0,那么遇到i === increasingNewIndexSequence[j]时,代表目前的nextChild是最长递增子序列的一个元素,由于最长递增子序列代表旧vnode的相关位置在新vnode的相关位置仍然保持递增,因此这些位于最长递增子序列的元素可以不进行move操作,直接进行j--即可