Серия рукописного ввода — Реализация React платинового уровня

внешний интерфейс React.js
Серия рукописного ввода — Реализация React платинового уровня

Это 7-й день моего участия в Gengwen Challenge.Подробности о мероприятии:Обновить вызов

Почему платина, ведь до короля еще далеко. В этой статье реализуется только простая версия React и рассматриваются основные функции React 16.8, включая виртуальный DOM, Fiber, алгоритм Diff, функциональные компоненты, хуки и т. д.

Введение

Эта статья основана натряпка тряпка.US/build-someone-…Реализуйте простую версию React.

Учебные идеи этой статьи исходят отИсходный код Kasong-b station-React, на каком уровне вы находитесь?.

Насмешливая версия — React 16.8.

Будут реализованы следующие функции:

  1. createElement (виртуальный DOM);
  2. render;
  3. параллельный режим;
  4. Fibers;
  5. Render and Commit Phases;
  6. Координация (алгоритм сравнения);
  7. функциональный компонент;
  8. hooks;

На ужин читайте дальше.

2. Подготовка

1. React Demo

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

const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);

Полный исходный код этого примера см.reactDemo

Откройте в браузере reactDemo.html, как показано ниже:

image.png

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

1.1 element

const element = <div>123</div>Собственно синтаксис JSX.

Реагировать на официальный сайтИнтерпретация JSX выглядит следующим образом:

JSX — это расширение синтаксиса JavaScript. Он похож на язык шаблонов, но обладает всеми возможностями JavaScript. В конечном итоге JSX будет скомпилирован babel в вызов функции React.createElement().

пройти черезвавилон онлайн сборник const element = <div>123</div>.

image.png

знатьconst element = <div>123</div>Фактический код после компиляции выглядит следующим образом:

const element = React.createElement("div", {
  title: "foo"
}, "hello");

Давайте посмотрим, какой объект на самом деле генерируется React.createElement выше.

Попробуйте распечатать в демо:

const element = <div title="foo">hello</div>
console.log(element)
const container = document.getElementById('container')
ReactDOM.render(element, container);

Вы можете видеть, что выходной элемент выглядит следующим образом:

image.png

Упростить элемент:

const element = {
    type: 'div',
    props: {
        title: 'foo',
        children: 'hello'
    }
}

Подводя итог,React.createElementФактически генерируется объект-элемент, обладающий следующими свойствами:

  • тип: имя тега
  • props
    • title: атрибут тега
    • дети: дочерние узлы

1.2 render

ReactDOM.render()Добавляем элемент в ноду DOM с id контейнера, ниже мы просто напишем вместо него методReactDOM.render().

  1. Создайте узел с меткой element.type;
const node = document.createElement(element.type)
  1. Установите заголовок узла node в element.props.title;
node["title"] = element.props.title
  1. Создайте пустой текстовый узел text;
const text = document.createTextNode("")
  1. Установите для nodeValue текстового узла значение element.props.children;
text["nodeValue"] = element.props.children
  1. Добавьте текст текстового узла к узлу узла;
node.appendChild(text)
  1. Добавить узел узла к узлу контейнера
container.appendChild(node)

Полный исходный код этого примера см.reactDemo2

Запускаем исходный код, результат следующий, что согласуется с результатом внедрения React:

image.png

3. Старт

Симулируя React выше, методы React.createElement и ReactDOM.render просто заменяются, и тогда различные функции React будут реально реализованы.

1. createElement (виртуальный DOM)

Выше было понятно, что роль createElement заключается в создании объекта элемента, структура которого следующая:

// 虚拟 DOM 结构
const element = {
    type: 'div', // 标签名
    props: { // 节点属性,包含 children
        title: 'foo', // title 属性
        children: 'hello' // 子节点,注:实际上这里应该是数组结构,帮助我们存储更多子节点
    }
}

В соответствии со структурой элемента разработана функция createElement, и код выглядит следующим образом:

/**
 * 创建虚拟 DOM 结构
 * @param {type} 标签名
 * @param {props} 属性对象
 * @param {children} 子节点
 * @return {element} 虚拟 DOM
 */
function createElement (type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => 
                typeof child === 'object'
                ? child
                : createTextElement(child)
            )
        }
    }
}

Здесь учитывается, что когда дочерние элементы не являются объектами, необходимо создать элемент textElement, код выглядит следующим образом:

/**
 * 创建文本节点
 * @param {text} 文本值
 * @return {element} 虚拟 DOM
 */
function createTextElement (text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    }
}

Затем попробуйте, код выглядит следующим образом:

const myReact = {
    createElement
}
const element = myReact.createElement(
  "div",
  { id: "foo" },
  myReact.createElement("a", null, "bar"),
  myReact.createElement("b")
)
console.log(element)

Полный исходный код этого примера см.reactDemo3

Результирующий объект элемента выглядит следующим образом:

const element = {
    "type": "div", 
    "props": {
        "id": "foo", 
        "children": [
            {
                "type": "a", 
                "props": {
                    "children": [
                        {
                            "type": "TEXT_ELEMENT", 
                            "props": {
                                "nodeValue": "bar", 
                                "children": [ ]
                            }
                        }
                    ]
                }
            }, 
            {
                "type": "b", 
                "props": {
                    "children": [ ]
                }
            }
        ]
    }
}

JSX

На самом деле в процессе разработки с react мы не создаем такие компоненты:

const element = myReact.createElement(
  "div",
  { id: "foo" },
  myReact.createElement("a", null, "bar"),
  myReact.createElement("b")
)

Вместо этого с помощью синтаксиса JSX код выглядит следующим образом:

const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

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

/** @jsx myReact.createElement */
const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

Полный исходный код этого примера см.reactDemo4

2. render

Функция рендеринга помогает нам добавить элемент к реальному узлу.

Он будет реализован в следующие этапы:

  1. Создайте узел dom типа element.type и добавьте его в контейнер;
/**
 * 将虚拟 DOM 添加至真实 DOM
 * @param {element} 虚拟 DOM
 * @param {container} 真实 DOM
 */
function render (element, container) {
    const dom = document.createElement(element.type)
    container.appendChild(dom)
}
  1. Добавьте element.children в узел dom;
element.props.children.forEach(child => 
    render(child, dom)
)
  1. Специальная обработка текстовых узлов;
const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode("")
    : document.createElement(element.type)
  1. Добавьте свойство props элемента в dom;
const isProperty = key => key !== "children"
Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
})

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

const myReact = {
    createElement,
    render
}
/** @jsx myReact.createElement */
const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

myReact.render(element, document.getElementById('container'))

Полный исходный код этого примера см.reactDemo5

Результат показан на рисунке, и вывод успешен:

image.png

3. Параллельный режим (requestIdleCallback)

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

/**
 * 将虚拟 DOM 添加至真实 DOM
 * @param {element} 虚拟 DOM
 * @param {container} 真实 DOM
 */
function render (element, container) {
    // 省略
    // 遍历所有子节点,并进行渲染
    element.props.children.forEach(child =>
        render(child, dom)
    )
    // 省略
}

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

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

Вышеуказанные проблемы могут быть решены с помощью следующих шагов:

  1. Работа рендеринга может быть прервана.Если вставлено задание с более высоким приоритетом, рендеринг браузера будет временно прерван.После завершения работы рендеринг браузера будет возобновлен;
  2. Разбейте работу по рендерингу на небольшие блоки;

Используйте requestIdleCallback, чтобы обойти прерванную работу рендеринга.

window.requestIdleCallbackФункции очередей, которые вызываются в период простоя браузера. Это позволяет разработчикам выполнять фоновую и низкоприоритетную работу в основном цикле событий, не влияя на критичные к задержке события, такие как анимация и ответы ввода.

Подробнее об window.requestIdleCallback см. в документации:Документация

код показывает, как показано ниже:

// 下一个工作单元
let nextUnitOfWork = null
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止时间
 */
function workLoop(deadline) {
  // 是否应该停止工作循环函数
  let shouldYield = false
  
  // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    
    // 如果截止时间快到了,停止工作循环函数
    shouldYield = deadline.timeRemaining() < 1
  }
  
  // 通知浏览器,空闲时间应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)

// 执行单元事件,并返回下一个单元事件
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

PerformUnitOfWork используется для выполнения единичного события и возврата следующего единичного события Конкретная реализация будет описана ниже.

4. fiber

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

Примечание: на самом деле функция requestIdleCallback нестабильна и не рекомендуется для производственных сред.Этот пример используется только для имитации идеи React.Сама React не использует requestIdleCallback, чтобы позволить браузеру отображать единицу работы в время простоя.

С другой стороны, React разработал волокна, позволяющие разделить работу по рендерингу на более мелкие блоки.

Каждый элемент представляет собой структуру волокна, а каждое волокно представляет собой единицу работы рендеринга.

такВолокно — это и структура данных, и единица работы..

Волокна представлены на простом примере ниже.

Предположим, вам нужно отрендерить такое дерево элементов:

myReact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

Сгенерированное дерево волокон показано на рисунке:

Оранжевым цветом представлены дочерние узлы, желтым — родительские узлы, а синим — одноуровневые узлы.

image.png

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

Стрелки на приведенном выше рисунке также указывают на процесс рендеринга волокон.Процесс рендеринга подробно описан следующим образом:

  1. Начиная с корня, найдите первый дочерний узел div;
  2. Найдите первый дочерний узел h1 div;
  3. Найдите первый дочерний узел p узла h1;
  4. найти первого потомка p,Если нет дочернего узла, найти следующий родственный узел, найти родственный узел a узла p;
  5. Найдите первого потомка ,Если нет дочернего узла и родственного узла, найдите следующий родственный узел его родительского узла., найти родственный узел h2 родительского узла a;
  6. Найти первый дочерний узел h2, не удается найти, найти родственный узел, не удается найти, найти родственный узел родительского узла div, не удается найти, продолжать поиск родственного узла родителя узел div, найти корень;
  7. Шаг 6 нашел корневой узел, и рендеринг завершен.

Процесс рендеринга реализован в коде ниже.

  1. Извлеките часть создания узлов DOM при рендеринге в функцию creactDOM;
/**
 * createDom 创建 DOM 节点
 * @param {fiber} fiber 节点
 * @return {dom} dom 节点
 */
function createDom (fiber) {
    // 如果是文本类型,创建空的文本节点,如果不是文本类型,按 type 类型创建节点
    const dom = fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode("")
        : document.createElement(fiber.type)

    // isProperty 表示不是 children 的属性
    const isProperty = key => key !== "children"
    
    // 遍历 props,为 dom 添加属性
    Object.keys(fiber.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = fiber.props[name]
        })
        
    // 返回 dom
    return dom
}
  1. Установите первую единицу работы в качестве корневого узла волокна в рендере;

Корневой узел волокна содержит только дочернее свойство, которое является параметром волокна.

// 下一个工作单元
let nextUnitOfWork = null
/**
 * 将 fiber 添加至真实 DOM
 * @param {element} fiber
 * @param {container} 真实 DOM
 */
function render (element, container) {
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element]
        }
    }
}
  1. Рендерим файбер, когда браузер бездействует, через requestIdleCallback;
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止时间
 */
function workLoop(deadline) {
  // 是否应该停止工作循环函数
  let shouldYield = false
  
  // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    
    // 如果截止时间快到了,停止工作循环函数
    shouldYield = deadline.timeRemaining() < 1
  }
  
  // 通知浏览器,空闲时间应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
  1. Функция PerformUnitOfWork для рендеринга волокна;
/**
 * performUnitOfWork 处理工作单元
 * @param {fiber} fiber
 * @return {nextUnitOfWork} 下一个工作单元
 */
function performUnitOfWork(fiber) {
  // TODO 添加 dom 节点
  // TODO 新建 filber
  // TODO 返回下一个工作单元(fiber)
}

4.1 Добавить узел дома

function performUnitOfWork(fiber) {
    // 如果 fiber 没有 dom 节点,为它创建一个 dom 节点
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    // 如果 fiber 有父节点,将 fiber.dom 添加至父节点
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
    }
}

4.2 Создать новый фильтр

function performUnitOfWork(fiber) {
    // ~~省略~~
    // 子节点
    const elements = fiber.props.children
    // 索引
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null
    // 遍历子节点
    while (index < elements.length) {
        const element = elements[index]

        // 创建 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // 将第一个子节点设置为 fiber 的子节点
        if (index === 0) {
            fiber.child = newFiber
        } else if (element) {
        // 第一个之外的子节点设置为该节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }
}

4.3 Возврат к следующей единице работы (волокну)


function performUnitOfWork(fiber) {
    // ~~省略~~
    // 如果有子节点,返回子节点
    if (fiber.child) {
        return fiber.child
    }
    let nextFiber = fiber
    while (nextFiber) {
        // 如果有兄弟节点,返回兄弟节点
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }

        // 否则继续走 while 循环,直到找到 root。
        nextFiber = nextFiber.parent
    }
}

Выше мы реализовали функцию рендеринга волокна на страницу, причем процесс рендеринга прерываем.

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

const element = (
    <div>
        <h1>
        <p />
        <a />
        </h1>
        <h2 />
    </div>
)

myReact.render(element, document.getElementById('container'))

Полный исходный код этого примера см.reactDemo7

Дом выводится, как и ожидалось, как показано на рисунке:

image.png

5. Этап отправки рендера

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

Обработка оптимизации этапа отправки рендеринга выглядит следующим образом:

  1. Логическое удаление добавления дочерних узлов к родительским узлам в PerformUnitOfWork;
function performUnitOfWork(fiber) {
    // 把这段删了
    if (fiber.parent) {
       fiber.parent.dom.appendChild(fiber.dom)
    }
}
  1. Добавьте переменную корневого узла для хранения корневого узла волокна;
// 根节点
let wipRoot = null
function render (element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        }
    }
    // 下一个工作单元是根节点
    nextUnitOfWork = wipRoot
}
  1. Когда вся работа с волокном завершена, nextUnitOfWork не определен, а затем визуализируется настоящий DOM;
function workLoop (deadline) {
    // 省略
    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }
    // 省略
}
  1. Добавлена ​​функция commitRoot для выполнения рендеринга реального DOM и рекурсивного рендеринга дерева волокон в реальный DOM;
// 全部工作单元完成后,将 fiber tree 渲染为真实 DOM;
function commitRoot () {
    commitWork(wipRoot.child)
    // 需要设置为 null,否则 workLoop 在浏览器空闲时不断的执行。
    wipRoot = null
}
/**
 * performUnitOfWork 处理工作单元
 * @param {fiber} fiber
 */
function commitWork (fiber) {
    if (!fiber) return
    const domParent = fiber.parent.dom
    domParent.appendChild(fiber.dom)
    // 渲染子节点
    commitWork(fiber.child)
    // 渲染兄弟节点
    commitWork(fiber.sibling)
}

Полный исходный код этого примера см.reactDemo8

Результат запуска исходного кода выглядит следующим образом:

image.png

6. Координация (алгоритм сравнения)

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

Согласование уменьшает количество операций над реальным DOM.

1. currentRoot

Добавьте переменную currentRoot, чтобы сохранить дерево волокон перед обновлением корневого узла, добавьте к волокну альтернативный атрибут и сохраните дерево волокон перед обновлением волокон;

let currentRoot = null
function render (element, container) {
    wipRoot = {
        // 省略
        alternate: currentRoot
    }
}
function commitRoot () {
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
}

2. performUnitOfWork

Извлечь логику нового волокна в PerformUnitOfWork в функцию reconcileChildren;

/**
 * 协调子节点
 * @param {fiber} fiber
 * @param {elements} fiber 的 子节点
 */
function reconcileChildren (fiber, elements) {
    // 用于统计子节点的索引值
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null

    // 遍历子节点
    while (index < elements.length) {
        const element = elements[index]

        // 新建 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // fiber的第一个子节点是它的子节点
        if (index === 0) {
            fiber.child = newFiber
        } else if (element) {
        // fiber 的其他子节点,是它第一个子节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        // 把新建的 newFiber 赋值给 prevSibling,这样就方便为 newFiber 添加兄弟节点了
        prevSibling = newFiber
        
        // 索引值 + 1
        index++
    }
}

3. reconcileChildren

Сравните старые и новые волокна в reconcileChildren;

3.1 Когда старый и новый типы волокна одинаковы

Сохраняйте дом, обновляйте только реквизиты, установите для effectTag значение UPDATE;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    // oldFiber 可以在 wipFiber.alternate 中找到
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child

    while (index < elements.length || oldFiber != null) {
        const element = elements[index]
        let newFiber = null

        // fiber 类型是否相同
        const sameType =
            oldFiber &&
            element &&
            element.type == oldFiber.type

        // 如果类型相同,仅更新 props
        if (sameType) {
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: "UPDATE",
            }
        }
        // ~~省略~~
    }
    // ~~省略~~
}

3.2 Когда старые и новые типы волокон отличаются и есть новые элементы

Создайте новый узел dom и установите для параметра effectTag значение PLACEMENT;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    if (element && !sameType) {
        newFiber = {
            type: element.type,
            props: element.props,
            dom: null,
            parent: wipFiber,
            alternate: null,
            effectTag: "PLACEMENT",
        }
    }
    // ~~省略~~
}

3.3 Когда старые и новые волокна разного типа и есть старые волокна

Удалите старое волокно, установите для параметра effectTag значение DELETION;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    if (oldFiber && !sameType) {
        oldFiber.effectTag = "DELETION"
        deletions.push(oldFiber)
    }
    // ~~省略~~
}

4. deletions

Создайте новый массив удалений для хранения узлов волокна, которые нужно удалить, и при рендеринге DOM пройдите удаления, чтобы удалить старые волокна;

let deletions = null
function render (element, container) {
    // 省略
    // render 时,初始化 deletions 数组
    deletions = []
}

// 渲染 DOM 时,遍历 deletions 删除旧 fiber
function commitRoot () {
    deletions.forEach(commitWork)
}

5. commitWork

EffectTag волокна оценивается в commitWork и обрабатывается отдельно.

5.1 PLACEMENT

Когда effectTag волокна равен PLACEMENT, это означает, что добавляется новое волокно, и узел добавляется к родительскому узлу.

if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
) {
    domParent.appendChild(fiber.dom)
}

5.2 DELETION

Когда effectTag волокна равен PLACEMENT, это означает, что волокно удалено, а узел родительского узла удален.

else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
}

5.3 DELETION

Когда effectTag волокна имеет значение UPDATE, это означает, что волокно обновлено, и свойство props обновлено.

else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}

Функция updateDom обновляет свойство props в соответствии с различными типами обновлений.

const isProperty = key => key !== "children"

// 是否是新属性
const isNew = (prev, next) => key => prev[key] !== next[key]

// 是否是旧属性
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
    // 删除旧属性
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })

    // 更新新属性
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })
}

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

function updateDom(dom, prevProps, nextProps) {
    // ~~省略~~
    const isEvent = key => key.startsWith("on")
    //删除旧的或者有变化的事件
    Object.keys(prevProps)
        .filter(isEvent)
        .filter(
          key =>
            !(key in nextProps) ||
            isNew(prevProps, nextProps)(key)
        )
        .forEach(name => {
          const eventType = name
            .toLowerCase()
            .substring(2)
          dom.removeEventListener(
            eventType,
            prevProps[name]
          )
        })

    // 注册新事件
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
        const eventType = name
            .toLowerCase()
            .substring(2)
        dom.addEventListener(
            eventType,
            nextProps[name]
        )
    })
    // ~~省略~~
}

Заменить логику установки реквизита в creactDOM.

function createDom (fiber) {
    const dom = fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode("")
        : document.createElement(fiber.type)
    // 看这里鸭
    updateDom(dom, {}, fiber.props)
    return dom
}

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

/** @jsx myReact.createElement */
const container = document.getElementById("container")

const updateValue = e => {
    rerender(e.target.value)
}

const rerender = value => {
    const element = (
        <div>
            <input onInput={updateValue} value={value} />
            <h2>Hello {value}</h2>
        </div>
    )
    myReact.render(element, container)
}

rerender("World")

Полный исходный код этого примера см.reactDemo9

Результат показан на рисунке:

12.gif

7. Функциональные компоненты

Начнем с простого примера функционального компонента:

myReact пока не поддерживает функциональные компоненты. Следующий код сообщит об ошибке. Он используется только для сравнения общего использования функциональных компонентов.

/** @jsx myReact.createElement */
const container = document.getElementById("container")

function App (props) {
    return (
        <h1>hi~ {props.name}</h1>
    )
}

const element = (
    <App name='foo' />
)

myReact.render(element, container)

Между функциональными компонентами и компонентами HTML-тегов есть два отличия:

  • Слой функционального компонента не имеет узла dom;
  • Дочерние элементы функционального компонента должны быть получены после запуска функции;

Реализуйте функциональный компонент, выполнив следующие шаги:

  1. Измените PerformUnitOfWork, чтобы выполнить единицу работы с волокном в соответствии с типом волокна;
function performUnitOfWork(fiber) {
    // 是否是函数类型组件
    const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
    // 如果是函数组件,执行 updateFunctionComponent 函数
    if (isFunctionComponent) {
        updateFunctionComponent(fiber)
    } else {
    // 如果不是函数组件,执行 updateHostComponent 函数
        updateHostComponent(fiber)
    }
    // 省略
}
  1. Определите функцию updateHostComponent для выполнения нефункциональных компонентов;

Нефункциональные компоненты могут напрямую передавать fiber.props.children в качестве параметра.

function updateHostComponent(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    reconcileChildren(fiber, fiber.props.children)
}
  1. Определите функцию updateFunctionComponent для выполнения функционального компонента;

Функциональные компоненты должны быть запущены, чтобы получить fiber.children.

function updateFunctionComponent(fiber) {
    // fiber.type 就是函数组件本身,fiber.props 就是函数组件的参数
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}
  1. Измените функцию commitWork, чтобы она была совместима с волокнами без узлов dom;

4.1. Измените логику получения domParent и продолжайте поиск в цикле while до тех пор, пока не будет найдено родительское волокно с узлом dom;

function commitWork (fiber) {
    // 省略
    let domParentFiber = fiber.parent
    // 如果 fiber.parent 没有 dom 节点,则继续找 fiber.parent.parent.dom,直到有 dom 节点。
    while (!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent
    }
    const domParent = domParentFiber.dom
    // 省略
}

4.2 Изменить логику удаления узла При удалении узла нужно смотреть вниз, пока не найдете дочерний файбер с узлом dom;

function commitWork (fiber) {
    // 省略
    // 如果 fiber 的更新类型是删除,执行 commitDeletion
     else if (fiber.effectTag === "DELETION") {
        commitDeletion(fiber.dom, domParent)
    }
    // 省略
}

// 删除节点
function commitDeletion (fiber, domParent) {
    // 如果该 fiber 有 dom 节点,直接删除
    if (fiber.dom) {
        domParent.removeChild(fiber.dom)
    } else {
    // 如果该 fiber 没有 dom 节点,则继续找它的子节点进行删除
        commitDeletion(fiber.child, domParent)
    }
}

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

/** @jsx myReact.createElement */
const container = document.getElementById("container")

function App (props) {
    return (
        <h1>hi~ {props.name}</h1>
    )
}

const element = (
    <App name='foo' />
)

myReact.render(element, container)

Полный исходный код этого примера см.reactDemo10

Результат работы показан на рисунке:

image.png

8. hooks

Давайте продолжим добавлять функцию управления состоянием в myReact Ожидается, что функциональный компонент имеет свое собственное состояние и может получать и обновлять состояние.

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

function Counter() {
    const [state, setState] = myReact.useState(1)
    return (
        <h1 onClick={() => setState(c => c + 1)}>
        Count: {state}
        </h1>
    )
}
const element = <Counter />

Известно, что для получения и обновления состояния требуется метод useState.

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

Сделайте это, выполнив следующие действия:

  1. Новые глобальные переменные WIPFIBFIBR;
// 当前工作单元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
    wipFiber = fiber
    // 当前工作单元 fiber 的 hook
    wipFiber.hook = []
    // 省略
}
  1. Добавлена ​​функция useState;
// initial 表示初始参数,在本例中,initail=1
function useState (initial) {
    // 是否有旧钩子,旧钩子存储了上一次更新的 hook
    const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hook

    // 初始化钩子,钩子的状态是旧钩子的状态或者初始状态
    const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: [],
    }

    // 从旧的钩子队列中获取所有动作,然后将它们一一应用到新的钩子状态
    const actions = oldHook ? oldHook.queue : []
    actions.forEach(action => {
        hook.state = action(hook.state)
    })

    // 设置钩子状态
    const setState = action => {
        // 将动作添加至钩子队列
        hook.queue.push(action)
        // 更新渲染
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot,
        }
        nextUnitOfWork = wipRoot
        deletions = []
    }

    // 把钩子添加至工作单元
    wipFiber.hook = hook
    
    // 返回钩子的状态和设置钩子的函数
    return [hook.state, setState]
}

Запустим компонент подсчета, код такой:

function Counter() {
    const [state, setState] = myReact.useState(1)
    return (
        <h1 onClick={() => setState(c => c + 1)}>
        Count: {state}
        </h1>
    )
}
const element = <Counter />

Полный исходный код этого примера см.reactDemo11

Результат работы показан на рисунке:123.gif

В этой главе просто реализуется функция ловушек myReact.

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

Суммировать

Ссылка в этой статьеpomb.usИзучите и внедрите собственный React, включая виртуальный DOM, Fiber, алгоритм Diff, функциональные компоненты, хуки и другие функции.

В процессе реализации редактор имеет общее представление об основных терминах и идеях реализации React.pomb.usЭто очень подходящий учебный материал для начинающих, и его можно пройти непосредственно черезpomb.usДля обучения также рекомендуется следовать этой статье, чтобы шаг за шагом реализовать общие функции React.

Исходный код этой статьи:исходный код на гитхабе.

Рекомендуется следовать пошагово и выполнять практические упражнения.

Надеюсь, это поможет вам, спасибо за чтение ~

Не забудьте поставить палец вверх, чтобы подбодрить меня, пополнить ❤️

использованная литература