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