Skip to content

Instantly share code, notes, and snippets.

@mikhail-angelov
Last active March 11, 2018 17:59
Show Gist options
  • Save mikhail-angelov/bc489f6eb4f04623eb4134e99ee9db10 to your computer and use it in GitHub Desktop.
Save mikhail-angelov/bc489f6eb4f04623eb4134e99ee9db10 to your computer and use it in GitHub Desktop.
rctjs

В этой статье я хочу показать как довольно просто сделать еще один UI Framework на основе виртуального дома

В начале хочу сказать что я понимаю под UI framework - потому как у многих разное мнения на этот счет. Например некоторые считаю что Angular и Ember это UI framework а React - это всего лишь библиотека которая позволят легче работать с view частью приложении

Определим UI framework так - это библиотека которая помогает создавать/обновлять/удалять страницы либо отдельные элементы страницы в этом смысле довольно широкий спектр обертка над DOM API может оказаться UI framework, вопрос лишь в вариантах абстракции (API) которые предоставляет эта библиотека для манипуляции с DOM и в эффективности этих манипуляций

В предложенной формулировке - React вполне является UI framework.

Что ж, давайте посмотрим как написать свой React c блэкджеком и прочим. Известно что React использует концепцию виртуального дома. В упрощенном виде она заключается в том что узлы (node) реального DOM строятся в четком соответствии с узлами предварительно построенного дерева виртуального DOM. Прямая манипуляция с реальным DOM не приветствуется, в случае если необходимо внести изменения в реальным DOM, изменения вносятся в виртуальный DOM, потом новая версия виртуальный DOM сравнивается со старой, собираются изменения которые необходимо применить к реальному DOM и они применяются таким образом минимизируется взаимодействие с реальным DOM - что делает работу приложения более оптимальной Поскольку дерево виртуального дома это обычный java-script объект - им довольно легко манипулировать - изменять/сравнивать его узлы, под словом легко тут я понимаю что код сборки виртуальных но довольно простой и может быть частично сгенерирован препроцессором из декларативного языка более высокого уровня JSX.

Начнем с JSX так выглядит пример JSX кода

const Component = () => (
  <div className="main">
    <input />
    <button onClick={() => console.log('yo')}> Submit </button>
  </div>
)

export default Component

нам нужно сделать так чтобы при вызове функции Component создавался такой виртуальный DOM

const vdom = {
  type: 'div',
  props: { className: 'main' },
  children: [
    { type: 'input' },
    {
      type: 'button',
      props: { onClick: () => console.log('yo') },
      children: ['Submit']
    }
  ]
}

Конечно мы не будем писать это преобразование вручную, воспользуемся этим плагином, плагин устарел, но он достаточно прост, чтобы помочь нам понять как все работает. Он использует jsx-transform, который преобразует JSX примерно так:

jsx.fromString('<h1>Hello World</h1>', {
  factory: 'h'
});
// => 'h("h1", null, ["Hello World"])'

так, все что нам нужно - реализовать конструктор vdom узлов h - функцию которая будет рекурсивно создавать узлы виртуального DOM в случае реакт этим занимается функция React.createElement. Ниже приметивная реализация такой функции

export function h(type, props, ...stack) {
  const children = (stack || []).reduce(addChild, [])
  props = props || {}
  return typeof type === "string" ? { type, props, children } : type(props, children)
}

function addChild(acc, node) {
  if (Array.isArray(node)) {
    acc = node.reduce(addChild, acc)
  } else if (null == node || true === node || false === node) {
  } else {
    acc.push(typeof node === "number" ? node + "" : node)
  }
  return acc
}

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

'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']}

и так для узлов любой вложенности

Отлично теперь наша функция Component - возвращает узел vdom теперь будет сложная часть, нам нужно написать функцию patch которая берет на вход корневой DOM элемент приложения, старый vdom, новый vdom - и осуществляет обновление узлов реального DOM в соответствии с новым vdom

возможно можно написать этот код проще, но получилось так я взял за основу код из пакета picodom

export function patch(parent, oldNode, newNode) {
  return patchElement(parent, parent.children[0], oldNode, newNode)
}
function patchElement(parent, element, oldNode, node, isSVG, nextSibling) {
  if (oldNode == null) {
    element = parent.insertBefore(createElement(node, isSVG), element)
  } else if (node.type != oldNode.type) {
    const oldElement = element
    element = parent.insertBefore(createElement(node, isSVG), oldElement)
    removeElement(parent, oldElement, oldNode)
  } else {
    updateElement(element, oldNode.props, node.props)

    isSVG = isSVG || node.type === "svg"
    let childNodes = []
      ; (element.childNodes || []).forEach(element => childNodes.push(element))
    let oldNodeIdex = 0
    if (node.children && node.children.length > 0) {
      for (var i = 0; i < node.children.length; i++) {
        if (oldNode.children && oldNodeIdex <= oldNode.children.length &&
          (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type ||
            (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex]))
        ) {
          patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG)
          oldNodeIdex++
        } else {
          let newChild = element.insertBefore(
            createElement(node.children[i], isSVG),
            childNodes[oldNodeIdex]
          )
          patchElement(element, newChild, {}, node.children[i], isSVG)
        }
      }
    }
    for (var i = oldNodeIdex; i < childNodes.length; i++) {
      removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {})
    }
  }
  return element
}

Эта наивная реализация, она ужасно не оптимальна, не принимает во внимание идентификаторы элементов (key, id) - чтобы корректно обновлять нужные элементы в списках, но в примитивных случаях она работает норм

Реализацию функций createElement updateElement removeElement я тут не привожу она приметивна, кого заинтересует можно посмотреть исходники тут Там есть единственный нюанс - когда обновляются свойства value для input элементов то сравнение нужно делать не со старой vnodе а с атрибутом value в реальном доме - это предотвратит обновление этого свойства у активного элемента (поскольку оно там уже и так обновлено) и предотвратит проблемы с курсором и выделением.

Ну вот и все теперь нам осталось только собрать эти кусочки вместе и написать UI Framework Уложимся в 5 строк.

1 как в React чтобы собрать приложение нам нужно 3 параметра export function app(selector, view, initProps) { selector - корневой селектор dom в который будет смонтировано приложение (по умолчанию 'body') view - функция которая конструирует корневой vnode initProps - начальные свойства приложения 2 берем корневой элемент в DOM const rootElement = document.querySelector(selector || 'body') 3 собираем vdom c начальными свойствами let node = view(initProps) 4 монтируем полученный vdom в DOM в качестве старой vdom берем null patch(rootElement, null, node) 5 возвращаем функцию обновления приложения с новыми свойствами return props => patch(rootElement, node, (node = view(props)))

Framework готов!

‘Hello world’ на этом Framework будет выглядеть таким образом

import { h, app } from "../src/index"

function view(state) {
  return (
    <div>
      <h2>{`Hello ${state}`}</h2>
      <input value={state} oninput={e => render(e.target.value)} />
    </div>
  )
}

const render = app('body', view, 'world')

Эта библиотека так же как React поддерживает композицию компонент, добавление, удаление компонент в момент исполнения, так что ее можно считать полноценным UI Framework Чуть более сложный пример использования можно посмотреть тут ToDo example Конечно в этой библиотеке много чего нет: событий жизненного цикла (хотя их не трудно прикрутить, мы же сами управляем созданием, обновлением, удалением узлов), отдельного обновления дочерних узлов по типу this.setState (для этого нужно сохранять ссылки на DOM элементы для каждого узла vdom - это немного усложнит логику), код patchElement ужасно неоптимальный, будет плохо работать на большом количестве элементов, не отслеживает элементы с идентификатором и т.д. В любом случае, библиотека разрабатывалась в образовательных целях - не используйте ее в продакшене :)

PS: на эту статья меня вдохновила великолепная библиотека Hyperapp, часть кода взята оттуда

Удачного кодинга!

@mikhail-angelov
Copy link
Author

start

@mikhail-angelov
Copy link
Author

v2

@mikhail-angelov
Copy link
Author

rename

@mikhail-angelov
Copy link
Author

spell checker

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment