Mini JS framework Анализ исходного кода Hyperapp

внешний интерфейс исходный код JavaScript React.js
Mini JS framework Анализ исходного кода Hyperapp

HyperappЭто мини JS-фреймворк, очень популярный в последнее время, его исходный код составляет менее 400 строк, а после сжатия gzip — всего 1 КБ, но он имеет очень высокую степень завершенности. С точки зрения общей реализации идея Hyperapp аналогична React, которая использует Virtual DOM для достижения эффективных обновлений DOM. Прежде чем исследовать реализацию Hyperapp, давайте посмотрим, как ее использовать.

Примечание. Эта статья основана на Hyperapp версии 1.2.5.

использовать

Пример приложения приведен в официальной документации (онлайн-демонстрация нажмите на меня), код показан ниже:

import { h, app } from "hyperapp"

const state = {
  count: 0
}

const actions = {
  down: value => state => ({ count: state.count - value }),
  up: value => state => ({ count: state.count + value })
}

const view = (state, actions) => (
  <div>
    <h1>{state.count}</h1>
    <button onclick={() => actions.down(1)}>-</button>
    <button onclick={() => actions.up(1)}>+</button>
  </div>
)

app(state, actions, view, document.body)

Несколько простых инструкций, которые помогут вам быстро начать работу с Hyperapp:

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

Во-первых, Hyperapp предоставляет внешнему миру только две функции:hа такжеapp. вappИспользуется для монтирования приложения на узле DOM, что эквивалентно функции запуска. а такжеhОн используется для обработки представления и возврата узла Virtual DOM. Поскольку браузер не понимает синтаксис JSX, используемый функцией представления в приведенном выше примере, его необходимо обработать с помощью инструмента компиляции, такого как Babel (стороны React должны быть знакомы с ними). Установитьtransform-react-jsxПосле плагина в.babel.rcуказать плагин вpragmaУстановить какh:

{
  "plugins": [["transform-react-jsx", { "pragma": "h" }]]
}

Таким образом, после компиляции Babel приведенный вышеviewФункция становится такой:

const view = (state, actions) =>
  h("div", {}, [
    h("h1", {}, state.count),
    h("button", { onclick: () => actions.down(1) }, "-"),
    h("button", { onclick: () => actions.up(1) }, "+")
  ])

нашhПосле одной операции функции структура возвращаемого узла Virtual DOM выглядит так:

{
  nodeName: "div",
  attributes: {},
  children: [
    {
      nodeName: "h1",
      attributes: {},
      children: [0]
    },
    {
      nodeName: "button",
      attributes: { ... },
      children: ["-"]
    },
    {
      nodeName:   "button",
      attributes: { ... },
      children: ["+"]
    }
  ]
}

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

Конечно, Hyperapp также поддерживает@hyperapp/html, hyperxДругие библиотеки, которые могут генерировать Virtual DOM, здесь не перечислены.

Анализ исходного кода

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

export function app(state, actions, view, container) {
  var map = [].map
  var rootElement = (container && container.children[0]) || null
  var oldNode = rootElement && recycleElement(rootElement)
  var lifecycle = []
  var skipRender
  var isRecycling = true
  var globalState = clone(state)
  var wiredActions = wireStateToActions([], globalState, clone(actions))

  scheduleRender()

  return wiredActions
}

Жизненный цикл

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

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

appПосле выполнения функции, после ряда подготовительных действий, она вызоветscheduleRenderфункция для рендеринга представления. Как следует из названия, эта функция предназначена для планирования рендеринга. Давайте посмотрим на исходный код:

  function scheduleRender() {
    if (!skipRender) {
      skipRender = true
      setTimeout(render)
    }
  }

Как видите, фактический рендеринг выполняетсяrenderфункцию для обработки, время выполнения определяетсяsetTimeout(function(){}, 0)Решение, то есть после запуска следующего цикла обработки событий, выполняется асинхронно. и тутskipRenderэто переменная блокировки, гарантированная в каждом цикле событийstateВыполняется только один рендер, независимо от того, сколько изменений внесено. Представьте себе сценарий, в котором мы выполняем 1000 раз в цикле.actionsкак-то изменитьstateЗначение в , если вышеперечисленные операции не выполнять, представление будет отрисовано 1000 раз, что довольно интенсивно по производительности, что очень неразумно. На самом деле, обработка Hyperapp также немного грубая.В более сложных интерфейсных фреймворках будут очень полные решения.Например, реализация Vue $nextTick намного сложнее.Подробности можно найти в этой статье -Механизм Vue nextTick.

renderпередачаresolveNodeчтобы получить последний узел в виде Virtual DOM, а затем отправить его наpatchФункция сравнивает старый и новый узлы, а затем обновляет представление и в то же время присваивает значение нового узла старому узлу, чтобы облегчить следующее сравнение и обновление. кроме как в концеpatchОперации DOM выполняются при обновлении представления.В остальное время узлы хранятся в памяти в виде виртуального DOM.Пока алгоритм сравнения старых и новых узлов достаточно эффективен, высокая эффективность обновления представления может быть достигнута. поддерживаться.

За исключением рендеринга при инициализации, всякий раз, когдаactionsМетод в модифицированном видеstateРендеринг также запускается, когда данные в файле . Конечно, Hyperapp не «наблюдает».state, Но поactionsМетод в обертке реализует эту функциональность (которая также указана только для Hyperapp).actionsМетоды могут быть измененыstateпричина данных в).

обработка действий

Давайте посмотрим, как Hyperappactionsметод в процессе, чтобы позволить ему срабатывать после его вызоваscheduleRenderиз.appПри подготовке перед тем, как функция выполнит первоначальный рендеринг, самой важной операцией является обработкаactionsметод в . Прежде чем изучать его исходный код, давайте взглянем на пару Hyperapp.actionsМетод, разработанный в спецификации, когдаstateКогда нет вложенных объектов, сводка выглядит примерно так:

  • Должна быть унарной функцией (принимает только один аргумент)
  • Возвращаемое значение функции должно быть одним из следующих:
    • "частичный объект состояния", т.е. содержащийstateОбъект частичного состояния. новыйstateбудет оригиналstateНеглубокое слияние с этим возвращаемым значением. Например:
      const state = {
        name: 'chris',
        age: 20
      }
      
      const actions = {
        setAge: newAge => ({ age: newAge })
      }
    
    • один принимает текущийstateа такжеactionsФункция, которая принимает параметр, возвращаемое значение функции должно быть "частичным объектом состояния". Обратите внимание, что приемлемоstateПараметры напрямую изменяются и возвращаются. Правильный пример выглядит следующим образом:
      const actions = {
        down: value => state => ({ count: state.count - value }),
        up: value => state => ({ count: state.count + value })
      }
    
    • Обещание / ноль / не определено. В этот момент он не будет запускать повторную визуализацию представления.

когдаstateКогда есть вложенные объекты,actionsСоответствующее значение атрибута в является объектом частичного состояния, на самом деле разницы по сути нет, вы сможете понять это, посмотрев на следующий пример:

const state = {
  counter: {
    count: 0
  }
}

const actions = {
  counter: {
    down: value => state => ({ count: state.count - value }),
    up: value => state => ({ count: state.count + value })
  }
}

Теперь давайте посмотрим на пару HyperappactionsОбработка метода в:

  /**
   * 
   * @param {Array} path  储存 state 中每层的 key,用于获取和设置 partial state object
   * @param {Object} state 
   * @param {Object} actions 
   */
  function wireStateToActions(path, state, actions) {
    // 遍历 actions
    for (var key in actions) {
      typeof actions[key] === "function"
        // actions 中属性值为函数时,重新封装
        ? (function(key, action) {
            actions[key] = function(data) {
              // 执行方法
              var result = action(data)
              
              /*返回值是函数时,传入 state 和 actions 再次执行之
                得到 partial state object
               */
              if (typeof result === "function") {
                result = result(getPartialState(path, globalState), actions)
              }
              
              /* result 不是 Promise/null/undefined
                 意味着 result 返回的是 partial state object
                 同时 result 与当前的 globalState(保存在全局的 state 的副本)中的 partial state object 不一致时 
                 调用 scheduleRender 重新渲染视图
               */
              if (
                result &&
                result !== (state = getPartialState(path, globalState)) &&
                !result.then // !isPromise
              ) {
                // globalState 立即更新
                // 安排视图渲染
                scheduleRender(
                  (globalState = setPartialState(
                    path,
                    clone(state, result),
                    globalState
                  ))
                )
              }
              return result
            }
          })(key, actions[key])
          // 直接返回 partial state object 
        : wireStateToActions(
            // 当 state 有嵌套时,规范要求 actions 中也有相同的嵌套层级
            path.concat(key),
            (state[key] = clone(state[key])),
            (actions[key] = clone(actions[key]))
          )
    }
    // 返回处理之后的所有函数
    // 作为对外接口
    return actions
  }

Комментарии были сказаны более подробно.Подводя итог, Hyperapp ставитactionsВсе методы вstateПосле «модификации» данных вызываетсяscheduleRenderПовторно визуализируйте вид. Причина, по которой слово «модификация» взято здесь в кавычки, заключается в том, что на самом делеactionsособо не изменилсяstateЗначение данных в данных, но каждый раз заменяется новым объектомstate. Это включает в себя концепцию «неизменяемости», которая является неизменностью. Эта функция позволяет нам отлаживать код подобно путешествию во времени (посколькуstateхранятся в памяти, аналогично снимкам). Вот почему в приведенном выше коде мы можем напрямую использовать===Причина сравнения двух объектов.

Virtual DOM

Продолжайте смотреть на жизненный цикл, прежде чем начнется отрисовка страницы, Hyperapp пройдет инициализациюappкорневой узел функции иviewВсе узлы, сгенерированные функцией, обрабатываются как Virtual DOM, форма которого показана в первом разделе в начале статьи. На этой основе Hyperapp предоставляетcreateElement/updateElement/removeElement/removeChildren/updateAttributeи другие методы обработки сопоставления виртуальных DOM с реальными узлами DOM.

Обновление различий между новыми и старыми узлами

Ниже приведена наиболее важная часть обновления узла. Возможно, обновления diff являются наиболее важной частью определения производительности React-подобного фреймворка. Давайте посмотрим, как это делает Hyperapp. Различия и обновления старых и новых узлов выполняютсяpatchФункция для завершения. Который принимает следующие четыре параметра (на самом деле 5, пятый параметр связан SVG, не обсуждаемый здесь):parent(родительский узел корневого узла текущей иерархии, узел DOM),element(Корневой узел текущего уровня, узел DOM, изначально генерируется сопоставлением oldNode),oldNode(виртуальный дом),newNode(Виртуальный дом).patchВ зависимости от разницы между старыми и новыми узлами функция может выполнять следующие четыре операции в соответствии с приоритетом:

  1. Старая и новая ноды одинаковы (можно напрямую пройти===Суждение): ничего не делать и вернуться сразу
  2. Старый узел не существует или старый и новый узлы отличаются (черезnodeNameсуждение): передачаcreateElementСоздайте новый узел и вставьте его вparentв дочернем элементе . Если старый узел существует, вызовитеremoveElementудалите это.
  3. Когда и старые, и новые узлы не являются узлами-элементами: БудуelementизnodeValueприсвоить значение какnewNode. Согласно спецификации DOM уровня 2, другие типы узлов, кроме текстовых, комментариев, CDATA и атрибутивных узлов, чьиnodeValueобеnull. Для вышеуказанных четырех узлов непосредственно обновите ихnodeValueзначение для завершения обновления узла
  4. И старый, и новый узлы существуют, и имена узлов одинаковы (то есть старый и новый узлыnodeNameТо же самое, но это не один и тот же узел, отличный от случая 1): Логика заключается в том, чтобы сначала обновить атрибуты узла, а затем ввести дочерний массив для рекурсивного вызова.patchфункция обновления. Однако для повышения производительности Hyperapp предоставляет узламkeyАтрибуты. имеютkeyВиртуальный DOM атрибута будет соответствовать определенному узлу DOM (каждый узелkeyЗначения атрибутов должны быть гарантированно уникальными среди одноуровневых узлов). Таким образом, он может быть непосредственно вставлен в новую позицию при обновлении вместо неэффективного удаления и создания новых узлов. Следующая блок-схема иллюстрирует эту стратегию:

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

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