Анатомия StyleX
- четверг, 7 марта 2024 г. в 00:00:20
Hello world!
По данным 2023 JavaScript Rising Stars библиотека StyleX заняла второе место в разделе Styling / CSS in JS (первое место вполне ожидаемо занял TailwindCSS).
stylex
— это решение CSS в JS
от Facebook
, которое недавно стало открытым и быстро набрало популярность (на сегодняшний день у библиотеки 7500 звезд на Github). Это обусловлено ее легковесностью, производительностью и небольшим размером итоговой таблицы стилей.
В этой статье мы разберемся, как stylex
работает. Но давайте начнем с примера ее использования.
Код проекта, который мы создадим, включая выдержки из исходного кода stylex
(директория stylex
), можно найти здесь.
Пример
В качестве примера создадим простой компонент кнопки, предусматривающий разные варианты стилизации.
Создаем шаблон приложения с помощью Vite:
# stylex-testing - название проекта
# react-ts - используемый шаблон
yarn create vite stylex-testing --template react-ts
# или
npm create vite@latest stylex-testing -- --template react-ts
Переходим в директорию и устанавливаем зависимости:
cd stylex-testing
yarn add @stylexjs/stylex
yarn add -D vite-plugin-stylex-dev
# или
npm i @stylexjs/stylex
npm i -D vite-plugin-stylex-dev
vite-plugin-stylex-dev — неофициальный плагин stylex
для vite
.
Редактируем файл vite.config.ts
:
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { stylexPlugin } from 'vite-plugin-stylex-dev'
export default defineConfig({
plugins: [react(), stylexPlugin()],
})
Определяем минимальные стили в файле index.css
:
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
}
Переходим к stylex
.
Начнем с определения переменных/токенов. Создаем файл tokens.stylex.ts
следующего содержания:
import stylex from '@stylexjs/stylex'
// Медиа-запрос темной цветовой схемы
const DARK = '@media (prefers-color-scheme: dark)'
// Цвета
export const colors = stylex.defineVars({
light: { default: '#FBFBFB', [DARK]: '#332D2D' },
dark: { default: '#332D2D', [DARK]: '#FBFBFB' },
primary: '#3B71CA',
success: '#14A44D',
})
// Отступы
export const spacing = stylex.defineVars({
none: '0px',
xsmall: '4px',
small: '8px',
medium: '12px',
large: '16px',
})
Создаем файл components/Button.tsx
для компонента кнопки. Определяем стили кнопки:
import stylex, { type StyleXStyles } from '@stylexjs/stylex'
import type { HTMLAttributes, PropsWithChildren } from 'react'
import { colors, spacing } from '../tokens.stylex'
// 3 варианта стилизации
const styles = stylex.create({
default: {
// Используем переменные
// https://stylexjs.com/docs/learn/theming/using-variables/
padding: `${spacing.small} ${spacing.large}`,
backgroundColor: colors.light,
border: `1px solid ${colors.dark}`,
outline: 'none',
borderRadius: spacing.small,
boxShadow: {
default: '0 2px 3px rgba(0, 0, 0, 0.25)',
':active': 'none',
},
cursor: 'pointer',
transition: 'all 0.25s ease-in-out',
':hover': {
backgroundColor: colors.dark,
color: colors.light,
},
},
primary: {
backgroundColor: colors.primary,
color: colors.light,
':hover': {
backgroundColor: null,
color: null,
},
},
dark: {
backgroundColor: colors.dark,
color: colors.light,
':hover': {
backgroundColor: colors.light,
color: colors.dark,
},
},
})
Определение стилей с помощью stylex
сильно напоминает то, как это делается в React Native
.
Определяем типы пропов и компонент кнопки:
type Props = PropsWithChildren<
HTMLAttributes<HTMLButtonElement> & {
// Вариант перезаписывает стилизацию `default`
variant?: 'primary' | 'dark'
// Кастомные цвета фона и текста, определенные с помощью `stylex`
// С точки зрения типизации стилей `stylex` лучше `tailwindcss`
customStyles?: StyleXStyles<{
backgroundColor?: string
color?: string
}>
}
>
export default function Button({
children,
variant,
customStyles,
...rest
}: Props) {
return (
<button
// Применяем стили
// https://stylexjs.com/docs/learn/styling-ui/using-styles/
{...stylex.props(
styles.default,
variant && styles[variant],
customStyles,
)}
{...rest}
>
{children}
</button>
)
}
Определяем стили контейнера и кастомной кнопки и рендерим несколько кнопок в файле App.tsx
:
import stylex from '@stylexjs/stylex'
import Button from './components/Button'
import { colors, spacing } from './tokens.stylex'
// Стили контейнера и кастомной кнопки
const styles = stylex.create({
app: {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: spacing.medium,
},
customButton: {
backgroundColor: colors.success,
color: colors.light,
},
})
export default function App() {
return (
<div {...stylex.props(styles.app)}>
<Button>Default</Button>
<Button variant='dark'>Dark</Button>
<Button variant='primary'>Primary</Button>
<Button customStyles={styles.customButton}>
Custom
</Button>
</div>
)
}
Запускаем сервер для разработки:
yarn dev
# или
npm run dev
Результат:
Это почти весь функционал, предоставляемый stylex
. Мы не рассмотрели только создание тем путем перезаписи переменных, но, думаю, вы сами легко с этим разберетесь.
Давайте взглянем на стили и пропы, генерируемые stylex
. Редактируем App.tsx
следующим образом:
import stylex from '@stylexjs/stylex'
const stylesForLog = stylex.create({
root: {
display: 'flex',
flexDirection: 'column',
},
})
const stylesForLog2 = stylex.create({
root2: {
display: 'flex',
},
})
// Утилита для красивого вывода
const stringify = (obj: Record<string, unknown>) => JSON.stringify(obj, null, 2)
export default function App() {
console.log('result of create:', stringify(stylesForLog))
console.log('result of create 2:', stringify(stylesForLog2))
console.log(stylesForLog.root.display === stylesForLog2.root2.display)
console.log(
'result of props:',
stringify(stylex.props(stylesForLog.root, stylesForLog2.root2)),
)
console.log(
'result of props 2:',
stringify(
// @ts-ignore
stylex.props(stylesForLog.root, stylesForLog2.root2, { display: 'flex' }),
),
)
return (
<div {...stylex.props(stylesForLog.root)}></div>
)
}
Вот что мы видим в консоли:
Строки типа App__stylesForLog.root
нужны для отладки, поэтому их можно игнорировать.
display: "flex"
превратилось в display: "x78zum5"
, причем поле display
в обоих случаях имеет одно и тоже значение, что подтверждается сравнением stylesForLog.root.display === stylesForLog2.root2.display
. Это наводит на мысль о том, что строки типа x78zum5
— это хеш правил CSS
(свойство: значение
).
Значения полей объекта styles
объединяются и становятся значением поля className
объекта props
(например, className: "xdt5ytf x78zum5"
). className
путем распаковки объекта props
передается компоненту (устанавливается элементу button
).
При передаче display: flex
в виде простого объекта возвращается поле style
со значением в виде этого объекта. При этом, соответствующий хеш-класс опускается.
Запомните поле $$css: true
, мы вернемся к нему позже.
Взглянем на разметку:
В таблице стилей с data-vite-dev-id=" vite-plugin:stylex.css"
мы видим "хеш-классы" и соответствующие им стили: .x78zum5{display:flex}.xdt5ytf{flex-direction:column}
. Те же хеш-классы мы видим у контейнера: <div class="x78zum5 xdt5ytf"></div>
.
Выполняем сборку приложения:
yarn build
# или
npm run build
Открываем файл dist/assets/index-[hash].css
:
/* stylex */
.x78zum5 {
display: flex;
}
.xdt5ytf {
flex-direction: column;
}
/* index.css */
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
}
Отлично. Начнем погружаться в исходный код stylex
.
Реверс-инжиниринг
Обратите внимание: дальнейший разбор кода актуален для stylex@0.4.1
. В будущем код может и наверняка изменится, возможно, до неузнаваемости 😊
Также обратите внимание, что с целью упрощения кода для облегчения его восприятия я беспощадно удалял строки и даже целые блоки кода 😁
Мы ограничимся изучением работы методов create
(создание стилей) и props
(применение стилей).
Выдержки из кода соответствуют порядку применения переменных и функций, а не порядку их определения.
Начнем с файла packages/stylex/src/stylex.js
:
// 294 - номер строки кода
export default _stylex
// 239
function _stylex(...styles) {
const [className] = styleq(styles)
return className
}
_stylex.props = props
_stylex.create = create
// 36
// Об этой библиотеке поговорим позже
import { styleq } from 'styleq'
// 136
export const create = stylexCreate
// 76
function stylexCreate(styles) {
if (__implementations.create != null) {
const create = __implementations.create
return create(styles)
}
throw new Error(
'stylex.create should never be called. It should be compiled away.',
)
}
// 280
const __implementations = {}
// 282
export function __monkey_patch__(key, implementation) {
__implementations[key] = implementation
}
Метод create
извлекается из объекта __implementations
, который инициализируется с помощью функции __monkey_patch__
.
Следуем за __monkey_patch__()
в файл packages/dev-runtime/src/index.js
:
// 10
import { __monkey_patch__ } from '@stylexjs/stylex'
import { styleSheet } from '@stylexjs/stylex/lib/StyleXSheet';
// 20
import getStyleXCreate from './stylex-create';
// 45
export default function inject({
insert = defaultInsert,
...config
}) {
// Инициализация `create()`
__monkey_patch__('create', getStyleXCreate({ ...config, insert }));
}
// 24
const defaultInsert = (
key,
ltrRule,
priority,
// На это можно не обращать особого внимания,
// это стили для направления текста "справа налево"
rtlRule,
) => {
if (priority === 0) {
if (injectedVariableObjs.has(key)) {
throw new Error('A VarGroup with this name already exists: ' + key);
} else {
injectedVariableObjs.add(key);
}
}
styleSheet.insert(ltrRule, priority, rtlRule);
};
// 22
const injectedVariableObjs = new Set();
Метод create
— это функция, возвращаемая getStyleXCreate({ ...config, defaultInsert })
.
В функции defaultInsert
вызывается метод insert
объекта styleSheet
. Этот объект создается в файле packages/stylex/src/StyleXSheet.js
:
// 364
export const styleSheet = new StyleXSheet({
supportsVariables: true,
rootTheme: {},
rootDarkTheme: {},
});
// 85
export class StyleXSheet {
static LIGHT_MODE_CLASS_NAME = LIGHT_MODE_CLASS_NAME;
static DARK_MODE_CLASS_NAME = DARK_MODE_CLASS_NAME;
constructor(opts) {
this.tag = null;
this.injected = false;
this.ruleForPriority = new Map();
this.rules = [];
this.rootTheme = opts.rootTheme;
this.rootDarkTheme = opts.rootDarkTheme;
}
// Цветовые схемы
rootTheme;
rootDarkTheme;
// Массив, содержащий все добавленные правила. Используется для
// отслеживания индексов правил в таблице стилей
rules;
// Индикатор добавления тега `style` в `document`
injected;
// Элемент `style` для добавления правил
tag;
// Для поддержки приоритетов необходимо хранить правило,
// которое находится в начале приоритета
ruleForPriority;
/**
* Извлекает тег `style`
*/
getTag() {
const { tag } = this;
invariant(tag != null, 'expected tag');
return tag;
}
/**
* Добавляет тег `style` в `head`
*/
inject() {
if (this.injected) {
return;
}
this.injected = true;
// Создаем тег `style`
this.tag = makeStyleTag();
this.injectTheme();
}
/**
* Вставляет стили темы - переменные/токены
*/
injectTheme() {
if (this.rootTheme != null) {
this.insert(
buildTheme(`:root, .${LIGHT_MODE_CLASS_NAME}`, this.rootTheme),
0,
);
}
if (this.rootDarkTheme != null) {
this.insert(
buildTheme(
`.${DARK_MODE_CLASS_NAME}:root, .${DARK_MODE_CLASS_NAME}`,
this.rootDarkTheme,
),
0,
);
}
}
/**
* Добавляет правила в таблицу стилей
*/
insert(rawLTRRule, priority) {
// Добавляем таблицу стилей при отсутствии
if (this.injected === false) {
this.inject();
}
const rawRule = rawLTRRule;
// Не добавляем правило при наличии (исключаем дубликаты)
if (this.rules.includes(rawRule)) {
return;
}
// Нормализованное правило с определенной специфичностью,
// это нас не интересует
const rule = this.normalizeRule(
addSpecificityLevel(rawRule, Math.floor(priority / 1000)),
);
// Получаем позицию для вставки правила по его приоритету,
// это нас не интересует
const insertPos = this.getInsertPositionForPriority(priority);
this.rules.splice(insertPos, 0, rule);
// Устанавливаем правило как конец группы приоритета
this.ruleForPriority.set(priority, rule);
const tag = this.getTag();
const sheet = tag.sheet;
if (sheet != null) {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
sheet.insertRule(rule, Math.min(insertPos, sheet.cssRules.length));
} catch (err) {
console.error('insertRule error', err, rule, insertPos);
}
}
}
}
// 344
/**
* Функция добавления `:not(#\#)` для повышения специфичности; полифилл для @layer
*/
function addSpecificityLevel(selector, index) {
if (selector.startsWith('@keyframes')) {
return selector;
}
// Чем больше `not(#\\#)`, тем выше специфичность
const pseudo = Array.from({ length: index })
.map(() => ':not(#\\#)')
.join('');
const lastOpenCurly = selector.includes('::')
? selector.indexOf('::')
: selector.lastIndexOf('{');
const beforeCurly = selector.slice(0, lastOpenCurly);
const afterCurly = selector.slice(lastOpenCurly);
return `${beforeCurly}${pseudo}${afterCurly}`;
}
// 45
/**
* Создает тег `style` и добавляет его в `head`
*/
function makeStyleTag() {
const tag = document.createElement('style');
tag.setAttribute('type', 'text/css');
tag.setAttribute('data-stylex', 'true');
const head = document.head || document.getElementsByTagName('head')[0];
invariant(head, 'expected head');
head.appendChild(tag);
return tag;
}
// 28
/**
* Принимает тему и генерирует переменные CSS
*/
function buildTheme(selector, theme) {
const lines = [];
lines.push(`${selector} {`);
for (const key in theme) {
const value = theme[key];
lines.push(` --${key}: ${value};`);
}
lines.push('}');
return lines.join('\n');
}
Здесь основной является строка sheet.insertRule(rule, position);
, отвечающая за однократное добавление переданного правила в таблицу стилей с учетом его приоритета.
Возвращаемся в файл packages/dev-runtime/src/index.js
и следуем за getStyleXCreate
в файл packages/dev-runtime/src/stylex-create.js
:
// 177
export default function getStyleXCreate(
config,
) {
const stylexCreate = (
styles,
) => {
return createWithFns(styles, config);
};
return stylexCreate;
}
// 106
// Значением поля объекта стилей может быть функция,
// мы не будем рассматривать такой вариант
function createWithFns(
styles,
{ insert, ...config },
) {
const stylesWithoutFns = {};
for (const key in styles) {
const value = styles[key];
stylesWithoutFns[key] = value;
}
// Одна из самых важных строк!
const [compiledStyles, injectedStyles] = create(stylesWithoutFns, config);
// Добавляем хеш-классы и стили в таблицу стилей
for (const key in injectedStyles) {
const { ltr, priority, rtl } = injectedStyles[key];
insert(key, ltr, priority, rtl);
}
const temp = compiledStyles;
const finalStyles = { ...temp };
// Возвращаем скомпилированные стили
return finalStyles;
}
// 16
import { create, IncludedStyles, utils } from '@stylexjs/shared';
Функция getStyleXCreate
возвращает функцию stylexCreate
, которая возвращает функцию createWithFns
, которая вызывает функцию create
, добавляет одни стили (injectedStyles
) в таблицу стилей и возвращает другие (finalStyles
). finalStyles
— это стили, которые впоследствии передаются в метод stylex.props
, которая находится в файле packages/stylex/src/stylex.js
:
// 47
export function props(
this,
...styles
) {
const options = this;
if (__implementations.props) {
return __implementations.props.call(options, styles);
}
// Мы подробнее поговорим об этой библиотеке в конце
const [className, style] = styleq(styles);
const result = {};
if (className != null && className !== '') {
result.className = className;
}
if (style != null && Object.keys(style).length > 0) {
result.style = style;
}
// result = { className: "хеш-классы", style: { ...стили } }
return result;
}
Следуем за функцией create
в файл packages/shared/src/index.js
:
// 43
export const create = styleXCreateSet;
// 23
import styleXCreateSet from './stylex-create';
Без комментариев 😊 Следуем за функцией styleXCreateSet
в файл packages/shared/src/stylex-create.js
:
// 24
// Эта функция принимает объект со стилями, передаваемый в метод `stylex.create` и преобразует его.
// Преобразование заключается в замене значений стилей на названия CSS-классов.
//
// Функция также собирает все внедряемые (injected) стили.
// Возвращает кортеж с преобразованным объектом стилей и объектом внедряемых стилей.
//
// Перед возвратом выполняется проверка отсутствия дубликатов во внедряемых стилях.
export default function styleXCreateSet(
namespaces,
options = defaultOptions,
) {
const resolvedNamespaces = {};
const injectedStyles = {};
for (const namespaceName of Object.keys(namespaces)) {
const namespace = namespaces[namespaceName];
const flattenedNamespace = flattenRawStyleObject(namespace, options);
const compiledNamespaceTuples = flattenedNamespace.map(([key, value]) => {
return [key, value.compiled(options)];
});
const compiledNamespace = objFromEntries(compiledNamespaceTuples);
const namespaceObj = {};
for (const key of Object.keys(compiledNamespace)) {
const value = compiledNamespace[key];
if (value instanceof IncludedStyles) {
namespaceObj[key] = value;
} else {
const classNameTuples =
value.map((v) => (Array.isArray(v) ? v : null)).filter(Boolean);
const className =
classNameTuples.map(([className]) => className).join(' ') || null;
namespaceObj[key] = className;
for (const [className, injectable] of classNameTuples) {
if (injectedStyles[className] == null) {
injectedStyles[className] = injectable;
}
}
}
}
// `$$css: true` требуется для `styleq`
resolvedNamespaces[namespaceName] = { ...namespaceObj, $$css: true };
}
return [resolvedNamespaces, injectedStyles];
}
Весь рассмотренный код можно найти в файле stylex/source-code.js
нашего проекта.
Код функции styleXCreateSet
и всех используемых ей утилит занимает почти 800 строк. Я вынес его в отдельный файл stylex/create-props.js
. Вы можете внимательно изучить его самостоятельно, я же остановлюсь только на основных моментах.
Посмотрим на результат, возвращаемый функцией styleXCreateSet
:
const [compiledStyles, injectedStyles] = styleXCreateSet({
root: {
display: 'flex',
flexDirection: 'column',
},
})
console.log(stringify({ compiledStyles, injectedStyles }))
Результат:
{
"compiledStyles": {
"root": {
"display": "x78zum5",
"flexDirection": "xdt5ytf",
"$$css": true
}
},
"injectedStyles": {
"x78zum5": {
"priority": 3000,
"ltr": ".x78zum5{display:flex}",
"rtl": null
},
"xdt5ytf": {
"priority": 3000,
"ltr": ".xdt5ytf{flex-direction:column}",
"rtl": null
}
}
}
injectedStyles
добавляются в таблицу стилей через метод styleSheet.insert
. compiledStyles
пропускаются через библиотеку styleq и передаются компоненту.
Вызовем функцию props
:
const result = props(compiledStyles.root)
console.log(stringify({ result }))
const result2 = props(compiledStyles.root, { display: 'flex' })
console.log(stringify({ result2 }))
Результат:
{
"result": {
"className": "x78zum5 xdt5ytf"
}
}
{
"result2": {
// Обратите внимание на отсутствие хеш-класса для `display: flex`
"className": "xdt5ytf",
"style": {
"display": "flex"
}
}
}
Посмотрим на цепочку преобразований такого объекта:
{
root: {
display: 'flex',
},
}
в такие:
{
"compiledStyles": {
"root": {
"display": "x78zum5",
"$$css": true
}
},
"injectedStyles": {
"x78zum5": {
"priority": 3000,
"ltr": ".x78zum5{display:flex}",
"rtl": null
}
}
}
function styleXCreateSet(namespaces, options = defaultOptions) {
const resolvedNamespaces = {}
const injectedStyles = {}
for (const namespaceName of Object.keys(namespaces)) {
const namespace = namespaces[namespaceName]
// !
console.log(stringify({ namespace }))
const flattenedNamespace = flattenRawStyleObject(namespace, options)
// !
console.log(stringify({ flattenedNamespace }))
const compiledNamespaceTuples = flattenedNamespace.map(([key, value]) => {
return [key, value.compiled(options)]
})
// !
console.log(stringify({ compiledNamespaceTuples }))
const compiledNamespace = objFromEntries(compiledNamespaceTuples)
// !
console.log(stringify({ compiledNamespace }))
const namespaceObj = {}
for (const key of Object.keys(compiledNamespace)) {
const value = compiledNamespace[key]
const classNameTuples = value
.map((v) => (Array.isArray(v) ? v : null))
.filter(Boolean)
const className =
classNameTuples.map(([className]) => className).join(' ') || null
namespaceObj[key] = className
for (const [className, injectable] of classNameTuples) {
if (injectedStyles[className] == null) {
injectedStyles[className] = injectable
}
}
}
resolvedNamespaces[namespaceName] = { ...namespaceObj, $$css: true }
}
return [resolvedNamespaces, injectedStyles]
}
Результат:
{
"namespace": {
"display": "flex"
}
}
{
"flattenedNamespace": [
[
"display",
{
"property": "display",
"value": "flex",
"pseudos": [],
"atRules": []
}
]
]
}
{
"compiledNamespaceTuples": [
[
"display",
[
[
"x78zum5",
{
"priority": 3000,
"ltr": ".x78zum5{display:flex}",
"rtl": null
}
]
]
]
]
}
{
"compiledNamespace": {
"display": [
[
"x78zum5",
{
"priority": 3000,
"ltr": ".x78zum5{display:flex}",
"rtl": null
}
]
]
}
}
За генерацию хеша на основе правила CSS
отвечает функция convertStyleToClassName
:
function convertStyleToClassName(
objEntry,
pseudos,
atRules,
options = defaultOptions,
) {
// !
console.log(stringify({ objEntry, pseudos, atRules }))
const { classNamePrefix = 'x' } = options
const [key, rawValue] = objEntry
const dashedKey = dashify(key)
const value = Array.isArray(rawValue)
? rawValue.map((eachValue) => transformValue(key, eachValue, options))
: transformValue(key, rawValue, options)
const sortedPseudos = arraySort(pseudos ?? [])
const sortedAtRules = arraySort(atRules ?? [])
const atRuleHashString = sortedPseudos.join('')
const pseudoHashString = sortedAtRules.join('')
const modifierHashString = atRuleHashString + pseudoHashString || 'null'
const stringToHash = Array.isArray(value)
? dashedKey + value.join(', ') + modifierHashString
: dashedKey + value + modifierHashString
// !
console.log(stringify({ dashedKey, value, modifierHashString, stringToHash }))
// Обратите внимание: `<>` используется для обеспечения стабильности хешей.
// Это должно быть удалено в будущих версиях
const className = classNamePrefix + createHash('<>' + stringToHash)
const cssRules = generateRule(className, dashedKey, value, pseudos, atRules)
// !
console.log(stringify({ key, className, cssRules }))
return [key, className, cssRules]
}
Результат:
{
"objEntry": [
"display",
"flex"
],
"pseudos": [],
"atRules": []
}
{
"dashedKey": "display",
"value": "flex",
"modifierHashString": "null",
// Строка для хеширования
"stringToHash": "displayflexnull"
}
{
"key": "display",
// Результат хеширования
"className": "x78zum5",
"cssRules": {
"priority": 3000,
"ltr": ".x78zum5{display:flex}",
"rtl": null
}
}
Строка "displayflexnull"
преобразуется/хешируется в x78zum5
Настоящая магия происходит в функции createHash
:
function createHash(str) {
return murmurhash2_32_gc(str, 1).toString(36)
}
function murmurhash2_32_gc(str, seed = 0) {
let l = str.length,
h = seed ^ l,
i = 0,
k
while (l >= 4) {
k =
(str.charCodeAt(i) & 0xff) |
((str.charCodeAt(++i) & 0xff) << 8) |
((str.charCodeAt(++i) & 0xff) << 16) |
((str.charCodeAt(++i) & 0xff) << 24)
k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)
k ^= k >>> 24
k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)
h =
((h & 0xffff) * 0x5bd1e995 +
((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^
k
l -= 4
++i
}
switch (l) {
case 3:
h ^= (str.charCodeAt(i + 2) & 0xff) << 16
case 2:
h ^= (str.charCodeAt(i + 1) & 0xff) << 8
case 1:
h ^= str.charCodeAt(i) & 0xff
h =
(h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)
}
h ^= h >>> 13
h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)
h ^= h >>> 15
return h >>> 0
}
MurmurHash2 — это простая и быстрая хеш-функция общего назначения, разработанная Остином Эпплби. Она не является криптографически-безопасной и возвращает 32-разрядное беззнаковое число.
В заключение, поговорим о функции styleq, которая вызывается в функции props
:
const [className, style] = styleq(styles);
styleq
— это быстрая и небольшая среда выполнения JavaScript
для объединения названий классов HTML
, созданных компиляторами CSS
. Вызов styleq(...styles)
объединяет объекты стилей и генерирует строку className
и объект встроенных (inline) стилей (с названиями свойств в стиле camelCase
):
const [className, inlineStyle] = styleq(styles.root, { opacity });
Функция styleq
эффективно объединяет глубоко вложенные массивы как извлеченных (extracted), так и встроенных объектов стилей:
$$css
со значением true
(помните resolvedNamespaces[namespaceName] = { ...namespaceObj, $$css: true }
?)$$css
считается встраиваемым стилемCSS
; разрешена любая строкаHTML
const styles = {
root: {
// Обязательное поле
$$css: true,
// Классы для отладки
'debug::file:styles.root': 'debug::file:styles.root',
// Атомарные классы
display: 'display-flex-class',
alignItems: 'alignItems-center-class'
}
};
const [className, inlineStyle] = styleq(styles.root, props.style);
Таким образом, styleq
обеспечивает эффективное объединение хеш-классов в одну строку className
без дублирования и с учетом встроенных стилей, которые возвращаются в виде объекта style
. className
и style
возвращаются в виде, пригодном для прямой передачи компоненту React
для установки элементу HTML
в качестве соответствующих атрибутов.
Пожалуй, это все, о чем я хотел рассказать вам в этой статье.
Happy coding!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩