Оригинальная ссылка: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…