Следуя трендам, можно было обнаружить для себя React Signals. Возможно, вам попадался такой фрагмент кода:
import { signal } from "@preact/signals-react";
const count = signal(0);
export default function App() {
return <button onClick={() => count.value++}>{count.value}</button>;
}
Код вызывает удивление — как это сделано? Почему App
обновляется при изменении count
? Что позволило так сделать? Похоже на хуки, но нет, хуки нельзя использовать вне компонента…
Найдем код @preact/signals-react
Пакеты берутся из npm-реестра, найдем @preact/signals-react
на npmjs.com. На странице пакета есть ссылка на репозиторий модуля на GitHub. В репозитории есть несколько директорий: docs, patches, scripts, packages. Первые три нам сейчас не интересны — это документация и какие-то вспомогательные вещи, посмотрим в packages. В packages есть core, preact, react. preact — нас сейчас не интересует, core — нечто очень важное и общее, но нам нужно конкретное — react. Внутри найдем src/index.ts.
С чего начать — код выполняемый при инициализации модуля
Разберемся, что происходит при инициализации модуля, посмотрим, что делает код, выполняемый при импорте модуля. Проигнорируем, для начала, определения функций и внутренних переменных, у нас останется:
const JsxPro: JsxRuntimeModule = jsxRuntime;
const JsxDev: JsxRuntimeModule = jsxRuntimeDev;
React.createElement = WrapJsx(React.createElement);
JsxDev.jsx && /* */ (JsxDev.jsx = WrapJsx(JsxDev.jsx));
JsxPro.jsx && /* */ (JsxPro.jsx = WrapJsx(JsxPro.jsx));
JsxDev.jsxs && /* */ (JsxDev.jsxs = WrapJsx(JsxDev.jsxs));
JsxPro.jsxs && /* */ (JsxPro.jsxs = WrapJsx(JsxPro.jsxs));
JsxDev.jsxDEV && /**/ (JsxDev.jsxDEV = WrapJsx(JsxDev.jsxDEV));
JsxPro.jsxDEV && /**/ (JsxPro.jsxDEV = WrapJsx(JsxPro.jsxDEV));
// Decorate Signals so React renders them as <Text> components.
Object.defineProperties(Signal.prototype, {
$$typeof: { configurable: true, value: ReactElemType },
type: { configurable: true, value: ProxyFunctionalComponent(Text) },
props: {
configurable: true,
get() {
return { data: this };
},
},
ref: { configurable: true, value: null },
});
Последний блок кода (с Object.defineProperties
) добавляет всем сигналам возможность рендериться: $$typeof
, type
, props
, ref
— свойства всех React-элементов.
Остановимся подробней на первой части, в ней участвуют: jsx
, jsxs
(из react/jsx-runtime
), jsx
, jsxs
(из react/jsx-dev-runtime
), createElement
из React и некая функция WrapJsx
.
Что такое jsx-runtime?
React-компоненты преобразуются в вызовы React.createElement
, jsx
или jsxs
Например, такой код:
const Foo = () => (<button>Click Me</button>);
const Bar = () => (
<div>
<Foo />
</div>
);
будет преобразован компилятором в:
import { jsx as _jsx } from "react/jsx-runtime";
const Foo = () =>
_jsx("button", {
children: "Click Me"
});
const Bar = () =>
_jsx("div", {
children: _jsx(Foo, {})
});
В зависимости от настроек js-компилятора (например, babel), вместо React.createElement
может быть jsx
из react/jsx-runtime
, будем считать их эквивалентными.
React.createElement
— это функция создающая React-элемент. У нее три аргумента: type
, config
, children
. Первый аргумент type
— это тип элемента, может быть:
- строкой — для хост-элеметов:
div
,span
,main
; - объектом — в случае экзотичных React-элементов:
forwardRef
,memo
; - функцией — для функциональных или класс-компонентов.
Второй аргумент config
— это объект, содержащий в себе пропсы элемента, ref
и key
.
Последний аргумент children
— список дочерних элементов. Точно такую же роль выполняют функции jsx
, jsxs
, jsxDev
— создают React-элементы, имеют такие же аргументы.
Вернемся к коду инициализации модуля:
const JsxPro: JsxRuntimeModule = jsxRuntime;
const JsxDev: JsxRuntimeModule = jsxRuntimeDev;
React.createElement = WrapJsx(React.createElement);
JsxDev.jsx && /* */ (JsxDev.jsx = WrapJsx(JsxDev.jsx));
JsxPro.jsx && /* */ (JsxPro.jsx = WrapJsx(JsxPro.jsx));
JsxDev.jsxs && /* */ (JsxDev.jsxs = WrapJsx(JsxDev.jsxs));
JsxPro.jsxs && /* */ (JsxPro.jsxs = WrapJsx(JsxPro.jsxs));
JsxDev.jsxDEV && /**/ (JsxDev.jsxDEV = WrapJsx(JsxDev.jsxDEV));
JsxPro.jsxDEV && /**/ (JsxPro.jsxDEV = WrapJsx(JsxPro.jsxDEV));
Теперь понятно, что модуль переопределяет функции создания React-элементов результатом вызова функции WrapJsx
. Каждая функция: jsx
, jsxs
, jsxDEV
, createElment
преобразуется с помощью WrapJsx
.
Что делает функция-декоратор WrapJsx
?
Посмотрим код функции WrapJsx
:
function WrapJsx<T>(jsx: T): T {
if (typeof jsx !== "function") return jsx;
return function (type: any, props: any, ...rest: any[]) {
if (typeof type === "function" && !(type instanceof Component)) {
return jsx.call(jsx, ProxyFunctionalComponent(type), props, ...rest);
}
if (type && typeof type === "object") {
if (type.$$typeof === ReactMemoType) {
type.type = ProxyFunctionalComponent(type.type);
return jsx.call(jsx, type, props, ...rest);
} else if (type.$$typeof === ReactForwardRefType) {
type.render = ProxyFunctionalComponent(type.render);
return jsx.call(jsx, type, props, ...rest);
}
}
if (typeof type === "string" && props) {
for (let i in props) {
let v = props[i];
if (i !== "children" && v instanceof Signal) {
props[i] = v.value;
}
}
}
return jsx.call(jsx, type, props, ...rest);
} as any as T;
}
WrapJsx
вызывается с единственным аргументом jsx
— оригинальной функцией создания React-элемента и возвращает функцию с такими же как у jsx
аргументами.
WrapJsx
обрабатывает четыре сценария:
- Функциональный компонент или класс-компонент:
if (typeof type === "function" && !(type instanceof Component)) {
return jsx.call(jsx, ProxyFunctionalComponent(type), props, ...rest);
}
Если создается React-элемент, тип у которого — функция, значит элементу соответствует фукнциональный компонент или класс-компонент. Например, для такого jsx-выражения: <div><Foo>text</Foo></div>
под условие выше подходит Foo
. Внутри условия вызывается оригинальная функция jsx
, но вместо исходного type
передается ProxyFunctionalComponent(type)
, это эквивалентно оборачиванию Foo
в ProxyFunctionalComponent
:
const Foo = ProxyFunctionalComponent(props => {
return (
...
);
})
- Экзотичные React-элементы:
if (type && typeof type === "object") {
if (type.$$typeof === ReactMemoType) {
type.type = ProxyFunctionalComponent(type.type);
return jsx.call(jsx, type, props, ...rest);
} else if (type.$$typeof === ReactForwardRefType) {
type.render = ProxyFunctionalComponent(type.render);
return jsx.call(jsx, type, props, ...rest);
}
}
Если тип элемента — объект, значит создается экзотичный React-элемент, их два: memo
и forwardRef
. Оба эти элемента ссылаются на функциональный компонент, который нужно отрендерить. Элемент memo
ссылается на компонент через свойство type
, forwardRef
ссылается через свойство render
. Во фрагменте выше, они также оборачиваются в ProxyFunctionalComponent
.
- Хост-компоненты.
Хост-компоненты (div
, span
, main
и тд) попадают в третью ветку, в этом случае type
— это строка.
if (typeof type === "string" && props) {
for (let i in props) {
let v = props[i];
if (i !== "children" && v instanceof Signal) {
props[i] = v.value;
}
}
}
Для таких элементов просматривают все пропсы и, если среди них есть экземпляр Сигнала, он заменяется на его значение value
.
Таким образом, декоратор WrapJsx
оборачивает все пользовательские компоненты в ProxyFunctionalComponent
. При каждом обновлении (рендере) пользовательского компонента будет сначала происходить вызов ProxyFunctionalComponent
. Так как можно быть уверенным, что это происходит в момент обновления, внутри ProxyFunctionalComponent
можно использовать хуки, создавать локальный компоненту стейт, подписываться на события. @preact/signals-react
использует эту возможность, чтобы отслеживать обращения к Сигналам внутри компонента и вызывать обновление компонента, когда Сигнал изменяется.
Top comments (0)