Следуя трендам, можно было обнаружить для себя 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)