Рукописная архитектура React's Fiber, глубокое понимание ее принципов

внешний интерфейс React.js

Друзья, знакомые с React, знают, что React поддерживает синтаксис jsx. Мы можем напрямую писать HTML-код в середине JS, а затем отображать его на странице. Если HTML-код, который мы пишем, обновляется, React также имеет сравнение виртуального DOM, и обновляет только измененный раздел без повторного рендеринга всей страницы, что значительно повышает эффективность рендеринга. В 16.x React использует метод, называемыйFiberАрхитектура улучшает пользовательский опыт, а также вводитhooksи другие характеристики. Так в чем же принцип React?Fiberа такжеhooksКак это достигается? Эта статья начнется сjsxДля начала напишите упрощенную версию React, чтобы глубоко понять принцип React.

В этой статье в основном реализованы следующие функции:

Простая оптоволоконная архитектура

Простая версия алгоритма DIFF

Простой функциональный компонент

Простой крючок:useState

Развлекательное изданиеClassкомпоненты

Кодовый адрес этой статьи:GitHub.com/Денис — см....

Эта программа работает следующим образом:

Jun-19-2020 17-01-28

JSX и createElement

Прежде чем мы напишем React Для поддержки JSX также понадобится библиотека под названиемJSXTransformer.js, а позже работа по преобразованию JSX была интегрирована в babel, и babel также предоставилОсобенности онлайн-просмотра, вы можете увидеть преобразованный эффект, например следующий простой код:

const App =
(
  <div>
    <h1 id="title">Title</h1>
    <a href="xxx">Jump</a>
    <section>
      <p>
        Article
      </p>
    </section>
  </div>
);

После преобразования babel это становится таким:

image-20200608175937104

На приведенном выше снимке экрана видно, что написанный нами HTML-код был преобразован вReact.createElement, давайте немного отформатируем приведенный выше код:

var App = React.createElement(
  'div',
  null,
  React.createElement(
    'h1',
    {
      id: 'title',
    },
    'Title',
  ),
  React.createElement(
    'a',
    {
      href: 'xxx',
    },
    'Jump',
  ),
  React.createElement(
    'section',
    null,
    React.createElement('p', null, 'Article'),
  ),
);

Из кода преобразования мы видимReact.createElementПоддерживается несколько параметров:

  1. тип, который является типом узла
  2. config, который является свойством узла, напримерidа такжеhref
  3. дочерние элементы, начиная с третьего параметра, все являются дочерними элементами, то есть дочерними элементами, дочерних элементов может быть несколько, а тип может быть простым текстовым илиReact.createElement,еслиReact.createElement, по сути это дочерний узел, и под дочерним узлом тоже могут быть дочерние узлы. так что используйтеReact.createElementВложенная связь реализует древовидную структуру узлов HTML.

Давайте полностью рассмотрим этот простой код страницы React:

image-20200608180112829

Отображается на странице так:

image-20200608180139663

На самом деле есть два места, где используется React: одно — JSX, другое —React.createElement, другойReactDOM.render, так что у нас есть первая цель почерка, которыйcreateElementа такжеrenderэти два метода.

рукописный

для<h1 id="title">Title</h1>Для такого простого узла собственный DOM также прикрепит к нему множество атрибутов и методов, так что мы находимся вcreateElementЛучше всего преобразовать ее в более простую структуру данных, содержащую только нужные нам элементы, например такую:

{
  type: 'h1',
  props: {
    id: 'title',
    children: 'Title'
  }
}

С этой структурой данных наши операции над DOM фактически могут быть преобразованы в операции над этой структурой данных, а сравнение между старой и новой DOM также может быть преобразовано в сравнение этой структуры данных, так что нам не нужно визуализировать каждую операцию.Страница, но структура данных визуализируется на странице, когда это необходимо. На самом деле это виртуальный DOM! и мыcreateElementЭто метод, ответственный за построение этого виртуального дома. Давайте реализовать его ниже:

function createElement(type, props, ...children) {
  // 核心逻辑不复杂,将参数都塞到一个对象上返回就行
  // children也要放到props里面去,这样我们在组件里面就能通过this.props.children拿到子元素
  return {
    type,
    props: {
      ...props,
      children
    }
  }
}

Реагируйте на приведенный выше кодcreateElementУпрощенная версия, друзья, которым интересен исходный код, могут посмотреть здесь:GitHub.com/Facebook/Горячие…

рукописный рендер

Для приведенного выше кода мы используемcreateElementПреобразуйте код JSX в виртуальный DOM, функция, которая фактически отображает его на странице,render, так что нам также нужно реализовать этот метод через наше общее использованиеReactDOM.render( <App />,document.getElementById('root'));Может быть известно, что он получает два параметра:

  1. Корневой компонент на самом деле является компонентом JSX, т.е.createElementВозвращенный виртуальный DOM
  2. Родительский узел, где мы хотим визуализировать этот виртуальный DOM.

С этими двумя параметрами давайте реализуем следующееrenderметод:

function render(vDom, container) {
  let dom;
  // 检查当前节点是文本还是对象
  if(typeof vDom !== 'object') {
    dom = document.createTextNode(vDom)
  } else {
    dom = document.createElement(vDom.type);
  }

  // 将vDom上除了children外的属性都挂载到真正的DOM上去
  if(vDom.props) {
    Object.keys(vDom.props)
      .filter(key => key != 'children')
      .forEach(item => {
        dom[item] = vDom.props[item];
      })
  }
  
  // 如果还有子元素,递归调用
  if(vDom.props && vDom.props.children && vDom.props.children.length) {
    vDom.props.children.forEach(child => render(child, dom));
  }

  container.appendChild(dom);
}

Приведенный выше код является упрощенной версиейrenderспособ, друзья, интересующиеся исходным кодом, могут посмотреть здесь:GitHub.com/Facebook/Горячие…

Теперь мы можем написать свой собственныйcreateElementа такжеrenderчтобы заменить собственный метод:

image-20200608180301596

Вы можете получить тот же результат рендеринга:

image-20200608180139663

Зачем вам нужно волокно

Выше мы просто реализовали код, который рендерит виртуальный DOM на страницу. Эта часть работы официально называется рендерером React. Рендерер — это модуль, который третьи стороны могут реализовать сами. Существует также основной модуль, называемый reconsiler. Функции реконсилера: Всем известный diff будет вычислять, какие узлы страницы следует обновить, а затем передавать виртуальный DOM узлов, которые необходимо обновить, рендереру, который отвечает за отрисовку этих узлов на страницу. Но с этим процессом есть проблема: хотя алгоритм diff в React оптимизирован, он синхронный, а за манипуляции с DOM отвечает рендерер.appendChildAPI также является синхронным, то есть при наличии большого количества узлов, которые необходимо обновить, время выполнения JS-потока может быть относительно большим, в это время браузер не будет реагировать на другие события, поскольку поток JS и поток графического интерфейса пользователя являются взаимоисключающими, страница не будет отвечать во время работы JS. Если это время слишком велико, пользователь может увидеть заикание, особенно заикание анимации будет очевидным. существуетОфициальный доклад ReactЕсть пример в , вы можете ясно увидеть заикание, вызванное этим синхронным вычислением:

1625d95bc100c7fe

Для решения этой проблемы используется Fiber.Fiber может разбивать долгосрочные задачи синхронизации на несколько небольших задач, так что браузер может отключиться, чтобы реагировать на другие события, а затем вернуться, чтобы продолжить вычисления, когда он будет пуст, так что все вычисления Процесс выглядит гораздо более плавным. Ниже приводится эффект после использования Fiber:

1625d95bc2baf0e1

как разделить

Вышеупомянутое мы реализовали самиrenderМетод напрямую рекурсивно обходит все дерево vDom, если мы остановимся на каком-то шаге посередине, то при следующем вызове мы не знаем, где остановились в прошлый раз, и не знаем, с чего начать, даже если вы записываете последний конечный узел, когда он сходит, вы не знаете, какой из них выполнять следующим, поэтому древовидная структура vDom не удовлетворяет потребности паузы в середине и продолжения в следующий раз, а структура данных нуждается быть изменены. Еще одна проблема, которую необходимо решить, — когда будут выполняться разделенные задачи? Наша цель — сделать работу пользователей более плавной, поэтому нам лучше не блокировать высокоприоритетные задачи, такие как пользовательский ввод, анимация и т. д., а ждать их завершения, прежде чем производить расчеты. Итак, как мне узнать, есть ли высокоприоритетные задачи и не простаивает ли браузер? Подводя итог, чтобы волокно достигло своей цели, ему необходимо решить две проблемы:

  1. Новое планирование задач, когда есть задачи с высоким приоритетом, браузер будет освобожден, а выполнение продолжится, когда браузер пуст.
  2. Новую структуру данных можно прервать в любой момент, и при следующем входе можно продолжить выполнение

requestIdleCallback

requestIdleCallbackЭто экспериментальный новый API, который называется следующим образом:

// 开启调用
var handle = window.requestIdleCallback(callback[, options])

// 结束调用
Window.cancelIdleCallback(handle) 

requestIdleCallbackПолучите обратный вызов, который будет вызываться при бездействии браузера, каждый вызов будет проходить вIdleDeadline, вы можете узнать, как долго это в настоящее время бесплатно,optionsВы можете передать параметр, как долго ждать, и он будет применяться, когда браузер не пуст. Использование этого API может решить проблему планирования задач, позволяя браузеру вычислять и отображать разницу, когда он бездействует.Дополнительную информацию об использовании requestIdleCallback можно найти в документации MDN.Но этот API все еще находится в стадии эксперимента, совместимость не очень хорошая,Итак, React официально реализовал собственный набор. Эта статья будет продолжать использоватьсяrequestIdleCallbackЧтобы выполнить планирование задач, наша идея планирования задач состоит в том, чтобы разделить задачу на несколько небольших задач,requestIdleCallbackНебольшие задачи постоянно вынимаются и выполняются.Когда все задачи будут выполнены или истечет время ожидания, выполнение завершится, и следующее выполнение должно быть зарегистрировано.Полка кода выглядит следующим образом:

function workLoop(deadline) {
  while(nextUnitOfWork && deadline.timeRemaining() > 1) {
    // 这个while循环会在任务执行完或者时间到了的时候结束
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  // 如果任务还没完,但是时间到了,我们需要继续注册requestIdleCallback
  requestIdleCallback(workLoop);
}

// performUnitOfWork用来执行任务,参数是我们的当前fiber任务,返回值是下一个任务
function performUnitOfWork(fiber) {
  
}
requestIdleCallback(workLoop);

вышесказанноеworkLoopСм. здесь исходный код React.

Прерываемые структуры данных оптоволокна

выше нашегоperformUnitOfWorkЭто не реализовано, но из вышеприведенной структуры видно, что параметр, который он получает, является маленькой задачей, и через эту маленькую задачу он также может найти свою следующую маленькую задачу.Fiber строит такую ​​структуру данных. Структура данных до Fiber представляет собой дерево, родительский узелchildrenОн указывает на дочерний узел, но только этот указатель не может добиться продолжения прерывания. Например, теперь у меня есть родительский узел A, у A есть три дочерних узла B, C, D. Когда я перехожу к C, он прерывается. Когда я начинаю снова, я фактически не знаю, какой из них выполнять под C, потому что я знаю только C , и нет ни указателя на его родителя, ни указателя на его родного брата. Fiber преобразует такую ​​структуру и добавляет указатели на родительские и одноуровневые узлы:

image-20200609173312276

Вышеприведенная картинка все еще из официальной речи.Вы можете видеть, что в отличие от предыдущего родительского узла, указывающего на все дочерние узлы, есть три указателя:

  1. child: родительский узел указывает напервый дочерний элементуказатель.
  2. sibling: Указывает на следующий родственный элемент после первого дочернего элемента.
  3. return: указатель на родительский элемент, который есть у всех дочерних элементов.

С помощью этих указателей мы можем прервать обход и возобновить его на любом элементе, например, как на рисунке выше.ListЕсли оно было прервано, его можно восстановить,childНайдите его дочерние элементы, такжеreturnНайдите его родительский элемент, если у него есть родственные узлы, его тоже можно использоватьsiblingоказаться. Структура Fiber выглядит как дерево, но в ней нет указателя на все дочерние элементы.Родительский узел указывает только на первый дочерний узел, а затем у дочернего узла есть указатели на другие дочерние узлы.На самом деле это связный список.

Реализовать волокно

Теперь мы можем реализовать Fiber самостоятельно, нам нужно преобразовать предыдущую структуру vDom в структуру данных Fiber, и в то же время нам нужно иметь возможность вернуться к следующему узлу через любой из узлов, который фактически пересекает связанный список. При обходе, начиная с корневого узла, сначала найдите дочерний элемент, если дочерний элемент существует, вернитесь напрямую, если дочернего элемента нет, найдите родственный элемент, после нахождения всех одноуровневых элементов вернитесь к родительскому элементу и затем найдите родительский элемент Sibling element. Весь процесс обхода на самом деле представляет собой обход в глубину сверху вниз, а затем последняя строка начинает обход слева направо. Например, следующий рисунок изdiv1Если вы начинаете обход, порядок обхода должен бытьdiv1 -> div2 -> h1 -> a -> div2 -> p -> div1. Как видно из этой последовательности, когда мыreturnКогда родительские узлы, эти родительские узлы будут проходиться во второй раз, поэтому, когда мы пишем код,returnРодительский узел не возвращается как следующая задача, толькоsiblingа такжеchildвернется в качестве следующей задачи.

image-20200610162336915

// performUnitOfWork用来执行任务,参数是我们的当前fiber任务,返回值是下一个任务
function performUnitOfWork(fiber) {
  // 根节点的dom就是container,如果没有这个属性,说明当前fiber不是根节点
  if(!fiber.dom) {
    fiber.dom = createDom(fiber);   // 创建一个DOM挂载上去
  } 

  // 如果有父节点,将当前节点挂载到父节点上
  if(fiber.return) {
    fiber.return.dom.appendChild(fiber.dom);
  }

  // 将我们前面的vDom结构转换为fiber结构
  const elements = fiber.children;
  let prevSibling = null;
  if(elements && elements.length) {
    for(let i = 0; i < elements.length; i++) {
      const element = elements[i];
      const newFiber = {
        type: element.type,
        props: element.props,
        return: fiber,
        dom: null
      }

      // 父级的child指向第一个子元素
      if(i === 0) {
        fiber.child = newFiber;
      } else {
        // 每个子元素拥有指向下一个子元素的指针
        prevSibling.sibling = newFiber;
      }

      prevSibling = newFiber;
    }
  }

  // 这个函数的返回值是下一个任务,这其实是一个深度优先遍历
  // 先找子元素,没有子元素了就找兄弟元素
  // 兄弟元素也没有了就返回父元素
  // 然后再找这个父元素的兄弟元素
  // 最后到根节点结束
  // 这个遍历的顺序其实就是从上到下,从左到右
  if(fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;
  while(nextFiber) {
    if(nextFiber.sibling) {
      return nextFiber.sibling;
    }

    nextFiber = nextFiber.return;
  }
}

Реагировать на исходный кодperformUnitOfWorkПосмотрите, здесь, конечно, намного сложнее, чем у нас.

Унифицированная манипуляция DOM фиксации

выше нашегоperformUnitOfWorkСтруктура волокна при создании операционной боковой домеappendChild, поэтому если одновременно обновляются несколько узлов, и операция прерывается после первого узла, то мы можем видеть только первый узел, отображаемый на странице, а последующие узлы будут отображаться один за другим, когда браузер пуст. Чтобы избежать этой ситуации, мы должны собрать все операции DOM и, наконец, выполнить их единообразно, чтоcommit. Чтобы иметь возможность записывать местоположение, нам также нужна глобальная переменнаяworkInProgressRootдля записи корневого узла, а затем вworkLoopПроверьте, завершена ли задача, затемcommit:

function workLoop(deadline) {
  while(nextUnitOfWork && deadline.timeRemaining() > 1) {
    // 这个while循环会在任务执行完或者时间到了的时候结束
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  // 任务做完后统一渲染
  if(!nextUnitOfWork && workInProgressRoot) {
    commitRoot();
  }

  // 如果任务还没完,但是时间到了,我们需要继续注册requestIdleCallback
  requestIdleCallback(workLoop);
}

Потому что мы выполняем его после того, как дерево Fiber будет полностью построено.commit, и есть переменнаяworkInProgressRootУказывает на корневой узел Fiber, поэтому мы можем напрямую поместитьworkInProgressRootПросто возьмите его и визуализируйте рекурсивно:

// 统一操作DOM
function commitRoot() {
  commitRootImpl(workInProgressRoot.child);    // 开启递归
  workInProgressRoot = null;     // 操作完后将workInProgressRoot重置
}

function commitRootImpl(fiber) {
  if(!fiber) {
    return;
  }

  const parentDom = fiber.return.dom;
  parentDom.appendChild(fiber.dom);

  // 递归操作子元素和兄弟元素
  commitRootImpl(fiber.child);
  commitRootImpl(fiber.sibling);
}

примирение

Согласование на самом деле является операцией сравнения виртуального дерева DOM, в которой необходимо удалить ненужные узлы, обновить измененные узлы и добавить новые узлы. Чтобы иметь возможность вернуться к работе после перерыва, нам также нужна переменнаяcurrentRoot, затем вfiberДобавить атрибут к узлуalternate, этот атрибут указывает на корневой узел последнего запуска, то естьcurrentRoot.currentRootбудет первымrenderПослеcommitПоэтапное задание, то есть после каждого расчета текущее состояние будет записано вalternateвверх, вы можете обновить его позжеalternateВыньте его и сделайте diff с новым состоянием. потомperformUnitOfWorkВам нужно добавить код согласования подэлементов, вы можете добавить функциюreconcileChildren. Эта функция не может просто создать новый узел, а сравнить старый узел с новым узлом, логика сравнения следующая:

  1. Если старый и новый типы узлов совпадают, повторно используйте DOM старого узла и обновите реквизиты.
  2. Если типы разные и новый узел существует, создайте новый узел, чтобы заменить старый узел.
  3. Если тип другой, нового узла нет, есть старый узел, удалить старый узел

Обратите внимание, что операция удаления старого узла заключается в непосредственном удалении старого узла.oldFiberПросто добавьте маркер удаления и одновременно используйте глобальную переменную.deletionsЗапишите все узлы, которые необходимо удалить:

      // 对比oldFiber和当前element
      const sameType = oldFiber && element && oldFiber.type === element.type;  //检测类型是不是一样
      // 先比较元素类型
      if(sameType) {
        // 如果类型一样,复用节点,更新props
        newFiber = {
          type: oldFiber.type,
          props: element.props,
          dom: oldFiber.dom,
          return: workInProgressFiber,
          alternate: oldFiber,          // 记录下上次状态
          effectTag: 'UPDATE'           // 添加一个操作标记
        }
      } else if(!sameType && element) {
        // 如果类型不一样,有新的节点,创建新节点替换老节点
        newFiber = {
          type: element.type,
          props: element.props,
          dom: null,                    // 构建fiber时没有dom,下次perform这个节点是才创建dom
          return: workInProgressFiber,
          alternate: null,              // 新增的没有老状态
          effectTag: 'REPLACEMENT'      // 添加一个操作标记
        }
      } else if(!sameType && oldFiber) {
        // 如果类型不一样,没有新节点,有老节点,删除老节点
        oldFiber.effectTag = 'DELETION';   // 添加删除标记
        deletions.push(oldFiber);          // 一个数组收集所有需要删除的节点
      }

затем вcommitЭтап обрабатывает настоящие манипуляции с DOM, конкретные манипуляции основаны на нашихeffectTagсудить:

function commitRootImpl(fiber) {
  if(!fiber) {
    return;
  }

  const parentDom = fiber.return.dom;
  if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) {
    parentDom.appendChild(fiber.dom);
  } else if(fiber.effectTag === 'DELETION') {
    parentDom.removeChild(fiber.dom);
  } else if(fiber.effectTag === 'UPDATE' && fiber.dom) {
    // 更新DOM属性
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

  // 递归操作子元素和兄弟元素
  commitRootImpl(fiber.child);
  commitRootImpl(fiber.sibling);
}

Операции DOM по замене и удалению относительно просты, а обновить атрибуты чуть хлопотнее, и нужно написать вспомогательную функциюupdateDomреализовать:

// 更新DOM的操作
function updateDom(dom, prevProps, nextProps) {
  // 1. 过滤children属性
  // 2. 老的存在,新的没了,取消
  // 3. 新的存在,老的没有,新增
  Object.keys(prevProps)
    .filter(name => name !== 'children')
    .filter(name => !(name in nextProps))
    .forEach(name => {
      if(name.indexOf('on') === 0) {
        dom.removeEventListener(name.substr(2).toLowerCase(), prevProps[name], false);
      } else {
        dom[name] = '';
      }
    });

  Object.keys(nextProps)
    .filter(name => name !== 'children')
    .forEach(name => {
      if(name.indexOf('on') === 0) {
        dom.addEventListener(name.substr(2).toLowerCase(), nextProps[name], false);
      } else {
        dom[name] = nextProps[name];
      }
    });
}

updateDomКод относительно прост для написания, и событие обрабатывает только простыеonВ начале тоже бывают проблемы с совместимостью.prevPropsа такжеnextPropsОдни и те же свойства могут быть пройдены, и есть повторяющиеся присваивания, но общий принцип остается правильным. Чтобы написать всю эту обработку, объем кода еще очень большой.

функциональный компонент

Функциональный компонент — очень распространенный компонент в React.Архитектура React перед нами фактически написана.Давайте поддержим здесь функциональный компонент. наш предыдущийfiberна узлеtypeвсе типы узлов DOM, такие какh1что, а узел функционального компонентаtypeПо сути, это функция, и с таким узлом нам нужно разобраться отдельно.

Прежде всего, необходимо определить, является ли текущий узел функциональным компонентом при обновлении, и если да, тоchildrenЛогика обработки будет немного другой:

// performUnitOfWork里面
// 检测函数组件
function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  if(isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }
  
  // ...下面省略n行代码...
}

function updateFunctionComponent(fiber) {
  // 函数组件的type就是个函数,直接拿来执行可以获得DOM元素
  const children = [fiber.type(fiber.props)];

  reconcileChildren(fiber, children);
}

// updateHostComponent就是之前的操作,只是单独抽取了一个方法
function updateHostComponent(fiber) {
  if(!fiber.dom) {
    fiber.dom = createDom(fiber);   // 创建一个DOM挂载上去
  } 

  // 将我们前面的vDom结构转换为fiber结构
  const elements = fiber.props.children;

  // 调和子元素
  reconcileChildren(fiber, elements);
}

Затем, когда мы отправляем операцию DOM, поскольку компонент функции не имеет элемента DOM, нам нужно обратить внимание на два момента:

  1. При получении родительского элемента DOM вам необходимо рекурсивно найти настоящий DOM онлайн.
  2. При удалении узла нужно рекурсивно спуститься вниз, чтобы найти настоящий узел

Давайте изменимcommitRootImpl:

function commitRootImpl() {
  // const parentDom = fiber.return.dom;
  // 向上查找真正的DOM
  let parentFiber = fiber.return;
  while(!parentFiber.dom) {
    parentFiber = parentFiber.return;
  }
  const parentDom = parentFiber.dom;
  
  // ...这里省略n行代码...
  
  if{fiber.effectTag === 'DELETION'} {
    commitDeletion(fiber, parentDom);
  }
}

function commitDeletion(fiber, domParent) {
  if(fiber.dom) {
    // dom存在,是普通节点
    domParent.removeChild(fiber.dom);
  } else {
    // dom不存在,是函数组件,向下递归查找真实DOM
    commitDeletion(fiber.child, domParent);
  }
}

Теперь мы можем передать функциональный компонент:

import React from './myReact';
const ReactDOM = React;

function App(props) {
  return (
    <div>
      <h1 id="title">{props.title}</h1>
      <a href="xxx">Jump</a>
      <section>
        <p>
          Article
        </p>
      </section>
    </div>
  );
}

ReactDOM.render(
  <App title="Fiber Demo"/>,
  document.getElementById('root')
);

Достичь состояния использования

useStateЭто API в React Hooks, который эквивалентен предыдущемуClass Componentвнутриstate, используемый для управления внутренним состоянием компонента, и теперь у нас есть упрощенная версияReactТеперь мы также можем попробовать реализовать этот API.

Простая версия

Мы по-прежнему начинаем с использования для достижения простейшей функции, мы обычно используемuseStateТакова, что:

function App(props) {
  const [count, setCount] = React.useState(1);
  const onClickHandler = () => {
    setCount(count + 1);
  }
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={onClickHandler}>Count+1</button>
    </div>
  );
}

ReactDOM.render(
  <App title="Fiber Demo"/>,
  document.getElementById('root')
);

Как видно из кода выше, нашuseStateПолучает начальное значение и возвращает массив с этимstateтекущее значение и изменениеstateметод, следует отметить, чтоAppВ качестве функционального компонента каждый разrenderбудет запускаться каждый раз, то есть локальные переменные вrenderбудет сбрасываться каждый раз, то нашstateЕе нельзя использовать как локальную переменную, но ее следует хранить как глобальную переменную:

let state = null;
function useState(init) {

  state = state === null ? init : state;

  // 修改state的方法
  const setState = value => {
    state = value;

    // 只要修改了state,我们就需要重新处理节点
    workInProgressRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }

    // 修改nextUnitOfWork指向workInProgressRoot,这样下次就会处理这个节点了
    nextUnitOfWork = workInProgressRoot;
    deletions = [];
  }

  return [state, setState]
}

Таким образом, мы можем использовать:

Jun-19-2020 15-16-04

Поддержка нескольких состояний

Вышеприведенный код имеет только одинstateпеременная, если у нас есть несколькоuseStateКак это сделать? Для поддержки несколькихuseState,нашstateЭто не может быть простое значение, мы можем рассмотреть возможность его преобразования в массив, несколькоuseStateПоместите его в этот массив в соответствии с порядком вызова и получите доступ к нему по индексу при доступе:

let state = [];
let hookIndex = 0;
function useState(init) {
  const currentIndex = hookIndex;
  state[currentIndex] = state[currentIndex] === undefined ? init : state[currentIndex];

  // 修改state的方法
  const setState = value => {
    state[currentIndex] = value;

    // 只要修改了state,我们就需要重新处理这个节点
    workInProgressRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }

    // 修改nextUnitOfWork指向workInProgressRoot,这样下次就会处理这个节点了
    nextUnitOfWork = workInProgressRoot;
    deletions = [];
  }

  hookIndex++;

  return [state[currentIndex], setState]
}

увидеть большеuseStateЭффект:

Jun-19-2020 15-28-59

Поддержка нескольких компонентов

Приведенный выше код, хотя мы поддерживаем несколькоuseState, но по-прежнему есть только один набор глобальных переменных.Если есть несколько функциональных компонентов, и каждый компонент работает с этой глобальной переменной, не загрязняет ли это данные между собой? Таким образом, наши данные не могут все существовать в глобальных переменных, но должны существовать в каждой из них.fiberУзел, тогда этот узел, занимающийся состоянием в глобальной переменной, используется для связи:

// 申明两个全局变量,用来处理useState
// wipFiber是当前的函数组件fiber节点
// hookIndex是当前函数组件内部useState状态计数
let wipFiber = null;
let hookIndex = null;

потому чтоuseStateДоступно только в функциональных компонентах, поэтому наш предыдущийupdateFunctionComponentЕго нужно инициализироватьuseStateПеременная:

function updateFunctionComponent(fiber) {
  // 支持useState,初始化变量
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];        // hooks用来存储具体的state序列
  
  // ......下面代码省略......
}

потому чтоhooksочередьfiberУзел вверх, так что мыuseStateПри взятии предыдущего значения нужно начинать сfiber.alternateВозьмите приведенное выше, полный код выглядит следующим образом:

function useState(init) {
  // 取出上次的Hook
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];

  // hook数据结构
  const hook = {
    state: oldHook ? oldHook.state : init      // state是每个具体的值
  }

  // 将所有useState调用按照顺序存到fiber节点上
  wipFiber.hooks.push(hook);
  hookIndex++;

  // 修改state的方法
  const setState = value => {
    hook.state = value;

    // 只要修改了state,我们就需要重新处理这个节点
    workInProgressRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }

    // 修改nextUnitOfWork指向workInProgressRoot,这样下次requestIdleCallback就会处理这个节点了
    nextUnitOfWork = workInProgressRoot;
    deletions = [];
  }

  return [hook.state, setState]
}

Приведенный выше код показывает, что мы помещаемuseStateи хранитсяstateиспользуется при сопоставленииuseStateПоследовательность вызова соответствуетstateИндекс , если этот индекс не совпадает,stateнеправильно, так чтоReactТакой код не может появиться в нем:

if (something) {
    const [state, setState] = useState(1);
}

Приведенный выше код не гарантирует, что каждый разsomethingудовлетворены, может привести кuseStateэтот разrenderОн выполняется, но не будет выполняться в следующий раз, поэтому индексы нового и старого узлов не будут совпадать.Для такого кодаReactОб ошибке будет сообщено напрямую:

image-20200619161005858

Компоненты Mock Class с хуками

Эта функция носит исключительно развлекательный характер. Компонент Class моделируется и реализуется с помощью ранее реализованных хуков.ReactОфициальная реализация ha~ Мы можем написать метод для преобразования компонента класса в предыдущий компонент функции:

function transfer(Component) {
  return function(props) {
    const component = new Component(props);
    let [state, setState] = useState(component.state);
    component.props = props;
    component.state = state;
    component.setState = setState;

    return component.render();
  }
}

Затем вы можете написать класс. Этот класс выглядит как класс, который мы написали в React.state,setStateа такжеrender:

import React from './myReact';

class Count4 {
  constructor(props) {
    this.props = props;
    this.state = {
      count: 1
    }
  }

  onClickHandler = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
        <h3>Class component Count: {this.state.count}</h3>
        <button onClick={this.onClickHandler}>Count+1</button>
      </div>
    ); 
  }
}

// export的时候用transfer包装下
export default React.transfer(Count4);

Затем при непосредственном использовании:

<div>
  <Count4></Count4>
</div>

Конечно, вы также можетеReactСоздать пустойclass Component,ПозволятьCount4Унаследуйте его, и это будет больше похоже на это.

Хорошо, поехали сюда, наш код готов, и полный код можно посмотреть у меня на Github.

Суммировать

  1. Код JSX, который мы написали, был преобразован Babel вReact.createElement.
  2. React.createElementТо, что возвращается, на самом деле является виртуальной структурой DOM.
  3. ReactDOM.renderМетод заключается в отображении виртуального DOM на странице.
  4. Согласование и рендеринг виртуального DOM могут быть простыми и грубо рекурсивными, но этот процесс является синхронным.Если необходимо обработать слишком много узлов, он может заблокировать пользовательский ввод и воспроизведение анимации, что приведет к зависаниям.
  5. Волокна - это новая функция, представленная в 16.x, которая используется для превращения синхронного примирения в асинхронные.
  6. Fiber преобразует структуру виртуального DOM,父 -> 第一个子,子 -> 兄,子 -> 父Эти указатели с помощью этих указателей могут найти другие узлы из любого узла Fiber.
  7. Fiber разбивает синхронные задачи всего дерева на асинхронные структуры выполнения, которые каждый узел может выполнять независимо.
  8. Волокно может пройти из любого узла, обход в глубину, порядок父 -> 子 -> 兄 -> 父Это сверху вниз, слева направо.
  9. Асинхронная задача фазы гармоники оптоволокна может быть небольшой, но фаза фиксации (commit) должен быть синхронным. из-за асинхронностиcommitЭто может позволить пользователям видеть, что узлы появляются один за другим, и это не очень хорошо.
  10. Функциональный компонент на самом деле является этим узломtypeэто функция, которая напрямую преобразуетtypeВы можете получить виртуальный DOM, запустив его.
  11. useStateВолоконный узел добавляется в массиве, каждое значение, которое соответствует массивуuseState,useStateПорядок вызова должен соответствовать индексу этого массива, иначе будет сообщено об ошибке.

использованная литература

A Cartoon Intro to Fiber

Замечательный классный руководитель: рукописная архитектура React’s Fiber and Hooks

React Fiber

Это, пожалуй, самый распространенный способ открыть React Fiber (разрезка по времени)

Анализ реактивного волокна

Архитектура React Fibre

В конце статьи спасибо, что потратили свое драгоценное время на чтение этой статьи. Если эта статья немного поможет вам или вдохновит, пожалуйста, не скупитесь на лайки и звезды GitHub. Ваша поддержка является движущей силой для автор продолжать творить.

Добро пожаловать, чтобы обратить внимание на мой общедоступный номербольшой фронт атакиПолучите высококачественные оригиналы впервые~

Цикл статей "Передовые передовые знания":nuggets.capable/post/684490…

Адрес GitHub с исходным кодом из серии статей «Advanced Front-end Knowledge»:GitHub.com/Денис — см....