深入浅出 CSS-in-JS 工作原理

点上方蓝字关注公众号「前端从进阶到入院

精选原创好文助你进入大厂

概述


现在的前端开发特别是 React 社区, CSS-in-JS 越来越常见了.

styled-components[1] 凭借着以下几种特性 脱颖而出[2]:

  • 它基于  标记模板 [3] 语法
  • 以编写 React 组件的形式来定义样式
  • 解决 CSS 模块化的问题
  • 提供了 CSS 不具备的功能, 比如嵌套
  • 上述特性都无需配置

开发者不再需要费尽脑汁去想 CSS 的类名. 那么, 上面所说的种种, 是如何实现的呢?

注意: 如果你不熟悉 styled-components, 请先阅读 这篇文档[4]

魔性语法


我们使用 styled-components 创建一个简单的按钮:

const Button = styled.button`
  color: coral;
  padding: 0.25rem 1rem;
  border: solid 2px coral;
  border-radius: 3px;
  margin: 0.5rem;
  font-size: 1rem;
`
;

在线示例[5]

styled.button 只是 styled('button') 的简写, styled 方法接收一个 可用的标签名称[6] 作为参数. 如果你熟悉标记模板的话, 你就会知道, 其实 button 只是一个函数, 可以接收一个字符串数组作为参数. 看一下下面的代码:

const Button = styled('button')([
  'color: coral;' +
  'padding: 0.25rem 1rem;' +
  'border: solid 2px coral;' +
  'border-radius: 3px;' +
  'margin: 0.5rem;' +
  'font-size: 1rem;'
]);

在线示例[7]

现在你可以看到其实 styled 就是一个组件工厂, 我们可以想象一下它是怎么实现的.

重构 styled-components


const myStyled = (TargetComponent) => ([style]) => class extends React.Component {
  componentDidMount() {
    this.element.setAttribute('style', style);
  }

  render() {
    return (
      <TargetComponent {...this.propsref={element => this.element = element } />
    );
  }
};

const Button = myStyled('button')`
  color: coral;
  padding: 0.25rem 1rem;
  border: solid 2px coral;
  border-radius: 3px;
  margin: 0.5rem;
  font-size: 1rem;
`
;

在线示例[8]

上面的代码实现看起来很简单——myStyled 工厂函数基于给定的标签名创建了一个新的组件, 在组件挂载之后设置行内样式. 但是如果我们的组件样式依赖于某个  props   呢?

const primaryColor = 'coral';

const Button = styled('button')`
  background: ${({ primary }) => primary ? 'white ' : primaryColor};
  color: ${({ primary }) => primary ? primaryColor : 'white'};
  padding: 0.25rem 1rem;
  border: solid 2px ${primaryColor};
  border-radius: 3px;
  margin: 0.5rem;
`
;

为了在组件挂载或者组件的 props 更新的时候计算样式中的相关插值, 我们需要更新上面代码的实现:

const myStyled = (TargetComponent) => (strs, ...exprs) => class extends React.Component {
  interpolateStyle() {
    const style = exprs.reduce((result, expr, index) => {
      const isFunc = typeof expr === 'function';
      const value = isFunc ? expr(this.props) : expr;

      return result + value + strs[index + 1];
    }, strs[0]);

    this.element.setAttribute('style', style);
  }

  componentDidMount() {
    this.interpolateStyle();
  }

  componentDidUpdate() {
    this.interpolateStyle();
  }

  render() {
    return <TargetComponent {...this.propsref={element => this.element = element } />
  }
};

const primaryColor = 'coral';

const Button = myStyled('button')`
  background: ${({ primary }) => primary ? primaryColor : 'white'};
  color: ${({ primary }) => primary ? 'white' : primaryColor};
  padding: 0.25rem 1rem;
  border: solid 2px ${primaryColor};
  border-radius: 3px;
  margin: 0.5rem;
  font-size: 1rem;
`
;

在线示例[9]

上述代码最棘手的部分在于如何得到样式字符串:

const style = exprs.reduce((result, expr, index) => {
  const isFunc = typeof expr === 'function';
  const value = isFunc ? expr(this.props) : expr;

  return result + value + strs[index + 1];
}, strs[0]);

我们把所有的字符串片段拼接得到一个一个的 result; 如果某个插值是函数类型, 那么就会把组件的 props 传递给它, 同时调用它.

上面这个简单的工厂看起来很像 styled-components 提供的, 但是实际上 styled-components 的底层实现更加有意思: 它不用内联样式. 让我们走近 styled-components 以了解当导入并且创建组件的时候究竟发生了什么.

styled-components 底层原理


引入 styled-components

当你首次引入 styled-components 库的时候, 它内部会创建一个 counter 变量, 用来记录每一个通过 styled 工厂函数创建的组件.

调用 styled.tag-name 工厂函数

const Button = styled.button`
  font-size: ${({ sizeValue }) => sizeValue + 'px'};
  color: coral;
  padding: 0.25rem 1rem;
  border: solid 2px coral;
  border-radius: 3px;
  margin: 0.5rem;
  &:hover {
    background-color: bisque;
  }
`
;

styled-components 创建新组件的同时会给该组件创建一个 componentId 标识符. 代码如下:

counter++;
const componentId = 'sc-' + hash('sc' + counter);

第一个创建的 styled-components 组件的 componentId 为 sc-bdVaJa

一般情况下 styled-components 会使用 MurmurHash[10] 算法创建唯一的标识符, 接着将 哈希值转化为乱序字母组成的字符串[11].

一旦创建好标识符, styled-components 会将 <style> 元素插入到 <head> 内部, 并且插入一条带有 componentId 的注释, 就像下面这样:

<style data-styled-components>
  /* sc-component-id: sc-bdVaJa */
</style>

创建好新组件之后, componentId 和 target 都会以静态属性的形式存储于 button 这个组件上:

StyledComponent.componentId = componentId;
StyledComponent.target = TargetComponent;

可以看到, 仅仅创建一个 styled-components 组件, 并不会消耗太多性能. 甚至如果你定义了成百上千的组件而不去使用它们, 你最终得到的也只是一个或多个带有注释的 <style> 元素.

通过 styled 工厂函数创建的组件有个很重要的点: 它们都继承了一个隐藏的 BaseStyledComponents 类, 这个类实现了一些生命周期方法. 让我们看一下.

componentWillMount()

我们给 Button 组件创建一个实例并挂载到页面上:

ReactDOM.render(
  <Button sizeValue={24}>I'm a button</Button>,
  document.getElementById('root')
);

BaseStyledComponent 组件的 componentWillMount() 生命周期被调用了, 这释放了一些重要信号:

  1. 解析标记模板: 这个算法和我们实现过的  myStyled 工厂很相似. 对于  Button 组件的实例:
<Button sizeValue={24}>I'm a button</Button>

我们得到了如下所示的 CSS 样式字符串:

font-size: 24px;
colorcoral;
padding: 0.25rem 1rem;
bordersolid 2px coral;
border-radius: 3px;
margin: 0.5rem;
&:hover {
  background-color: bisque;
}
  1. 生成 CSS 类名: 每个组件实例都会有一个唯一的 CSS 类名, 这个类名也是基于  MurmurHash [12] 算法、 componentId 以及  evaluatedStyles 字符串生成的:
const className = hash(componentId + evaluatedStyles);

所以我们的 Button 实例生成的 className 是 jsZVzX.

之后这个类名会保存到组件的 state 上, 字段名为 generatedClassName.

  1. 预处理 CSS: 我们使用流行的 CSS 预处理器—— stylis [13], 提取 CSS 字符串:
const selector = '.' + className;
const cssStr = stylis(selector, evaluatedStyles);

下面是 Button 实例最终的 CSS 样式:

.jsZVzX {
  font-size24px;
  color: coral;
  padding0.25rem 1rem;
  border: solid 2px coral;
  border-radius3px;
  margin0.5rem;
}
.jsZVzX:hover{
  background-color: bisque;
}
  1. 将 CSS 字符串注入到页面上: 现在可以将 CSS 注入到  <style> 标签内部的带有组件标识注释的后面:
<style data-styled-components>
  /* sc-component-id: sc-bdVaJa */
  .sc-bdVaJa {} .jsZVzX{font-size:24px;color:coral; ... }
  .jsZVzX:hover{background-color:bisque;}
</style>

正如你看到的, styled-components 也将 componentId(.sc-bdVaJa) 注入到页面上, 并且没有给 .sc-bdVaJa 定义样式.

render()

当完成 CSS 的相关工作后, styled-components 只需要去创建组件的类名(className)即可:

const TargetComponent = this.constructor.target; // In our case just 'button' string.
const componentId = this.constructor.componentId;
const generatedClassName = this.state.generatedClassName;

return (
  <TargetComponent
    {...this.props}
    className={this.props.className + ' ' + componentId + ' ' + generatedClassName}
  />

);

styled-components 给渲染的元素(TargetComponent)添加了 3 个类名:

  1. this.props.className —— 从父组件传递过来的类名, 是可选的.
  2. componentId —— 一个组件唯一的标识, 但是要注意不是组件实例. 这个类名没有 CSS 样式, 但是当需要  引用其它组件 [14] 的时候, 可以作为一个嵌套选择器来使用.
  3. generatedClassName —— 具有 CSS 样式的组件的唯一前缀

很棒! 最终渲染出来的 HTML 是这样的:

<button class="sc-bdVaJa jsZVzX">I'm a button</button>

componentWillReceiveProps()

现在让我们尝试着在 Button 组件挂载完成之后更改它的 props. 需要做的是给 Button 组件添加一个交互式的事件:

let sizeValue = 24;

const updateButton = () => {
  ReactDOM.render(
    <Button sizeValue={sizeValue} onClick={updateButton}>
      Font size is {sizeValue}px
    </Button>
,
    document.getElementById('root')
  );
  sizeValue++;
}

updateButton();

在线示例[15]

你点击一次按钮, componentWillReceiveProps() 会被调用, 并且 sizeValue 会自增, 之后的流程和 componentWillMount() 一样:

  1. 解析标记模板
  2. 生成新的 CSS 类名
  3. stylis [16] 预处理样式
  4. 将 CSS 注入到页面上

在多次点击按钮之后查看浏览器开发者工具, 可以看到:

<style data-styled-components>
  /* sc-component-id: sc-bdVaJa */
  .sc-bdVaJa {}
  .jsZVzX{font-size:24px;color:coral; ... } .jsZVzX:hover{background-color:bisque;}
  .kkRXUB{font-size:25px;color:coral; ... } .kkRXUB:hover{background-color:bisque;}
  .jvOYbh{font-size:26px;color:coral; ... } .jvOYbh:hover{background-color:bisque;}
  .ljDvEV{font-size:27px;color:coral; ... } .ljDvEV:hover{background-color:bisque;}
</style>

是的, 所有类只有 font-size 属性不同, 并且无用的 CSS 类都没有被移除. 这是为什么? 因为移除无用的类会增加性能开销, 具体可以看 这篇文章[17].

这里有个小的优化点: 可以添加一个 isStatic 变量, 在 componentWillReceiveProps() 检查这个变量, 如果组件不需要插入样式的话, 直接跳过, 从而避免不必要的样式计算.

性能优化技巧


了解了 styled-components 底层是如何工作的, 之后才能更好的专注于性能优化.

这是一个有彩蛋的 按钮例子[18](提示: 点击按钮超过 200 次, 你会在控制台看到 styled-components 的隐藏信息. 不是开玩笑的哦!😉).

下面就是隐藏的彩蛋:

styled.button 组件生成了超过 200 个类名,
需要频繁更改样式的话,
可以考虑使用 attrs 方法。

Example:
const Component = styled.div.attrs({
style: ({ background }) => ({
background,
}),
})`width: 100%;`
<Component />

重构后的 Button 组件是这样的:

const Button = styled.button.attrs({
  style({ sizeValue }) => ({ fontSize: sizeValue + 'px' })
})`
  color: coral;
  padding: 0.25rem 1rem;
  border: solid 2px coral;
  border-radius: 3px;
  margin: 0.5rem;
  &:hover {
    background-color: bisque;
  }
`
;

然而, 并不是所有的动态样式都应该采取这种方式. 我自己的规则是对于起伏比较大的数值, 使用 style 属性. 比如:

  • 像  word cloud [19] 这种可以高度定制  font-size 的组件
  • 从服务端获取的具有不同颜色的标签列表

但是, 如果你的按钮是多样化的, 比如 default、primary、warn等, 还是使用样式字符串比较好.

在下面的例子里面, 我使用的是开发版本的 styled-components 包, 而你应该使用速度更快的生产版本. 在 React 项目里面, styled-components 的生产包禁用了很多开发环境下的警告, 这些警告是很重要的, 它使用 CSSStyleSheet.insertRule()[20] 将生成的样式注入到页面上, 但是开发环境下却用了 Node.appendChild()[21]

Evan Scott 在这里[22] 展示了 insertRule 到底有多快。

同时你也可以考虑使用 babel-plugin-styled-components[23] 插件, 它可以压缩并预处理样式文件.

总结


styled-components 的工作流程是很简洁的, 它会在组件渲染之前创建必要的 CSS 样式, 并且在需要解析标签字符串和预处理 CSS 的前提下也足够快.

这篇文章的讲解并没有覆盖到 styled-components 的各个方面, 但是我尽量的去专注于主要的点.

在这篇文章里面, 我使用的 styled-components 版本是 v3.3.3[24]. 在后续的版本中它的源码可能会发生变化.

考资料

[1]

styled-components: https://www.styled-components.com/

[2]

脱颖而出: https://github.com/tuchk4/awesome-css-in-js#libraries

[3]

标记模板: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates

[4]

这篇文档: https://www.styled-components.com/docs

[5]

在线示例: https://jsfiddle.net/gevgeny/r07th16o/?utm_source=website&utm_medium=embed&utm_campaign=r07th16o

[6]

可用的标签名称: https://github.com/styled-components/styled-components/blob/v3.3.3/src/utils/domElements.js#L4

[7]

在线示例: https://jsfiddle.net/gevgeny/o3zhc7xw/

[8]

在线示例: https://jsfiddle.net/gevgeny/dxc6gumn/?utm_source=website&utm_medium=embed&utm_campaign=dxc6gumn

[9]

在线示例: https://jsfiddle.net/gevgeny/et4wu96y/?utm_source=website&utm_medium=embed&utm_campaign=et4wu96y

[10]

MurmurHash: https://www.wikiwand.com/en/MurmurHash

[11]

哈希值转化为乱序字母组成的字符串: https://github.com/styled-components/styled-components/blob/v3.3.3/src/utils/generateAlphabeticName.js#L13

[12]

MurmurHash: https://www.wikiwand.com/en/MurmurHash

[13]

stylis: https://github.com/thysultan/stylis.js

[14]

引用其它组件: https://www.styled-components.com/docs/advanced#referring-to-other-components

[15]

在线示例: https://jsfiddle.net/gevgeny/0ezu72Ly/

[16]

stylis: https://github.com/thysultan/stylis.js

[17]

这篇文章: https://github.com/styled-components/styled-components/issues/1431#issuecomment-358097912

[18]

按钮例子: https://jsfiddle.net/gevgeny/0ezu72Ly/

[19]

word cloudhttps://www.jasondavies.com/wordcloud/

[20]

CSSStyleSheet.insertRule(): https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule

[21]

Node.appendChild(): https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild

[22]

在这里: https://medium.com/styled-components/v3-1-0-such-perf-wow-many-streams-c45c434dbd03

[23]

babel-plugin-styled-components: https://www.styled-components.com/docs/tooling#babel-plugin

[24]

v3.3.3: https://github.com/styled-components/styled-components/tree/v3.3.3


原文链接: https://medium.com/styled-components/how-styled-components-works-618a69970421

翻译:崩崩老猫

译文:https://www.zhihu.com/people/bu-jing-tong-jsbu-zhao-dui-xiang-81

转载需要联系作者本人授权。



❤️ 谢谢支持 

1.喜欢的话别忘了分享、点赞、在看三连哦~。

2.长按下方图片,关注「前端从进阶到入院」,获取我分类整理的原创精选面试热点文章,Vue、React、TS、手写题等应有尽有,我进入字节、滴滴、百度的小伙伴们说文章对他们帮助很大,希望也能帮助到你。



本文分享自微信公众号 - 前端从进阶到入院(code_with_love)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

微信关注我们

原文链接:https://my.oschina.net/sl1673495/blog/4868512

转载内容版权归作者及来源网站所有!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

相关文章

发表评论

资源下载

更多资源
Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Eclipse(集成开发环境)

Eclipse(集成开发环境)

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。

Java Development Kit(Java开发工具)

Java Development Kit(Java开发工具)

JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。

Sublime Text 一个代码编辑器

Sublime Text 一个代码编辑器

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。