Preact: Самостоятельное изготовление запаски

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

Первоначально из колонки Чжиху:zhuanlan.zhihu.com/ne-fe

Некоторое время назад из-за проблемы с лицензией React команда активно изучала альтернативы React, и с учетом возможного мобильного бизнеса в будущем цель команды — найти альтернативный продукт с низкой стоимостью миграции и небольшим объемом. После многих исследований Preact попал в поле нашего зрения. С тех пор, как я вступил в контакт с Preact, я потерял много волос и много думал на этом пути.Я хотел бы представить вам идею реализации Preact, а также поделиться своими мыслями.

Что такое преакт

Несколько слов о Preact, облегченной альтернативе React размером 3 КБ с тем же API ES6. Если мне покажется, что это предложение слишком расплывчато, я могу добавить еще несколько слов. Preact = перформанс + реакция, отсюда и название Preact, одного перфоманса достаточно, чтобы увидеть замысел автора. На следующей картинке показана производительность разных фреймворков в сценарии инициализации длинных списков, видно, что Preact действительно работает хорошо.

Высокая производительность, малый вес, мгновенное производство — вот суть Preact. Основываясь на этих темах, Preact сосредоточился на основной функции React, реализовал простой и прогнозирующий алгоритм DIFF, чтобы сделать его самой быстрой виртуальной DOM-платформой, а preact-compat обеспечивает гарантию совместимости, делая preact бесшовной стыковкой большого количества компонентов в Экологию React, но также дополняющую множество функций, которые PREACT не реализует.

长列表初始化时间对比
Сравнение времени инициализации длинного списка

Рабочий процесс Preact

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

  • component
  • h функция
  • render
  • алгоритм сравнения
  • механизм рециркуляции

Процесс потока показан на рисунке ниже.

Первый компонент, который мы определили, при запуске рендеринга сначала войдет в функцию h для генерации соответствующего виртуального узла (если он написан на JSX, перед этим требуется один шаг транскодирования). Каждый vnode содержит информацию о своем собственном узле и информацию о его дочерних узлах, которые связаны для формирования дерева виртуального дома. На основе сгенерированного vnode модуль рендеринга будет управлять потоком в сочетании с текущим деревом DOM и делать некоторые приготовления для последующих операций сравнения. Реализация алгоритма сравнения Preact отличается от идеи реакции на основе двойных деревьев виртуальных домов.Preact поддерживает только новое дерево виртуальных домов.Во время процесса сравнения старое дерево виртуальных домов будет восстановлено на основе дерева домов, а затем два будут сравниваться И во время процесса сравнения операция исправления выполняется в дереве DOM в режиме реального времени, и, наконец, создается новое дерево DOM. В то же время компоненты и узлы, удаленные в процессе сравнения, не будут удалены напрямую, а будут кэшироваться в пуле утилизации соответственно. найдены в бассейне переработки.Элементы модернизируются, чтобы избежать накладных расходов на строительство с нуля.

Preact工作流程图
Блок-схема предварительной работы

После понимания рабочего процесса Preact мы будем интерпретировать пять модулей, упомянутых выше, один за другим.

Component

Ключевые слова:ловушка, linkState, пакетное обновление

Я полагаю, что студенты, имеющие опыт разработки на React, не будут незнакомы с концепцией компонента, поэтому я не буду здесь слишком много объяснять, а лишь представлю некоторые новые функции, добавленные Preact на уровне компонентов.

функция ловушки

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

  • afterMount
  • afterUpdate
  • beforeUnmount

linkState

Сценарий для linkState заключается в том, чтобы привязать this к обратному вызову пользовательской операции в методе рендеринга, чтобы каждый рендеринг создавал замыкание функции локально, что очень неэффективно и заставляет сборщик мусора выполнять много ненужной работы. Идеальный сценарий применения linkState выглядит следующим образом.

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      text: 'initial'
    }
  }

  handleChange = e => {
    this.setState({
      text: e.target.value
    })
  }

  render({desc}, {text}} {
    return (
      <div>
        <input value={text} onChange={this.linkState('text', 'target.value')}>
        <div>{text}</div>
      </div>
    )
  }
}

Однако linkState реализован. . . Это создание закрытия для каждого обратного вызова при инициализации компонента, привязка этого и создание свойства экземпляра для кэширования функции обратного вызова после привязки, чтобы его не нужно было связывать снова при повторном рендеринге. Фактический эффект эквивалентен связыванию в конструкторе компонента. Смущает то, что linkState реализует операцию setState только внутри и не поддерживает пользовательские параметры, поэтому сценарии использования относительно ограничены.

//linkState源码
//缓存回调
linkState(key, eventPath) {
  let c = this._linkedStates || (this._linkedStates = {});
  return c[key+eventPath] || (c[key+eventPath] = createLinkedState(this, key, eventPath));
}

//首次注册回调的时候创建闭包
export function createLinkedState(component, key, eventPath) {
  let path = key.split('.');
  return function(e) {
    let t = e && e.target || this,
      state = {},
      obj = state,
      v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? (t.type.match(/^che|rad/) ? t.checked : t.value) : e,
      i = 0;
    for ( ; i<path.length-1; i++) {
      obj = obj[path[i]] || (obj[path[i]] = !i && component.state[path[i]] || {});
    }
    obj[path[i]] = v;
    component.setState(state);
  };
}

Массовое обновление

Preact реализует пакетное обновление компонентов.Конкретная идея реализации заключается в том, что каждый раз, когда выполняется обновление состояния или реквизита, соответствующее свойство будет обновляться немедленно, но операция рендеринга на основе нового состояния или реквизита будет помещена в очередь обновления. Операции в очереди будут выполняться одна за другой в конце текущего цикла событий или в начале следующего цикла событий. Множественные обновления одного и того же состояния компонента не будут поступать в очередь повторно. Как показано на рисунке ниже, после обновления атрибута и перед отрисовкой компонента значение _dirty равно true, поэтому последующая операция обновления атрибута перед отрисовкой компонента не приведет к повторной постановке компонента в очередь.

//更新队列源码
export function enqueueRender(component) {
  if (!component._dirty && (component._dirty = true) && items.push(component)==1) {
    (options.debounceRendering || defer)(rerender);
  }
}

h функция

Ключевые слова:Слияние узлов

Функция h действует как React.CreateElement и используется для создания виртуальных узлов. Входной формат, который он принимает, следующий: три параметра: тип узла, атрибут узла и дочерний элемент.

h('a', { href: '/', h{'span', null, 'Home'}})

Слияние узлов

В процессе генерации vnode функция h объединит соседние простые узлы, цель — уменьшить количество узлов и снизить нагрузку на diff. См. пример ниже.

import { h, Component } from 'preact';
const innerinnerchildren = [['innerchild2', 'innerchild3'], 'innerchild4'];
const innerchildren = [
  <div>
    {innerinnerchildren}
  </div>,
  <span>desc</span>
]

export default class App extends Component {
  render() {
    return (
      <div>
        {innerchildren}
      </div>
    )
  }
}

Render

Ключевые слова:Управление потоком, подготовка дифференциала

Прежде всего, объясните, что модуль рендеринга здесь вообще относится к операции вставки vnode в дерево dom во всем процессе, однако часть этой операции берет на себя модуль diff, поэтому на самом деле модуль рендеринга более ответственен. для процесса Контролируйте и продвигайте работу в диф.

Контроль над процессом

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

Дифф готов

Как упоминалось ранее, Preact поддерживает только новое виртуальное дерево dom, содержащее обновленное содержимое в памяти, а другое представляет собой обновленное старое виртуальное дерево dom, которое фактически восстанавливается из дерева dom. также в процессе сравнения, пока патч сравнивается. Чтобы вышеуказанные операции не сбивали с толку, перед созданием/обновлением дерева DOM вам необходимо добавить некоторые настраиваемые атрибуты в узел DOM для записи состояния.

//创建自定义属性记录
export function renderComponent(component, opts, mountAll, isChild) {
  if (component._disable) return;

  let skip, rendered,
    props = component.props,
    state = component.state,
    context = component.context,
    previousProps = component.prevProps || props,
    previousState = component.prevState || state,
    previousContext = component.prevContext || context,
    isUpdate = component.base,
    nextBase = component.nextBase,
    initialBase = isUpdate || nextBase,
    initialChildComponent = component._component,
    inst, cbase;

Алгоритм сравнения

Ключевые слова:Зависимости DOM, Отключено или нет, DocumentFragment

Процесс diff в основном делится на два этапа: первый этап — установление соответствующих отношений между виртуальным узлом и dom-узлом, а второй этап — их сравнение и обновление dom-узла.

  • При фактическом выполнении отправной точкой операции diff является сравнение между корневым узлом компонента обновления и vnode, представляющим его следующее состояние. На этом шаге соответствующие отношения между ними очень ясны, а на следующем этапе необходимо определить соответствующие отношения в подэлементах этих двух узлов. end рассматривается как вновь добавленный узел, и судьба единственного узла dom должна быть переработана.
  • После входа в фазу обновления он будет классифицирован и обработан в соответствии с типом виртуального узла и эталонным узлом в дереве dom, а операция исправления будет выполняться в режиме реального времени во время процесса сравнения, и, наконец, будет создан новый узел dom. быть сгенерированы, а затем рекурсивно будут рекурсивны дочерние узлы.
    Diff流程图
    Дифференциальная блок-схема

DOM-зависимости

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

Disconnected or Not

  • What does Disconnected mean

Все мы знаем, что когда мы выполняем операции appendChild и removeChild для узлов в дереве dom, каждый раз, когда мы их выполняем, будет запускаться перекомпоновка страницы, а это поведение сопряжено со значительными накладными расходами. Поэтому, когда нам нужно выполнить ряд таких операций, мы можем предпринять такие меры по оптимизации: сначала создать узел, выполнить операцию добавления всех дочерних узлов на этом узле, а затем использовать этот узел как поддерево корневого узла. Однократное добавление или замена дерева dom, вызывающее перекомпоновку только один раз, завершает обновление всего поддерева. Этот метод обновления называется отключенным.

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

  • Go ahead to Preact

После прояснения этой предпосылки давайте посмотрим на реализацию Preact, Disconnected или Connected, которая является осадой. Хотя автор утверждает, что метод рендеринга Preact отключен, правда в том, что это не всегда так. В простом случае значение textnode изменяется или старый узел заменяется текстовым узлом. Все, что делает Preact, — это создает текстовый узел или изменяет значение nodeValue предыдущего текстового узла. Хотя запутывать эту сцену бессмысленно, чтобы полностью представить процесс diff, необходимо сначала объяснить его. Доберитесь до сути. Давайте посмотрим на первый пример. Чтобы проиллюстрировать, давайте использовать несколько экстремальный пример.

В этом примере видно, что после ввода текста происходит обновление из поддерева div в поддерево раздела, Для описания крайнего случая дочерние узлы до и после обновления одинаковы.

//例一 placeholder所在子树只有根节点不同
import { h, Component } from 'preact';

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      text: ''
    }
  }

  handlechang = e => {
    this.setState({
      text: e.target.value
    })
  }

  render({desc}, { text }) {
    return (
      <div>
        <input value={text} onChange={this.handlechang}/>
        {text ? <section key='placeholder'> 
          <h2>placeholder</h2>  
        </section>: <div key='placeholder'>
          <h2>placeholder</h2>  
        </div>}
      </div>
    )
  }
}

Далее давайте подробно рассмотрим процесс операции сравнения для этого сценария.

//原生dom的idiff逻辑
let out = dom,  //注释1
  nodeName = String(vnode.nodeName),
  prevSvgMode = isSvgMode,
  vchildren = vnode.children;

isSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode;

if (!dom) {  //注释2
  out = createNode(nodeName, isSvgMode);
}
else if (!isNamedNode(dom, nodeName)) {  //注释3
  out = createNode(nodeName, isSvgMode);
  while (dom.firstChild) out.appendChild(dom.firstChild);
  if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
  recollectNodeTree(dom);
}

//子节点递归
……
else if (vchildren && vchildren.length || fc) {
  innerDiffNode(out, vchildren, context, mountAll);
}
……

Независимо от того, являются ли элементы, участвующие в diff, пользовательскими компонентами или родной DOM, после слоев деконструкции они окончательно сравниваются в форме DOM. Поэтому нам нужно сосредоточиться только на логике сравнения нативного DOM.

Первый взглядПримечание 1, dom представляет узел в дереве dom, то есть узел, который нужно обновить, а vnode — это виртуальный узел, который нужно отобразить. В примере 1 отправной точкой diff является самый внешний div, который является переменной dom первого раунда, поэтомуЗаметка 2,Заметка 3Все суждения ложны. После этого над дочерними узлами исходящего узла и соответствующих дочерних узлов vnode выполняется рекурсивная операция сравнения.

Итак, вот первый вопрос,Начальная точка операции рендеринга всегда подключена.

if (vlen) {
  for (let i=0; i<vlen; i++) {
    vchild = vchildren[i];
    child = null;

    let key = vchild.key;
    // 相同key值匹配
    if (key!=null) {
      if (keyedLen && key in keyed) {
    child = keyed[key];
    keyed[key] = undefined;
    keyedLen--;
      }
    }
    // 相同nodeName匹配            
    else if (!child && min<childrenLen) {
      for (j=min; j<childrenLen; j++) {
    c = children[j];
    if (c && isSameNodeType(c, vchild)) {
      child = c;
      children[j] = undefined;
      if (j===childrenLen-1) childrenLen--;
          if (j===min) min++;
      break;
    }
      }
    }
    // vnode为section节点时,dom树中既无同key节点,也无同nodeName节点,因此为null
    child = idiff(child, vchild, context, mountAll);
……

Основой для установления соответствующей связи между дочерними узлами является либо одно и то же значение ключа, либо одно и то же имя узла.Можно знать, что связь между разделом и div не удовлетворяет двум вышеуказанным ситуациям. Итак, при повторном входе в метод idiff вЗаметка 2Поскольку dom не существует, будет создан новый узел раздела и назначен out, поэтому, когда снова выполняется сравнение подэлементов, поскольку out является новым узлом и не содержит никаких подэлементов, все различия подэлементов объекты раздела имеют значение null , что означает, что все дочерние элементы этого раздела окончательно созданы (независимо от того, задано ли значение ключа), хотя они точно такие же, как узлы в старом DOM. . . Итак, подводя итог, это первый случай,Все подузлы секции создаются заново, повторно не используются, но весь процесс работы осуществляется в отключенном состоянии..

Так что, если вы добавите одно и то же значение ключа к обоим?

// 例二,组件结构相同,唯一的区别是placeholder所在子树添加了相同的key值
import { h, Component } from 'preact';

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      text: ''
    }
  }

  handlechang = e => {
    this.setState({
      text: e.target.value
    })
  }


  render({desc}, { text }) {
    return (
      <div>
    <input value={text} onChange={this.handlechang}/>
        {text ? <section key='placeholder'> 
          <h2>placeholder</h2>  
        </section>: <div key='placeholder'>
          <h2>placeholder</h2>  
        </div>}
      </div>
    )
  }
}

Поскольку они имеют одно и то же значение ключа, их можно успешно соединить, когда определена соответствующая связь между vnode и dom, и войти в ссылку diff. Однако операция замены связывает все последующие операции. Хорошая новость заключается в том, что одни и те же дочерние узлы используются повторно.

// 原生dom的diff逻辑
// dom节点,即div存在,且与vnode节点类型section不同类型
else if (!isNamedNode(dom, nodeName)) {
  out = createNode(nodeName, isSvgMode);
  while (dom.firstChild) out.appendChild(dom.firstChild);
  if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
  recollectNodeTree(dom);
}

DocumentFragment

В дополнение к методу отключения, описанному выше, вы также можете вставить серию узлов в dom одновременно с помощью DocumentFragment. Когда узел DocumentFragment вставляется в дерево документа, вставляется не сам DocumentFragment, а все его дочерние узлы. Это делает DocumentFragment полезным заполнителем для временного хранения узлов, которые вставляются в документ одновременно. Кто-то на github также задал автору тот же вопрос, автор сказал, что он пытался использовать DocumentFragment, чтобы уменьшить количество перекомпоновок, но окончательный результат был неожиданным.

На картинке выше показана сравнительная диаграмма производительности тестового примера, написанного автором, по оси абсцисс — количество операций в секунду, чем больше значение, тем выше эффективность выполнения. Видно, что DocumentFragement работает хуже независимо от того, подключен он или нет. Конкретные причины еще предстоит выяснить.Оригинальная ссылка BenchMark.

механизм рециркуляции

Ключевые слова:Перерабатывающий бассейн и улучшенное крепление

Перерабатывающий бассейн и улучшенное крепление

Когда узел удаляется из DOM, узел не будет удален напрямую, а будет сохранен в двух пулах повторного использования после выполнения некоторой логики очистки в соответствии с типом узла (компонент или узел). Каждый раз, когда выполняется операция монтирования, метод создания будет искать узел того же типа в пуле повторного использования.Как только такой похожий узел будет найден, он будет передан в алгоритм сравнения в качестве эталонного узла для обновления, так что в последующем процессе сравнения узлы из пула рециркуляции будут исправлены как прототипы для создания новых узлов. Это эквивалентно изменению Mount to Update, что позволяет избежать дополнительных накладных расходов на сборку с нуля.

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

заключительные замечания

В этой статье основное внимание уделяется рабочему процессу Preact и некоторым деталям работы каждого модуля в нем, в надежде привлечь больше людей к участию в общении с сообществом. Друзья, заинтересованные в содержании, обсуждаемом в статье, могут связаться со мной в любое время.Если онлайн-общение не гладко, вы можете отправить свое резюме наcolaz1667@163.com. Самая романтичная вещь, которую я могу придумать, это собрать каждый кусочек смеха с вами по пути, сохранить его на потом, сесть за рабочую станцию ​​и неспешно болтать.