DevGang
Авторизоваться

Создайте свою собственную React-подобную библиотеку с нуля

Практически каждый разработчик программного обеспечения более или менее знает React. Даже некоторые серверные разработчики или разработчики машинного обучения - и даже некоторые не разработчики. Да, это хорошая библиотека, элегантная и многообещающая. Каждый фронтенд-разработчик рассказывает о своем API, его оптимизации, своих внутренних механизмах.

Внутренний механизм

Внутренние механизмы - самая важная часть, но как их понять? Чтение исходного кода React, безусловно, является вариантом, но может показаться ошеломляющим. Итак, читаем ли мы дайджесты от кого-то, кто прочитал весь исходный код? Интересно, сколько дайджестов мы можем прочитать, чтобы получить полное представление. Итак, я предлагаю создать React самостоятельно!

Создайте свой собственный React

Звучит безумно? Может, немного. Нам просто нужно закодировать некоторые важные части, которые могут заставить работать крошечную программу - вот и все. Вы можете попробовать собрать интересующие вас детали самостоятельно. 

Шаг 1. Рендеринг компонентов

Допустим, у нас есть такой компонент:

<p>hello world
  <span style={{color:'red'}}>!</span>
</p>

Он в формате JSX, поэтому в React код будет:

React.createElement('p',null,'hello world',
  React.createElement('span',{style:{color:'red'}},'!')
)

React.createElement мне не удобен. Вместо этого я просто использую класс D, поэтому мне нужно использовать ключевое слово new. И я изменил дочерние элементы на параметр props с фиксированным ключом children. Итак, определение и использование компонента должно быть таким:  

import { D, render } from '../lib'
let el = document.createElement('div')
document.body.append(el)
render(el, new D('p', {
  children: [
    'hello world',
    new D('span', {
      children: '!',
      style: { color: 'red' }
    })
  ]
}))

И мы ожидаем, что это будет выглядеть так:

Итак, нам просто нужно рекурсивно преобразовать экземпляры D в элементы и добавить их в дерево DOM.

// lib/d.js
export class D {
  ...
  init(parent) {
    this.parent = parent
    if (!this.component) {
      // text
      this.el = document.createTextNode(this.props.children)
    } else if (typeof this.component == 'string') {
      // html element 
      this.el = document.createElement(this.component)
      let { children, ...rest } = this.props
      assignProps(this.el, rest)
      this.children = normalizeDArr(children)
    } else {
      // component
      setREACT_LIKE_CUR_COMPONENT(this)
      setREACT_LIKE_CUR_COMPONENT_STATEI(0)
      this.children = normalizeDArr(this.component(this.props))
    }
    // append to DOM tree
    if (this.el) this.parent.append(this.el)
    // init children recursively
    this.children.forEach(x => x.init(this.el || this.parent))
  }
  ...
}

Если вы посмотрите мой код и запустите npm run dev1, вы получите такой результат:  

Это каким-то образом представляет виртуальную модель DOM, и каждый элемент HTML имеет привязанный к нему экземпляр D.

Шаг 2. Визуализация с состоянием

Теперь мы визуализируем hello world буквально или с переменной, содержащей то же значение. Тогда единственное, что нам нужно сделать, это сохранить его. useState хранит данные в массиве.

Вам нужно будет знать, какой компонент отрисовывается при вызове useState, и соответствующий индекс массива состояний.  

// lib/hooks.js
export function useState(initalValue) {
  let d = getREACT_LIKE_CUR_COMPONENT() // get component
  let i = getREACT_LIKE_CUR_COMPONENT_STATEI() // get index
  if (d.states[i] === undefined) d.states[i] = initalValue
  let v = d.states[i]
  setREACT_LIKE_CUR_COMPONENT_STATEI(i + 1)
  return [v, v1 => {
    d.states[i] = v1
    d.updateState()
  }]
}

useState вызывается внутри компонента App. Поэтому перед вызовом компонента App нам нужно сохранить массив состояний component и сбросить его index до 0.  

// lib/d.js
export class D {
  constructor(component, props = {}) {
    ...
    this.states = []
    ...
  }
  init(parent) {
    ...
      // render component
      setREACT_LIKE_CUR_COMPONENT(this)
      setREACT_LIKE_CUR_COMPONENT_STATEI(0)
      this.children = normalizeDArr(this.component(this.props))
    ...
  }
}

Теперь вы можете использовать состояние useState при рендеринге.

Шаг 3. Обработка события

Событие React управляется глобально, поэтому мы имитируем его с делегированием:

// lib/event.js
const EVENT_TYPES = ['click']
// delegate from mount point
export function listen(el) {
  return EVENT_TYPES.map(eventType => {
    let listener = e => {
      let key = 'on' + capitalize(eventType)
      bubble(e.target, e, key)
    }
    el.addEventListener(eventType, listener)
  })
}
// bubble event until it's handled
function bubble(el, e, key) {
  if (!el) return
  if (el[key]) return el[key](e)
  bubble(el.parentElement, e, key)
}

Затем добавьте опору onClick к кнопке:  

// step2/App.js
import { D, useState } from "../lib"
const App = () => {
  let [i, setI] = useState(0)
  let [str, setStr] = useState('hello counter!')
  return [
    str,
    new D('div', {
      children: [
        new D('button', {
          children: '-',
          // onClick assigned to element as prop
          onClick: () => setI(i - 1)
        }),
        i + '',
        new D('button', {
          children: '+',
          // onClick assigned to element as prop
          onClick: () => setI(i + 1)
        })
      ]
    })
  ]
}
export default App

React - это скорее планировщик, чем реактор, так как же он может объединять изменения состояния / свойств и обрабатывать их сразу? Здесь я использовал Promise.resolve.  

// lib/d.js
export class D {
  ...
  updateState() {
    this.stateChangeCount++
    // wait for changes to merge
    Promise.resolve().then(() => {
      if (!this.stateChangeCount) return
      this.stateChangeCount = 0
      if (this.el) {
        let { children, ...rest } = this.props
        assignProps(this.el, rest)
        if (!this.component) {
          return this.el.data = children
        }
      }
      let children = this.getChildren(this.props)
      ...
  }
  ...
}

Это не такая уж и сложная проблема, правда? Если вы запустите мой код npm run dev, вы получите такой результат. Попробуйте нажимать на кнопки.  

Важное примечание

React может делать гораздо больше. Например, React может отложить рендеринг до следующего кадра, если рендеринг занимает слишком много времени, и вы не получите сайт, не отвечающий на запросы. Он также может выполнять сравнение списков, переход, приостановку и т. д. Но нельзя ожидать, что 200 строк кода сделают так много. Это просто руководство по созданию React-подобной библиотеки и простое объяснение базового механизма React.

Источник:

#React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить