Создайте свой собственный React в 90 строках кода

JavaScript React.js
Создайте свой собственный React в 90 строках кода

Оригинальная ссылка:Dev.to/ ах какие дети и ВАК...

Этот перевод был одобрен первоначальным автором:

Другие статьи можно нажать: GitHub.com/Иветт Л.А. Ю/Б…

Примечание переводчика:

Реализация в этой статье использует преимуществаsnabbdom, поэтому если вас интересует реализация виртуального DOM или базовая реализация рендеринга виртуального DOM в браузере, в этой статье это не рассматривается. Некоторые люди могут быть разочарованы этим, но мы должны делать это шаг за шагом.


текст:

Я не могу понять, что я не могу создать - Фейнман

Когда я изучал React, я думал, что все, что он делает, было волшебством, а потом я начал думать о том, что это за волшебство. Я был поражен, когда узнал, что все, что делает React, настолько просто, что даже если мы не являемся следующим крупным стартапом, повышающим ставки, его можно построить с очень небольшим количеством кода JS. Вот что побудило меня написать эту статью, и я надеюсь, что вы почувствуете то же самое после ее прочтения.

Какой функционал мы будем создавать?

  • JSX
  • функциональный компонент
  • компонент класса
  • Функция хука жизненного цикла

Что мы не собираемся строить?

виртуальный DOM

Опять же для простоты мы не будем реализовывать в этой статье собственный виртуальный DOM, мы будем использоватьsnabbdom,Интересно,Vue.jsВиртуальный DOM заимствует у него, подробнее об этом можно прочитать здесьsnabbdomСодержание:GitHub.com/yes, возьмите BB Dom/Suning…

React Hooks

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

отлаживаемость

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

Производительность и совместимость

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

Давайте начнем

Прежде чем мы начнем, нам нужен каркас, поддерживающий ES6, с автоматическими горячими обновлениями. Я создал очень простойwebpackСкаффолдинг можно клонировать и настроить:GitHub.com/Ahh Children и HAC…

JSX

JSXявляется открытым стандартом, который не ограничиваетсяReact, мы можем сделать это безReactЭто проще, чем вы думаете. Хотите узнать, как сделать так, чтобы наша библиотека поддерживалаJSX, нам сначала нужно посмотреть, где мы используемJSXчто происходило за кулисами.

const App = (
    <div>
        <h1 className="primary">QndReact is Quick and dirty react</h1>
        <p>It is about building your own React in 90 lines of JavsScript</p>
    </div>
);

// 上面的 jsx 被转换成下面这样:
/**
 * React.createElement(type, attributes, children)
 */
var App = React.createElement(
    "div",
    null,
    React.createElement(
        "h1",
        {
            className: "primary"
        },
        "QndReact is Quick and dirty react"
    ),
    React.createElement(
        "p",
        null,
        "It is about building your own React in 90 lines of JavsScript"
    )
);

Как видите, каждыйJSXэлементы проходят через@babel/plugin-transform-react-jsxПлагин преобразуется вReact.createElement(...)В виде вызова функции можноздесьиспользоватьJSXсовершать больше конверсий

Для того, чтобы приведенное выше преобразование работало корректно, напишитеJSXкогда вам нужно импортироватьReact, ты почему когда не вводишьReactкогда пишешьJSXПричина ошибки появится.@babel/plugin-transform-react-jsxПлагин был добавлен в наши проектные зависимости, давайте сначала установим зависимости

npm install

Добавьте конфигурацию проекта на.babelrcВ файле:

{
    "plugins": [
        [
            "@babel/plugin-transform-react-jsx",
            {
                "pragma": "QndReact.createElement", // default pragma is React.createElement
                "throwIfNamespace": false // defaults to true
            }
        ]
    ]
}

После этого, покаBabelВидетьJSX, он позвонитQntReact.createElement(...), но мы еще не определили эту функцию, теперь запишем ее какsrc/qnd-react.jsсередина.

const createElement = (type, props = {}, ...children) => {
    console.log(type, props, children);
};

// 像 React.createElement 一样导出
const QndReact = {
    createElement
};

export default QndReact;

Мы распечатаем консоль, переданную намtype,props,children. Чтобы проверить, работает ли наше преобразование, мы можем сделать это вsrc/index.jsнаписать немногоJSX.

// QndReact 需要被引入
import QndReact from "./qnd-react";

const App = (
    <div>
        <h1 className="primary">
            QndReact is Quick and dirty react
        </h1>
        <p>It is about building your own React in 90 lines of JavsScript</p>
    </div>
);

Стартовый проект:npm start, введите в браузереlocalhost:3000Теперь ваша консоль выглядит примерно так, как показано на рисунке ниже:

Согласно приведенной выше информации, мы можем использоватьsnabbdomСоздайте наш внутреннийузел виртуального DOM, и тогда мы можем использовать его для нашей координации (reconciliation), вы можете использовать следующую команду для установкиsnabbdom:

npm install snabbdom

когдаQndReact.createElement(...)При вызове создать и вернутьузел виртуального DOM.

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    return h(type, { props }, children);
};

const QndReact = {
    createElement
};

export default QndReact;

Отлично, теперь мы можем разобратьJSXи создать свой собственный виртуальный DOM-узел, но он по-прежнему не отображается в браузере. С этой целью мыsrc/qnd-react-dom.jsдобавить одинrenderметод.

//src/qnd-react-dom.js

//React.render(<App />, document.getElementById('root'));
const render = (el, rootElement) => {
    //将el渲染到rootElement的逻辑
}

const QndReactDom = {
    render
}

Вместо того, чтобы обрабатывать это сами, поместив элемент вDOMтяжелая работа наsnabbdomиметь дело с. Для этого мы можем ввести модули для инициализацииsnabbdom.snabbdomМодули можно рассматривать как плагины, которые могут поддерживатьsnabbdomсделать больше.

//src/qnd-react-dom.js
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
const reconcile = snabbdom.init([propsModule]);

const render = (el, rootDomElement) => {
    //将el渲染到rootElement
    reconcile(rootDomElement, el);
}

const QndReactDom = {
    render
}
export default QndReactDom;

мы используем этот новыйrenderфункция идтиsrc/indexчтобы сделать немного магии.

//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
const App = (
    <div>
        <h1 className="primary">
            QndReact is Quick and dirty react
        </h1>
        <p>It is about building your own React in 90 lines of JavsScript</p>
    </div>
);

QndReactDom.render(App, document.getElementById('root'));

Вуаля, наш JSX готов к отображению на экране.

подождите, с этим есть небольшая проблема, когда мы вызываем его дваждыrender, мы видим какие-то странные ошибки в консоли (Примечание переводчика: может вызываться несколько раз в index.jsrender, см. ошибку консоли), причина в том, что мы можем вызывать настоящий DOM-узел только при первом рендеринге.reconcileметод, который мы затем должны вызвать на виртуальном узле DOM, который был возвращен ранее.

//src/qnd-react-dom.js
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';

const reconcile = snabbdom.init([propsModule]);

let rootVNode;
//QndReactDom.render(App, document.getElementById('root'))
const render = (el, rootDomElement) => {
    if(rootVNode == null) {
        //第一次调用 render 时
        rootVNode = rootDomElement;
    }
    rootVNode = reconcile(rootVNode, el);
}

const QndReactDom = {
    render
}
export default QndReactDom;

Мы очень рады, что у нас есть рабочий рендеринг JSX в нашем приложении, теперь давайте начнем рендеринг функционального компонента вместо простого HTML.

разрешите намsrc/index.jsдобавить одинGreetingФункциональные компоненты, такие как:

//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';

const Greeting = ({ name }) => <p>Welcome {name}!</p>;

const App = (
    <div>
        <h1 className="primary">
            QndReact is Quick and dirty react
        </h1>
        <p>It is about building your own React in 90 lines of JavsScript</p>
        <Greeting name={"Ameer Jhan"} />
    </div>
);

QndReactDom.render(App, document.getElementById('root'));

В этот момент в консоли появляется следующая ошибка:

мы можемQndReact.createElement(...)Распечатайте данные в методе, чтобы понять, почему.

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(type, props, children);
    return h(type, { props }, children);
};
...

Если вы видите, функциональный компонент передаетсяtypeявляется JS-функцией. Если мы вызовем эту функцию, мы сможем получить то, что компонент хочет отобразить.HTMLрезультат.

Мы основаны наtypeТип параметра, если это тип функции, мы вызываем функцию и передаемpropsПередайте его как параметр, если это не тип функции, мы рассматриваем его как обычныйHTMLОбработка элемента.

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};

const QndReact = {
    createElement
};

export default QndReact;

радость! Наш функциональный компонент уже работает нормально.

Мы много сделали, давайте сделаем глубокое дыхание и иметь кофе, потому что мы почти тамReact, но нам все еще нужно заняться компонентом класса.

мы первыеsrc/qnd-react.jsсоздан вComponentБазовый класс:

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};


class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { }

    render() { }
}


const QndReact = {
    createElement,
    Component
};

export default QndReact;

теперь мыsrc/counter.jsпишем наш первый вCounterКомпонент класса:

//src/counter.js
import QndReact from './qnd-react';

export default class Counter extends QndReact.Component {
    constructor(props) {
        super(props);

        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        console.log('Component mounted');
    }

    render() {
        return <p>Count: {this.state.count}</p>
    }
}

Да, я знаю, что мы еще не реализовали никакой логики в счетчике, но не волнуйтесь, мы добавим их, как только наша система управления состоянием будет запущена. Теперь давайте попробуемsrc/index.jsсделать это.

//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
import Counter from "./counter";

const Greeting = ({ name }) => <p>Welcome {name}!</p>;

const App = (
    <div>
        <h1 className="primary">
            QndReact is Quick and dirty react
        </h1>
        <p>It is about building your own React in 90 lines of JavsScript</p>
        <Greeting name={"Ameer Jhan"} />
        <Counter />
    </div>
);

QndReactDom.render(App, document.getElementById('root'));

Как и ожидалось, ошибка повторилась.

Вышеупомянутая ошибка выглядит знакомо, когда вы пытаетесь использовать компоненты класса без интеграции себя?React.Component, возможно, вы столкнулись с вышеуказанными ошибками. Чтобы узнать, почему это так, мы можемReact.createElement(...)добавитьconsole.log,Следующим образом:

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(typeof (type), type);
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};

Посмотрим, что напечатает консоль.

ты можешь видетьCounterизtypeТипы также являются функциями, потому чтоBabelПреобразует классы ES6 в обычные функции JS, так как же мы классифицируем компоненты. На самом деле, мы можемComponentДобавьте статическое свойство в базовый класс, поэтому мы используем это свойство для проверкиtypeЯвляется ли параметр классом.Reactтакже такая же логика обработки, вы можете прочитатьблог Дэна

//src/qnt-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(typeof (type), type);
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};


class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { }

    render() { }
}
//给 Component 组件添加静态属性来区分是函数还是类 
Component.prototype.isQndReactClassComponent = true;

const QndReact = {
    createElement,
    Component
};

export default QndReact;

Теперь мыQndReact.createElement(...)Добавьте код для обработки компонентов класса.

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(type.prototype);
    /**
     * 如果是类组件
     * 1.创建一个实例
     * 2.调用实例的 render 方法
     */
    if (type.prototype && type.prototype.isQndReactClassComponent) {
        const componentInstance = new type(props);

        return componentInstance.render();
    }
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};


class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { }

    render() { }
}
//给 Component 组件添加静态属性来区分是函数还是类 
Component.prototype.isQndReactClassComponent = true;

const QndReact = {
    createElement,
    Component
};

export default QndReact;

Теперь наш компонент класса можно отобразить в браузере:

Добавляем в класс компонентstate, прежде чем нам нужно знать, что каждый вызовthis.setState({})Когда ответственность за обновление DOM лежит наreact-domпакет вместоReactОбязанность. это сделатьReactосновная часть, напр.ComponentКлассы отделены от платформы, что повышает возможность повторного использования кода. то естьReactNative, вы также можете использовать тот жеComponentДобрый,react-nativeОтвечает за обновление пользовательского интерфейса. Вы можете спросить себя: при звонкеthis.setState(...)час,ReactКак узнать, что делать, ответreact-domвReactустановить__updaterатрибут сReactобщаться. У Дэна также есть отличная статья на эту тему, которую вы можете прочитать здесь. Давайте сейчасQndReactDomЧжунвэйQndReactДобавить к__updaterАтрибуты.

//src/qnd-react-dom.js
import QndReact from './qnd-react';
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';

...

//QndReactDom 告诉 QndReact 如何更新 DOM
QndReact.__updater = () => {
    //当调用 this.setState 的时候更新 DOM 逻辑
}

всякий раз, когда мы звонимthis.setState({...}), нам всем нужно сравнить компоненты'oldVNodeи на компоненте, называемомrenderгенерируется после методаnewVNode. Для сравнения добавляем в класс компонент__vNodeсвойство для поддержания текущего состояния компонентаVNodeпример.

//src/qnd-react.js
...
const createElement = (type, props = {}, ...children) => {
    /**
     * 如果是类组件
     * 1.创建一个实例
     * 2.调用实例的 render 方法
     */
    if (type.prototype && type.prototype.isQndReactClassComponent) {
        const componentInstance = new type(props);
        componentInstance.__vNode = componentInstance.render();
        return componentInstance.__vNode;
    }
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};
...

теперь мы пришлиComponentреализован в базовом классеsetStateметод.

//src/qnd-react.js
...
class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { 
        this.state = {
            ...this.state,
            ...partialState
        }
        //调用 QndReactDom 提供的 __updater 方法
        QndReact.__updater(this);
    }

    render() { }
}
...

Обработка QndReactDom__updaterметод.

//src/qnd-react-dom.js
...
QndReact.__updater = (componentInstance) => {
    //当调用 this.setState 的时候更新 DOM 逻辑
    //获取在 __vNode 上存储的 oldVNode
    const oldVNode = componentInstance.__vNode;
    //获取 newVNode
    const newVNode = componentInstance.render();
    //更新 __vNode
    componentInstance.__vNode = reconcile(oldVNode, newVNode);
}
...
export default QndReactDom;

Хорошо, мыCounterУвеличение компонентовstateпроверить нашиsetStateВступает ли реализация в силу.

//src/counter.js
import QndReact from './qnd-react';

export default class Counter extends QndReact.Component {
    constructor(props) {
        super(props);

        this.state = {
            count: 0
        }

        // update the count every second
        setInterval(() => {
            this.setState({
                count: this.state.count + 1
            })
        }, 1000);
    }

    componentDidMount() {
        console.log('Component mounted');
    }

    render() {
        return <p>Count: {this.state.count}</p>
    }
}

отлично, сейчасCounterКомпоненты ведут себя именно так, как мы и ожидали.

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

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    /**
     * 如果是类组件
     * 1.创建一个实例
     * 2.调用实例的 render 方法
     */
    if (type.prototype && type.prototype.isQndReactClassComponent) {
        const componentInstance = new type(props);
        componentInstance.__vNode = componentInstance.render();
        return componentInstance.__vNode;

        //增加钩子函数(当虚拟DOM被添加到真实DOM节点上时)
        componentInstance.__vNode.data.hook = {
            create: () => {
                componentInstance.componentDidMount()
            }
        }
    }
    //如果是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};

...

export default QndReact;

До сих пор мы поддерживали компоненты классаcomponentDidMountФункция хука жизненного цикла.

Прежде чем мы закончим, давайте добавим поддержку привязки событий. Для этого мы можемCounterВ компонент добавлена ​​кнопка, при нажатии на которую увеличивается число счетчика. Обратите внимание, что мы следуем обычным соглашениям об именах событий JS, а не основанным наReact, то есть использование события двойного щелчкаonDblClick, вместоonDoubleClick.

import QndReact from './qnd-react';

export default class Counter extends QndReact.Component {
    constructor(props) {
        super(props);

        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        console.log('Component mounted');
    }

    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={() => this.setState({
                    count: this.state.count + 1
                })}>Increment</button>
            </div>
        )
    }
}

Компоненты выше не будут работать, потому что мы не сказали нашемуVDomКак с этим бороться. Во-первых, мы даемSnabdomДобавлен модуль мониторинга событий.

//src/qnd-react-dom.js
import QndReact from './qnd-react';
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
import eventlistenersModule from 'snabbdom/modules/eventlisteners';

const reconcile = snabbdom.init([propsModule, eventlistenersModule]);

...

SnabdomЧтобы иметь свойство текста и свойство события как два отдельных объекта, нам нужно сделать следующее:

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    ...

    let dataProps = {};
    let eventProps = {};

    for (let propKey in props) {
        // event 属性总是以 `on` 开头
        if (propKey.startsWith('on')) {
            const event = propKey.substring(2).toLowerCase();
            eventProps[event] = props[propKey];
        } else {
            dataProps[propKey] = props[propKey];
        }
    }
    return h(type, { props: dataProps, on: eventProps }, children);
};

...

Теперь, когда мы нажимаемCounterПри нажатии кнопки компонента счетчик увеличивается на 1.

Отлично, у нас наконец-то есть скромная реализация React. Тем не менее, мы пока не можем представить список, и я хотел бы оставить его вам в качестве небольшой забавной задачи. я предлагаю вам попробоватьsrc/index.jsпредставить список вQndReact.createElement(...)способ найти проблему.

Спасибо, что остаетесь со мной, надеюсь, вам понравится создавать свои собственныеReact, и узналReactКак этот процесс работает. Если вы никуда застряли, не стесняйтесь ссылаться на код, который я поделился:GitHub.com/Ahh Children и HAC…


Подпишитесь на официальный аккаунт и присоединитесь к группе технического обмена