博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[技术地图]
阅读量:5903 次
发布时间:2019-06-19

本文共 4085 字,大约阅读时间需要 13 分钟。

在一文中吹了一波 后,本文想深入来了解一下 styled-components 的原理. 如果你对 styled-components 还不了解,建议先阅读一下官方或前面的文章.

本文基于 styled-components v4.13 版本

目录


从 Tagged Template Literals 说起

是 ES6 新增的特性,它允许你自定义字符串的内插(interpolation)规则, styled-components 正是基于这个特性构建:

它的原理非常简单,所有静态字符串会被拆分出来合并成为数组, 作为第一个参数传入到目标函数,而内插(interpolation)表达式的值则会作为 rest 参数传入:

标签模板字面量相比普通的模板字面量更加灵活. 普通模板字符串会将所有内插值转换为字符串,而标签模板字面量则由你自己来控制:

因为标签模板字符串简洁的语法和灵活性,它比较适用于作为DSL, 不需要在语言层面进行支持,比如前阵子作者开发的, 口号就是"取代 JSX,而且不需要编译器支持", 通过这种方式是可以优雅地实现.

另一个典型的例子就是 jest 的, 这样形式可读性更高:

标签模板字面量的脑洞还在继续,比如可以用来写 markdown,再生成 react 组件。限于篇幅就不啰嗦了

扩展:


源码导读

现在来看一下 styled-components 的实现。为了行文简洁,我们只关心 styled-components 的核心逻辑,所以我对源代码进行了大量的简化,比如忽略掉服务端渲染、ReactNative 实现、babel 插件等等.

1. 处理标签模板字面量

先从 styled 构造函数看起:

styled 构造函数接收一个包装组件 target,而标签模板字面量则由函数进行处理的. 这个函数在 styled-components 中非常常用,类似于 SCSSmixin 角色. css 函数会标签模板字面量规范化, 例如:

css 实现也非常简单:

interleave函数将将静态字符串数组和内插值’拉链式‘交叉合并为单个数组, 比如[1, 2] + [a, b]会合并为[1, a, 2, b]

关键在于如何将数组进行扁平化, 这个由 flatten 函数实现. flatten 函数会将嵌套的 css(数组形式)递归 concat 在一起,将 StyledComponent 组件转换为类名引用、还有处理 keyframe 等等. 最终剩下静态字符串和函数, 输出结果如上所示。

实际上 styled-components 会进行两次 flatten,第一次 flatten 将能够静态化的都转换成字符串,将嵌套的 css 结构打平, 只剩下一些函数,这些函数只能在运行时(比如在组件渲染时)执行;第二次是在运行时,拿到函数的运行上下文(props、theme 等等)后, 执行所有函数,将函数的执行结果进行递归合并,最终生成的是一个纯字符串数组. 对于标签模板字面量的处理大概都是这个过程. 看看 flatten 的实现:

总结一下标签模板字面量的处理流程大概是这样子:


2. React 组件的封装

现在看看如何构造出 React 组件。styled-components 通过 createStyledComponent 高阶组件将组件封装为 StyledComponent 组件:

createStyledComponent 是一个典型的高阶组件,它在执行期间会生成一个唯一的组件 id 和创建ComponentStyle对象. ComponentStyle 对象用于维护 css 函数生成的 cssRules, 在运行时(组件渲染时)得到执行的上下文后生成最终的样式和类名。

再来看看 StyledComponent 的实现, StyledComponent 在组件渲染时,将当前的 props+theme 作为 context 传递给 ComponentStyle,生成类名.


3. 样式和类名的生成

上面看到 StyleComponent 通过 ComponentStyle 类来构造样式表并生成类名, ComponentStyle 拿到 context 后,再次调用 flatten 将 css rule 扁平化,得到一个纯字符串数组。通过使用 hash 算法生成类名, 并使用stylis 对样式进行预处理. 最后通过 StyleSheet 对象将样式规则插入到 DOM 中

是一个 3kb 的轻量的 CSS 预处理器, styled-components 所有的 CSS 特性都依赖于它, 例如嵌套规则(a {&:hover{}})、厂商前缀、压缩等等.


4. DOM 层操作

现在来看一下 StyleSheet, StyleSheet 负责收集所有组件的样式规则,并插入到 DOM 中

看看简化版的 makeTag


5. 总结

代码可能看晕了,通过流程图来梳理一下过程.

上一篇文章一点代码也没有罗列,只有一个流程图, 读者可能一下子就傻眼了, 不知道在说些什么; 而且这个流程图太大,在移动端不好阅读. 这期稍微改进一下,新增’源码导读‘一节,代码表达能力毫无疑问是胜于流程图的,但是代码相对比较细节琐碎,所以第一是将代码进行简化,留下核心的逻辑,第二是使用流程图表示大概的程序流程,以及流程主体之间的关系.

如上图 styled-components 主要有四个核心对象:

  • WrappedComponent: 这是 createStyledComponent 创建的包装组件,这个组件保存的被包装的 target、并生成组件 id 和 ComponentStyle 对象
  • StyledComponent: 这是样式组件,在它 render 时会将 props 作为 context 传递给 ComponentStyle,并生成类名
  • ComponentStyle: 负责生成最终的样式表和唯一的类名,并调用 StyleSheet 将生成的样表注入到文档中
  • StyleSheet: 负责管理已生成的样式表, 并注入到文档中

styled-components 性能优化建议

styled-components 每次渲染都会重新计算 cssRule,并进行 hash 计算出 className,如果已经对应的 className 还没插入到样式表中,则使用 stylis 进行预处理,并插入到样式表中;

另外 styled-components 对静态 cssRule(没有任何内插函数)进行了优化,它们不会监听 ThemeContext 变化, 且在渲染时不会重新计算。

通过这些规则可以得出以下性能优化的建议:

  • 静态化的 cssRule 性能是最好的

  • 降低 StyledComponent 状态复杂度. styled-components 并不会对已有的不变的样式规则进行复用,一旦状态变化 styled-component 会生成一个全新的样式规则和类名. 这是最简单的一种实现, 避免了样式复用的复杂性,同时保持样式的隔离性, 问题就是会产生样式冗余。 例如

    const Foo = styled.div<{ active: boolean }>`  color: red;  background: ${props => (props.active ? 'blue' : 'red')};`;复制代码

    active 切换之间会生成两个类名:

    .cQAOKL {  color: red;  background: red;}.kklCtT {  color: red;  background: blue;}复制代码

    如果把 StyledComponent 看做是一个状态机,那么 styled-components 可能会为每一个可能的状态生成独立的样式. 如果 StyledComponent 样式很多, 而且状态比较复杂,那么会生成很多冗余的样式.

  • 不要用于动画。上面了解到 styled-component 会为每个状态生成一个样式表. 动画一般会有很多中间值,在短时间内进行变化,如果动画值通过props传入该StyledComponent来应用样式,这样会生成很多样式,性能非常差:

    const Bar = styled.div<{ width: boolean }>`  color: red;  // 千万别这么干  width: ${props => props.width};`;复制代码

    这种动画场景最好使用 style 内联样式来做

OK, 行文结束。styled-components 不过如此是吧?


技术地图

  • CSS 相关
    • @emotion/unitless 判断属性值是否需要单位
    • css-to-react-native 将 css 转换为 ReactNative style 属性
    • ✨ 轻量的 CSS 预处理器
  • React 相关
    • @emotion/is-prop-valid 判断是否是合法的 DOM 属性
    • 提升React组件的静态属性,用于高阶组件场景
    • ✨: 判断各种 React 组件类型
    • 这是一个有意思的库,这个库试图围绕着构建 React 应用提出一套理想的原语,通俗的说就是通过它可以导入不同平台的组件。
    • 将react渲染到iframe中。也是一个比较有意思的库
    • react实时编辑器和展示,主要用于文档
  • 构建相关
    • 检查包大小
    • 使用babel-plugin来重写Javascript或Typescript代码, 一般用于制作升级脚本
    • ✨ 一个零配置的打包器,基于Rollup,可以用于库的打包和开发, preact作者开发必属精品

转载于:https://juejin.im/post/5cf23f35e51d45598611b911

你可能感兴趣的文章
云计算的十大特点
查看>>
自学Python的步骤与方法
查看>>
Linux下使用yum安装ElasticSearch的方法
查看>>
软件测试对比软件开发,选择更适合的
查看>>
听硬盘声音。闻声识好坏。【一】
查看>>
Lintcode98 Sort List solution 题解
查看>>
如何寻回台式电脑调整分区后分区丢失的数据
查看>>
linux基础学习(四)
查看>>
php生成随机密码的几种方法
查看>>
基于PHP+Ajax实现表单验证的详解
查看>>
简单的点击短信发送计时器
查看>>
网络客户端工具—ftp、lftp、wget
查看>>
简练软考知识点整理-边际效用递减法则
查看>>
Signal处理中的函数可重入问题
查看>>
BD-WAF-M5000规则设置方法
查看>>
磁盘管理体系续2
查看>>
以太坊开发DApp实战教程——用区块链、星际文件系统(IPFS)、Node.js和MongoDB来构...
查看>>
2018-05-14笔记
查看>>
Linux环境下MariaDB数据库四种安装方式
查看>>
openstack neutron网络主机节点网口配置 liberty版本之前的
查看>>