jennypollard
Posted on April 28, 2023
Следуя трендам, можно было обнаружить для себя 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
использует эту возможность, чтобы отслеживать обращения к Сигналам внутри компонента и вызывать обновление компонента, когда Сигнал изменяется.
Posted on April 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 6, 2024