styled-components原理

奴止
May 6, 2021
Last edited: 2022-10-6
type
Post
status
Published
date
May 6, 2021
slug
styled-components-how
summary
简单的看看 CSS-in-JS 生态中 styled-components 原理。
tags
前端开发
源码
category
技术随手记
icon
password
Property
Oct 6, 2022 02:59 AM
 
先来看官网 Get Started 的第一个示例:
// Create a Title component that'll render an <h1> tag with some styles const Title = styled.h1` font-size: 1.5em; text-align: center; color: palevioletred; `; // Create a Wrapper component that'll render a <section> tag with some styles const Wrapper = styled.section` padding: 4em; background: papayawhip; `; // Use Title and Wrapper like any other React component – except they're styled! render( <Wrapper> <Title> Hello World! </Title> </Wrapper> );
 
一般情况下,我们使用时都是直接安装依赖,然后定义组件,在组件 render 里直接使用,没有其它额外配置工作,所以上面的 WrapperTitle 肯定也都是标准的组件定义,即类似下面的定义:
function Wrapper(props) { return <section>{props.children}</section> }
 
所以首先要搞清楚:styled.h1`xxxx` 是怎么转成 React 组件定义的。这种写法是 tag template literals。

tag template literals

我们都很熟悉这种模版语法:
const name = "Jonge"; const greeting = `Hello ${name}!` // "Hello Jonge"
 
而这种 tag`something ${expression}` 语法只是稍微高级一点的用法:
function div() { console.log(arguments); return 'over'; } div`hello world` // print: [["hello\nworld"]] // return: "over" const yourName = 'Jonge'; div`hello ${yourName}!` // print: [["hello ", "!"], "Jonge"] // return: "over" const yourName = 'Jonge'; const time = 'Jonge'; div`hello ${yourName}!——@${time}` // [["hello ", "!——@", ""], "Jonge", "2021-07-01"] // return: "over"
 

模版字符串到 React 组件

简单实现一个 tag template literals 到 react 组件的转化:
function title([styleStr]: TemplateStringsArray) { return function (props) { return <h1 style={generateStyle(styleStr)}>{props.children}</h1>; }; } // css string style to React.CSSProperties function generateStyle(str: string) { const styleLines = str.split(";"); const style: React.CSSProperties = {}; styleLines.forEach((line) => { const [key, value] = line.trim().split(/ *: */); if (key && value) { style[hyphenToCamel(key)] = value; } }); return style; } function hyphenToCamel(key: string){ return key.replace(/-([a-z])/g, (_, up) => up.toUpperCase()); } // render <Title>Hello World!</Title>
 
添加基于 props 的动态样式处理:
function styled() {} function generateComponent( type: string, templateStr: TemplateStringsArray, values: any[] ) { return (props) => { let resultStr = ""; for (let i = 0; i < templateStr.length; i++) { let str = templateStr[i]; if (values) { let val = values[i]; val = typeof val === "function" ? val(props) : val; if (val) { str += val; } } resultStr += str; } return React.createElement(type, { style: generateStyle(resultStr), ...props }); }; } ["h1", "section", "div", "button"].forEach((tag) => { styled[tag] = (strs: TemplateStringsArray, ...values: any[]) => generateComponent(tag, strs, values); });
 
在线示例(👈点击展开)

styled-components 官方实现

 
核心的实现类似上节的示例,这里我们更多关注下它如何生成 className 以及如何更新到 style。
const styled = (tag: Target) => constructWithOptions(StyledComponent, tag); // styled.div 等方法的定义 // domElements = ['a', 'div', 'button', ...] domElements.forEach(domElement => { styled[domElement] = styled(domElement); }); // constructWithOptions 返回 () => StyledComponent export default function constructWithOptions( componentConstructor: Function, tag: Target, options: Object = EMPTY_OBJECT ) { const templateFunction = (...args) => componentConstructor(tag, options, css(...args)); // 其它的 API // templateFunction.withConfig // templateFunction.attrs return templateFunction; } // tag template literals const Wrapper = styled.section`YOUR_CSS_STYLE` // 以 styled.section 为例,它最终就是一个标准的 tag template function styled.section = (...args) => StyledComponent('section', options, css(...args)) // StyledComponent 即 createStyledComponent
 
⚠️
这里先强调两个概念:组件ID和组件实例类名,前者是一个组件定义对应一个ID,后者是组件使用中根据样式文本生成的hash,默认情况下两者的值分别为 sc-bdnxRMdmnuNZ,两者都会设置在组件的 className 中,但只有后者才有具体的样式规则。
 
// @models/StyledComponent.js export default function createStyledComponent( target: $PropertyType<IStyledComponent, 'target'>, options: { attrs?: Attrs, componentId: string, displayName?: string, parentComponentId?: string, shouldForwardProp?: ShouldForwardProp, }, rules: RuleSet ) { const componentStyle = new ComponentStyle( rules, styledComponentId, // 组件 ID,如 sc-bdnxRM isTargetStyledComp ? ((target: Object).componentStyle: ComponentStyle) : undefined ); let WrappedStyledComponent: IStyledComponent; const forwardRef = (props, ref) => useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic); WrappedStyledComponent = ((React.forwardRef(forwardRef): any): IStyledComponent); return WrappedStyledComponent; }
 
抛开 React.forwardRef,可以看到最终返回的组件定义是:
// 仍然以 Wrapper = styled.section`YOUR_CSS_STYLE` 为例 const Wrapper = (props, ref) => useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic)
 
来看 useStyledComponentImpl 的功能:
  • 根据具体某个组件的实例样式生成相应的 hash
  • 更新样式到对应的 style 节点
function useStyledComponentImpl( forwardedComponent: IStyledComponent, props: Object, forwardedRef: Ref<any>, isStatic: boolean ) { // 根据 css rules 最终字符串生成的 hash: dmnuNZ const generatedClassName = useInjectedStyle( componentStyle, isStatic, context, process.env.NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined ); const propsForElement = {}; propsForElement.className = Array.prototype .concat( foldedComponentIds, styledComponentId, // 组件ID,componentId: sc-bdnxRM generatedClassName !== styledComponentId ? generatedClassName : null, props.className, attrs.className ) .filter(Boolean) .join(' '); // className: "sc-bdnxRM dmnuNZ" return React.createElement(elementToBeCreated, propsForElement); } function useInjectedStyle<T>( componentStyle: ComponentStyle, isStatic: boolean, resolvedAttrs: T, warnTooManyClasses?: $Call<typeof createWarnTooManyClasses, string, string> ) { const className = isStatic ? componentStyle.generateAndInjectStyles(EMPTY_OBJECT, styleSheet, stylis) : componentStyle.generateAndInjectStyles(resolvedAttrs, styleSheet, stylis); return className; }
 
// @models/ComponentStyle.js export default class ComponentStyle { generateAndInjectStyles(executionContext: Object, styleSheet: StyleSheet, stylis: Stringifier) { const names = []; // 生成 css string // 计算对应的 hash const name = generateName(dynamicHash >>> 0); if (!styleSheet.hasNameForId(componentId, name)) { // 格式化 css rules,如 ".dmnuNZ{background:palevioletred;}" const cssFormatted = stylis(css, `.${name}`, undefined, componentId); // 插入样式到 style 标签 styleSheet.insertRules(componentId, name, cssFormatted); } names.push(name); // className return names.join(' '); } }
 
styleSheet.insertRules 存储组件 ID 和组件实例样式 hash 的关系,性能考虑,防止每次 render 重新计算。
// @sheet/Sheet.js class StyleSheet implements Sheet { insertRules(id: string, name: string, rules: string[]) { this.registerName(id, name); this.getTag().insertRules(getGroupForId(id), rules); } getTag(): GroupedTag { // 如 this.tag = new DefaultGroupedTag(new TextTag()) return this.tag || (this.tag = makeGroupedTag(makeTag(this.options))); } }
 
new TextTag 会创建 style 节点,并插入指定的 target 中(如果没有默认为 head)最后一个 style 之前(没有就放在末尾)。
// @sheet/Tag.js export class TextTag implements Tag { constructor(target?: HTMLElement) { // <style data-styled="active" data-styled-version="5.3.0"></style> const element = (this.element = makeStyleTag(target)); this.nodes = element.childNodes; this.length = 0; } insertRule(index: number, rule: string): boolean { if (index <= this.length && index >= 0) { const node = document.createTextNode(rule); const refNode = this.nodes[index]; this.element.insertBefore(node, refNode || null); this.length++; return true; } else { return false; } } } // @sheet/GroupedTag.js class DefaultGroupedTag implements GroupedTag { constructor(tag: Tag) { // 初始大小 512,在 insertRules 中如果超容则会 double 扩容直到可以容纳 this.groupSizes = new Uint32Array(BASE_SIZE); this.length = BASE_SIZE; this.tag = tag; } insertRules(group: number, rules: string[]): void { let ruleIndex = this.indexOfGroup(group + 1); for (let i = 0, l = rules.length; i < l; i++) { // TextTag, CSSOMTag, ... if (this.tag.insertRule(ruleIndex, rules[i])) { this.groupSizes[group]++; ruleIndex++; } } } }
 
一个简单的例子截图:
notion image

性能

直观看来 css-in-js 相比较于纯 css 有额外的 js 处理,性能肯定不如纯 css,但是出于所谓开发者体验(DX, Developer Experience) 以及更灵活的样式更新,很多项目还是采用了 css-in-js 技术。
 
一般来讲,也只有在较多节点(比如大表格、列表等)的场景才需要关注 css-in-js 引发的性能问题,因为那时,才会触及所谓“瓶颈”,不要为了优化而优化,适合项目的才是最优的。
 
个人并不是太喜欢 css-in-js,大概是总抱有一种「css-in-js 是给 css 玩不转的人用的」的想法吧🐶。

参考

  1. https://blog.logrocket.com/build-your-own-styled-components-library/
  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates
SVG sprite方案vue3 + vite 常见问题