алгоритм diff (мама больше не беспокоится о моем diff-интервью)

React.js

Анализ исходного кода React, понятный каждому (требуется для высокой зарплаты на крупных фабриках)

8.Алгоритм сравнения (мама больше не беспокоится о моем интервью)

Видео конечно и Debug Demos

​ Цель видеокурса - быстро освоить процесс запуска исходного кода реакта и планировщика, реконсилера, рендерера, файбера и т.д. в реакте, а также детально отладить исходный код и анализ, и процесс понятнее .

Видео уроки:войти в курс

демо:demo

структура курса:

  1. Начало (я слышал, что вы все еще боретесь с исходным кодом реакции)
  2. Ментальная модель React (Давай, давай, пусть мозг будет реагировать на мышление)
  3. Волокно (я дом в памяти)
  4. Начните с устаревшего или параллельного (начните с записи, затем давайте перейдем к будущему)
  5. процесс обновления состояния (что именно происходит в setState)
  6. этап рендеринга (отлично, у меня есть навыки создания Fiber)
  7. Этап коммита (я слышал, что рендерер помог нам его разметить, отобразить реальный узел)
  8. алгоритм diff (мама больше не беспокоится о моем diff-интервью)
  9. Источник крючков (функциональный компонент хотел знать, как сохранить состояние вещи)
  10. модель планировщика и полосы (чтобы увидеть, приостановлены ли задачи, возобновлены и поставлены в очередь)
  11. параллельный режим (что такое параллельный режим)
  12. Рукописная мини-реакция (короткий и умный - это я)

​ При обновлении узла Fiber на этапе рендеринга мы будем вызывать reconcileChildFibers для сравнения текущих объектов Fiber и jsx для построения workInProgress Fiber, где current Fiber ссылается на дерево волокон, соответствующее текущему dom, а jsx — это возвращаемое значение метод рендеринга компонента класса или функционального компонента.

​ В reconcileChildFibers одноузловое или многоузловое различие будет введено в соответствии с типом newChild.

function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {

  const isObject = typeof newChild === 'object' && newChild !== null;

  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
				//单一节点diff
        return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
    }
  }
	//...
  
  if (isArray(newChild)) {
     //多节点diff
    return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
  }

  // 删除节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

Основной поток процесса diff выглядит следующим образом:

_14

Мы знаем, что сложность сравнения двух деревьев составляет O(n3), что является невыносимой величиной для нашего приложения.Чтобы уменьшить сложность, react предлагает три предпосылки:

  1. Только для однорангового сравнения, межуровневый домен не будет повторно использоваться.

  2. Деревья домов, сгенерированные разными типами узлов, различаются.В это время старые узлы и узлы-потомки будут непосредственно уничтожены, а новые узлы будут созданы.

  3. Ключи можно использовать для предоставления подсказок мультиплексирования процессу сравнения элементов, например:

    const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
    const b = (
      <>
        <p key="1">1</p>
        <p key="0">0</p>
      </>
    );
    

    ​ Если в элементах a и b нет ключа, поскольку текстовые узлы до и после обновления узла различны, их нельзя использовать повторно, поэтому предыдущий узел будет уничтожен и будет создан новый узел, но теперь нет является ключом, узел в b будет искать узлы с тем же ключом в старом a и пытаться использовать их повторно, и, наконец, обнаружил, что обновление может быть завершено, просто заменив позицию, Конкретный процесс сравнения будет обсуждаться позже .

    разница в одном узле

    Один точка Diff следующие ситуации:

    • Один и тот же ключ и тип указывают, что узлы можно использовать повторно.
    • Если ключ отличается, сразу отметьте и удалите узел, а затем создайте новый узел.
    • Ключ тот же, а тип другой, отметьте удаление узла и родственного узла, а затем создайте новый узел
    function reconcileSingleElement(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      element: ReactElement
    ): Fiber {
      const key = element.key;
      let child = currentFirstChild;
      
      //child节点不为null执行对比
      while (child !== null) {
    
        // 1.比较key
        if (child.key === key) {
    
          // 2.比较type
    
          switch (child.tag) {
            //...
            
            default: {
              if (child.elementType === element.type) {
                // type相同则可以复用 返回复用的节点
                return existing;
              }
              // type不同跳出
              break;
            }
          }
          //key相同,type不同则把fiber及和兄弟fiber标记删除
          deleteRemainingChildren(returnFiber, child);
          break;
        } else {
          //key不同直接标记删除该节点
          deleteChild(returnFiber, child);
        }
        child = child.sibling;
      }
       
      //新建新Fiber
    }
    
    

    многоузловая разность

    Дифференциал с несколькими узлами более сложен, мы обсудим его в трех случаях, где a представляет собой узел до обновления, а b представляет узел после обновления.

    • изменение свойства

      const a = (
          <>
            <p key="0" name='0'>0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <p key="0" name='00'>0</p>
            <p key="1">1</p>
          </>
        );
      
    • изменение типа

      const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <div key="0">0</div>
            <p key="1">1</p>
          </>
        );
      
    • новый узел

      const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
            <p key="2">2</p>
          </>
        );	
      
    • Удаление узла

      const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
            <p key="2">2</p>
          </>
        );
        const b = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
      
    • Изменение положения узла

      	const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <p key="1">1</p>
            <p key="0">0</p>
          </>
        );
      

    ​ В исходном коде многоузловое сравнение будет проходить через три обхода.Первый обход обрабатывает обновления узлов (включая обновления свойств, обновления и удаления типов), а второй обход обрабатывает другие случаи (добавления узлов).Причина заключается в том, что в больших приложениях частота обновлений узла более частая, и в третий раз положение узла изменяется

    • первый обход

      ​ Поскольку старый узел существует в текущем волокне, он представляет собой структуру связанного списка. Помните о структуре двойного кеша Fiber. Узлы связаны дочерними, возвратными и одноуровневыми узлами, а newChildren существует в jsx, поэтому при обходе и сравнении сначала пусть новые дети [i]Сравните oldFiber, затем пусть i++, nextOldFiber = oldFiber.sibling. В первом раунде обхода будут обработаны три кейса, из которых первый и второй кейсы завершат первый цикл.

      1. Ключ другой, первый цикл заканчивается
      2. После прохождения newChildren или oldFiber первый цикл завершается.
      3. Ключ отличается от типа, а oldFiber помечен как DELETION
      4. Один и тот же ключ и тот же тип можно использовать повторно

      После завершения обхода newChildren старыйFiber не был пройден.После завершения первого обхода непройденные узлы в oldFiber помечаются как DELETION, то есть удаленный тег DELETION

  • второй обход

    Второй обход рассматривает три случая

      1. newChildren和oldFiber都遍历完:多节点diff过程结束
      2. newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag
    
    1. Если newChildren и oldFiber не были пройдены, введите логику перемещения узла
  • третий обход

    Основная логика в функции placeChild, например порядок узлов до обновления ABCD, а после обновления ACDB

    1. A в первой позиции newChild и A в первой позиции oldFiber, один и тот же ключ можно использовать повторно, lastPlacedIndex=0

    2. C во второй позиции newChild и B во второй позиции oldFiber, ключи разные и выскакивают из первого цикла, сохраняем BCD в oldFiber в карту

    3. C во второй позиции в newChild не нужно перемещать, если index=2 > lastPlacedIndex=0 в oldFiber, lastPlacedIndex=2

    4. newChild третья позиция в индексе D oldFiber in = 3> lastPlacedIndex = 2 перемещать не нужно, lastPlacedIndex = 3

      1. Индекс = 1

        Смотрите на картинку более интуитивно

      _15

    Например, порядок узлов до обновления — ABCD, а после обновления — DABC.

    1. D в первой позиции newChild и A в первой позиции oldFiber, ключи разные и не могут использоваться повторно, сохраните ABCD в oldFiber на карте, lastPlacedIndex=0

    2. Первая позиция D в newChild: index=3 в oldFiber> lastPlacedIndex=0 не нужно перемещать, lastPlacedIndex=3

    3. A во второй позиции в newChild имеет index=0

    4. B на третьей позиции в newChild index=1

    5. C на четвертой позиции в newChild index=2

      Смотрите на картинку более интуитивно

    _16

    код показывает, как показано ниже:

      function placeChild(newFiber, lastPlacedIndex, newIndex) {
        newFiber.index = newIndex;
    
        if (!shouldTrackSideEffects) {
          return lastPlacedIndex;
        }
    
     var current = newFiber.alternate;
    
        if (current !== null) {
          var oldIndex = current.index;
    
          if (oldIndex < lastPlacedIndex) {
            //oldIndex小于lastPlacedIndex的位置 则将节点插入到最后
            newFiber.flags = Placement;
            return lastPlacedIndex;
          } else {
            return oldIndex;//不需要移动 lastPlacedIndex = oldIndex;
          }
        } else {
          //新增插入
          newFiber.flags = Placement;
          return lastPlacedIndex;
        }
      }
    

    function reconcileChildrenArray(
        returnFiber: Fiber,//父fiber节点
        currentFirstChild: Fiber | null,//childs中第一个节点
        newChildren: Array<*>,//新节点数组 也就是jsx数组
        lanes: Lanes,//lane相关 第12章介绍
      ): Fiber | null {
    
        let resultingFirstChild: Fiber | null = null;//diff之后返回的第一个节点
        let previousNewFiber: Fiber | null = null;//新节点中上次对比过的节点
    
        let oldFiber = currentFirstChild;//正在对比的oldFiber
        let lastPlacedIndex = 0;//上次可复用的节点位置 或者oldFiber的位置
        let newIdx = 0;//新节点中对比到了的位置
        let nextOldFiber = null;//正在对比的oldFiber
        for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {//第一次遍历
          if (oldFiber.index > newIdx) {//nextOldFiber赋值
            nextOldFiber = oldFiber;
            oldFiber = null;
          } else {
            nextOldFiber = oldFiber.sibling;
          }
          const newFiber = updateSlot(//更新节点,如果key不同则newFiber=null
            returnFiber,
            oldFiber,
            newChildren[newIdx],
            lanes,
          );
          if (newFiber === null) {
            if (oldFiber === null) {
              oldFiber = nextOldFiber;
            }
            break;//跳出第一次遍历
          }
          if (shouldTrackSideEffects) {//检查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;
        }
    
        if (newIdx === newChildren.length) {
          deleteRemainingChildren(returnFiber, oldFiber);//将oldFiber中没遍历完的节点标记为DELETION
          return resultingFirstChild;
        }
    
        if (oldFiber === null) {
          for (; newIdx < newChildren.length; newIdx++) {//第2次遍历
            const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
            if (newFiber === null) {
              continue;
            }
            lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//插入新增节点
            if (previousNewFiber === null) {
              resultingFirstChild = newFiber;
            } else {
              previousNewFiber.sibling = newFiber;
            }
            previousNewFiber = newFiber;
          }
          return resultingFirstChild;
        }
    
        // 将剩下的oldFiber加入map中
        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    
        for (; newIdx < newChildren.length; newIdx++) {//第三次循环 处理节点移动
          const newFiber = updateFromMap(
            existingChildren,
            returnFiber,
            newIdx,
            newChildren[newIdx],
            lanes,
          );
          if (newFiber !== null) {
            if (shouldTrackSideEffects) {
              if (newFiber.alternate !== null) {
                existingChildren.delete(//删除找到的节点
                  newFiber.key === null ? newIdx : newFiber.key,
                );
              }
            }
            lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//标记为插入的逻辑
            if (previousNewFiber === null) {
              resultingFirstChild = newFiber;
            } else {
              previousNewFiber.sibling = newFiber;
            }
            previousNewFiber = newFiber;
          }
        }
    
        if (shouldTrackSideEffects) {
          //删除existingChildren中剩下的节点
          existingChildren.forEach(child => deleteChild(returnFiber, child));
        }
    
        return resultingFirstChild;
      }