源码解析
首先拉取 styled-components 仓库,然后找到 /packages/styled-components 文件夹,核心代码都放在这个文件夹里;为了方便,后面会把 styled-components 简写为 sc。
往期 styled-components 文章:
《styled-components 深入浅出 (一) : 基础使用》
《styled-components 深入浅出 (二) : 高阶组件》
如何阅读源代码?
当我们对该项目有所了解的时候。
- 第一种:当我们使用过该项目的一些 api ,对于某个功能是如何实现的,带着这个问题去看,比如当我们使用 styled 函数的时候,我们会想到 styled 函数应该是通过
document.createElement来创建<style>标签来实现样式化的,然后全局搜索document.createElement快速定位到。这种方式适合看具体单独的某个功能如何实现。 - 第二种:从入口文件看起,根据导入、导出的文件,找到对应的文件,一层一层剥开,但是这种在比较复杂的项目中,可能需要花比较长的时间才能理清逻辑。这个时候我们就需要通过打断点的方式,来调试代码,找到一些关键函数的调用关系,梳理清楚主要逻辑。
对于 sc 这个项目我是采用两种方式结合使用。
makeStyleTag
一开始我就通过 document.createElement('style') 定位到 makeStyleTag 函数。代码所在文件:
代码非常简单易懂,这是去掉类型后的代码:
export const makeStyleTag = (target) => {
const head = document.head;
const parent = target || head;
const style = document.createElement('style');
// style 标签的插入位置
const prevStyle = findLastStyleTag(parent);
const nextSibling = prevStyle !== undefined ? prevStyle.nextSibling : null;
// 自定义属性 data-set
style.setAttribute(SC_ATTR, SC_ATTR_ACTIVE);
style.setAttribute(SC_ATTR_VERSION, SC_VERSION);
const nonce = getNonce();
// 设置 style 标签的 nonce 属性:一种加密的随机数(一次使用的数字)
if (nonce) style.setAttribute('nonce', nonce);
// 在父节点里的最后一个子节点的位置插入新的 style 节点
parent.insertBefore(style, nextSibling);
return style;
};
我们可以看到,这个函数主要做了以下几件事:
- 获取父节点,如果没有传入,则默认获取
document.head - 获取父节点的最后一个
<style>标签,如果没有,则返回null - 创建
<style>标签,并设置了自定义属性 - 设置
<style>标签的nonce属性 - 将
<style>标签插入到父节点的最后一个子节点的位置,然后返回 style 节点。
标签模板字符串
通过第一篇文章,我们知道通过 styled.div 和 styled.div() 都能创建一个样式化的 <div> 标签,这是怎么回事?来看一个例子:
这其实是 ES6 的一个新语法:模板字符串,在这可以把它看做是一个函数,接受传参。
styled 函数
我们找到入口文件:packages/styled-components/src/index.ts,
import styled from './constructors/styled';
export * from './base';
export {
CSSKeyframes,
CSSObject,
CSSProp,
CSSProperties,
CSSPseudos,
DefaultTheme,
ExecutionContext,
ExecutionProps,
IStyledComponent,
IStyledComponentFactory,
IStyledStatics,
PolymorphicComponent,
PolymorphicComponentProps,
RuleSet,
Runtime,
StyledObject,
StyledOptions,
WebTarget,
} from './types';
export { styled as default, styled };
默认导出的 styled 函数是从 packages/styled-components/src/constructors/styled.tsx 导出的,找到它,去掉类型后的代码方便阅读。
import createStyledComponent from '../models/StyledComponent';
// HTML 标签列表
import domElements from '../utils/domElements';
import constructWithOptions from './constructWithOptions';
// 创建基础的 styled 方法
const baseStyled = (tag) =>
constructWithOptions(createStyledComponent, tag);
const styled = baseStyled;
// 实现通过 styled[domElement] 和 styled(domElement) 都能创建样式化组件
domElements.forEach(domElement => {
styled[domElement] = baseStyled(domElement);
});
export default styled;
createStyledComponent 构造样式化组件
我们看到通过 styled 函数是基础的 styled 方法:baseStyled 调用了 constructWithOptions 方法,找到 constructWithOptions 方法所在的文件 src/constructors/constructWithOptions.ts,去掉类型只留下关键的代码如下
import css from './css';
export default function constructWithOptions(componentConstructor, tag, options) {
const templateFunction = (initialStyles, ...interpolations) =>
componentConstructor(tag, options, css(initialStyles, ...interpolations));
// 返回样式化组件
return templateFunction;
}
constructWithOptions 函数的核心是 templateFunction 方法,它调用组件的构造方法 componentConstructor返回一个样式化组件(携带样式的组件)。其实这个方法就是上面说到的 baseStyled 方法传入的 createStyledComponent 方法。该方法所在文件:src/models/StyledComponent.ts
为了方便理解,我画了个简单的函数调用关系图:
从这我们可以看出进来,当 sc 的使用者调用 styled() 这个api创建样式化组件时,本质上是通过 createStyledComponent 这个组件构造函数来实现的. 它返回一个 WrappedStyledComponent 组件,这个组件是通过 React.forwardRef 创建的,并且在返回之前会对 WrappedStyledComponent 组件添加一些属性。
生成唯一类名
sc的样式是组件级隔离的,通过生成唯一的类名来实现组件级隔离,sc 是如何生成唯一的类名的呢?接着上面来继续分析哈!
通过前面的分析我知道通过 React.forwardRef(forwardRefRender) 创建返回 WrappedStyledComponent 组件,我们找到 forwardRefRender 发现是调用了 useStyledComponentImpl 来创建元素的。
function forwardRefRender(props: ExecutionProps & OuterProps, ref: Ref<Element>) {
return useStyledComponentImpl<OuterProps>(WrappedStyledComponent, props, ref);
}
找到 useStyledComponentImpl 的实现,这是去掉类型精简后的代码:
function useStyledComponentImpl(
forwardedComponent,
props,
forwardedRef
) {
const {
attrs: componentAttrs,
componentStyle,
defaultProps,
foldedComponentIds,
styledComponentId,
target,
} = forwardedComponent;
const elementToBeCreated = context.as || target;
// 生成组件的类名
const generatedClassName = useInjectedStyle(componentStyle, context);
if (process.env.NODE_ENV !== 'production' && forwardedComponent.warnTooManyClasses) {
forwardedComponent.warnTooManyClasses(generatedClassName);
}
// 合并类名
let classString = joinStrings(foldedComponentIds, styledComponentId);
if (generatedClassName) {
classString += ' ' + generatedClassName;
}
if (context.className) {
classString += ' ' + context.className;
}
propsForElement[
// handle custom elements which React doesn't properly alias
isTag(elementToBeCreated) &&
!domElements.has(elementToBeCreated)
? 'class'
: 'className'
] = classString;
return createElement(elementToBeCreated, propsForElement);
}
我们可以看到这个函数返回一个 React 创建的元素,而这个元素的唯一类名是取决于 styledComponentId 、generatedClassName、context.className来生成的。接下来我们看看这三个东西是如何生成的。
styledComponentId
我们在 createStyledComponent 中找到 styledComponentId 的生成规则
const {
attrs = EMPTY_ARRAY,
componentId = generateId(options.displayName, options.parentComponentId),
displayName = generateDisplayName(target),
} = options;
// 生成 styledComponentId
const styledComponentId =
options.displayName && options.componentId
? `${escape(options.displayName)}-${options.componentId}`
: options.componentId || componentId;
一开始的时候 options 里面没有东西,会直接返回组件唯一id componentsId ,找到 generateId 方法查看是如何生成组件唯一id的,这是去掉类型精简后的代码
function generateId(
displayName,
parentComponentId
) {
const name = typeof displayName !== 'string' ? 'sc' : escape(displayName);
// Ensure that no displayName can lead to duplicate componentIds
identifiers[name] = (identifiers[name] || 0) + 1;
const componentId = `${name}-${generateComponentId(
// SC_VERSION gives us isolation between multiple runtimes on the page at once
// this is improved further with use of the babel plugin "namespace" feature
SC_VERSION + name + identifiers[name]
)}`;
return parentComponentId ? `${parentComponentId}-${componentId}` : componentId;
}
我们可以看到组件的唯一ID 是 name 加上 generateComponentId()函数对 sc 的版本号 + name + identifiers[name] 结果进行 has 加密后产生的唯一值后的组合。由于刚开始的时候 displayName 和 parentComponentId 都没有值,所以这里面的 name 就是字符串 sc;
如果我们调用 styled api一次创建了多个相同的元素,identifiers[name] 的值是根据相同元素个数递增来确保 identifiers[name] 不重复的
我们平常使用 styled api 创建样式化组件的时候,如果有观察的话,可以发现一般生成的类名都有两个 sc-xxxx xxxx ,根据这个结构特点我们发现生成的 componentsId 就是类名的第一个。而前面我们也有说到元素的唯一类名是取决于 styledComponentId 、generatedClassName来生成的,所以我猜类名的第二个应该和 generatedClassName 有很大的联系。
generatedClassName
// 生成组件的类名
const generatedClassName = useInjectedStyle(componentStyle, context);
我们看到入参里的 componentStyle ,在调用 createStyledComponent 的时候就实例化好并放到 WrappedStyledComponent 上了,
const componentStyle = new ComponentStyle(
rules,
styledComponentId,
isTargetStyledComp ? styledComponentTarget.componentStyle : undefined
);
WrappedStyledComponent.componentStyle = componentStyle;
return WrappedStyledComponent;
构造函数里面的 rules 其实就是 css 代码,而 styledComponentId 就是元素类的前一个 sc-xxxx;
继续查看 useInjectedStyle 代码
function useInjectedStyle(
componentStyle,
resolvedAttrs
) {
const ssc = useStyleSheetContext();
const className = componentStyle.generateAndInjectStyles(
resolvedAttrs,
ssc.styleSheet,
ssc.stylis
);
if (process.env.NODE_ENV !== 'production') useDebugValue(className);
return className;
}
它又调用了 generateAndInjectStyles 方法:
generateAndInjectStyles(
executionContext: ExecutionContext,
styleSheet: StyleSheet,
stylis: Stringifier
) {
let names = this.baseStyle
? this.baseStyle.generateAndInjectStyles(executionContext, styleSheet, stylis)
: '';
// force dynamic classnames if user-supplied stylis plugins are in use
if (this.isStatic && !stylis.hash) {
if (this.staticRulesId && styleSheet.hasNameForId(this.componentId, this.staticRulesId)) {
names = joinStrings(names, this.staticRulesId);
} else {
const cssStatic = joinStringArray(
flatten(this.rules, executionContext, styleSheet, stylis) as string[]
);
const name = generateName(phash(this.baseHash, cssStatic) >>> 0);
if (!styleSheet.hasNameForId(this.componentId, name)) {
const cssStaticFormatted = stylis(cssStatic, `.${name}`, undefined, this.componentId);
styleSheet.insertRules(this.componentId, name, cssStaticFormatted);
}
names = joinStrings(names, name);
this.staticRulesId = name;
}
} else {
let dynamicHash = phash(this.baseHash, stylis.hash);
let css = '';
for (let i = 0; i < this.rules.length; i++) {
const partRule = this.rules[i];
if (typeof partRule === 'string') {
css += partRule;
if (process.env.NODE_ENV !== 'production') dynamicHash = phash(dynamicHash, partRule);
} else if (partRule) {
const partString = joinStringArray(
flatten(partRule, executionContext, styleSheet, stylis) as string[]
);
dynamicHash = phash(dynamicHash, partString);
css += partString;
}
}
if (css) {
const name = generateName(dynamicHash >>> 0);
if (!styleSheet.hasNameForId(this.componentId, name)) {
styleSheet.insertRules(
this.componentId,
name,
stylis(css, `.${name}`, undefined, this.componentId)
);
}
names = joinStrings(names, name);
}
}
return names;
}
我们发现 name 受 dynamicHash 影响,dynamicHash 通过 this.bashHash 生成, this.bashHash 是通过 phash(SEED, componentId) 实现。 SEED 是通过 hash(SC_VERSION) 哈希sc的版本号生成。这个过程进行了很多次哈希处理,只需要找到对应元素的第一个类 componentsId 插入类就可以了。
然后只需要把创建好的样式插入到 <head></head> 中就可以了,我们找到 styleSheet.insertRules 方法
getTag() {
return this.tag || (this.tag = makeGroupedTag(makeTag(this.options)));
}
insertRules(id, name, rules) {
this.registerName(id, name);
this.getTag().insertRules(getGroupForId(id), rules);
}
因为初始化时 tag 为空,会调用 makeTag 方法:
/** Create a CSSStyleSheet-like tag depending on the environment */
export const makeTag = ({ isServer, useCSSOMInjection, target }: SheetOptions) => {
if (isServer) {
return new VirtualTag(target);
} else if (useCSSOMInjection) {
return new CSSOMTag(target);
} else {
return new TextTag(target);
}
};
export const CSSOMTag = class CSSOMTag implements Tag {
element: HTMLStyleElement;
sheet: CSSStyleSheet;
length: number;
constructor(target?: HTMLElement | undefined) {
this.element = makeStyleTag(target);
// Avoid Edge bug where empty style elements don't create sheets
this.element.appendChild(document.createTextNode(''));
this.sheet = getSheet(this.element);
this.length = 0;
}
insertRule(index: number, rule: string): boolean {
try {
this.sheet.insertRule(rule, index);
this.length++;
return true;
} catch (_error) {
return false;
}
}
deleteRule(index: number): void {
this.sheet.deleteRule(index);
this.length--;
}
getRule(index: number): string {
const rule = this.sheet.cssRules[index];
// Avoid IE11 quirk where cssText is inaccessible on some invalid rules
if (rule && rule.cssText) {
return rule.cssText;
} else {
return '';
}
}
};
/** A Tag that emulates the CSSStyleSheet API but uses text nodes */
export const TextTag = class TextTag implements Tag {
element: HTMLStyleElement;
nodes: NodeListOf<Node>;
length: number;
constructor(target?: HTMLElement | undefined) {
this.element = makeStyleTag(target);
this.nodes = this.element.childNodes;
this.length = 0;
}
insertRule(index: number, rule: string) {
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;
}
}
deleteRule(index: number) {
this.element.removeChild(this.nodes[index]);
this.length--;
}
getRule(index: number) {
if (index < this.length) {
return this.nodes[index].textContent as string;
} else {
return '';
}
}
};
/** A completely virtual (server-side) Tag that doesn't manipulate the DOM */
export const VirtualTag = class VirtualTag implements Tag {
rules: string[];
length: number;
constructor(_target?: HTMLElement | undefined) {
this.rules = [];
this.length = 0;
}
insertRule(index: number, rule: string) {
if (index <= this.length) {
this.rules.splice(index, 0, rule);
this.length++;
return true;
} else {
return false;
}
}
deleteRule(index: number) {
this.rules.splice(index, 1);
this.length--;
}
getRule(index: number) {
if (index < this.length) {
return this.rules[index];
} else {
return '';
}
}
};
我们看到 textTag 中的 insertRule 方法 通过 document.createTextNode() 将css代码作为文本节点的方式插入到 this.element 中,这个 this.element 是指什么呢?通过构造函数发现 this.element 是 makeStyleTag 这个方法构建的。
还记得我们最开始猜想的 style api是通过 document.createElement('style') 来创建 style 标签然后插入到 head 标签里面实现的吗?来看代码:
export const makeStyleTag = (target) => {
const head = document.head;
const parent = target || head;
const style = document.createElement('style');
// style 标签的插入位置
const prevStyle = findLastStyleTag(parent);
const nextSibling = prevStyle !== undefined ? prevStyle.nextSibling : null;
// 自定义属性 data-set
style.setAttribute(SC_ATTR, SC_ATTR_ACTIVE);
style.setAttribute(SC_ATTR_VERSION, SC_VERSION);
const nonce = getNonce();
// 设置 style 标签的 nonce 属性:一种加密的随机数(一次使用的数字)
if (nonce) style.setAttribute('nonce', nonce);
// 在父节点里的最后一个子节点的位置插入新的 style 节点
parent.insertBefore(style, nextSibling);
return style;
};
makeStyleTag 通过 document.createElement('style') 创建 style 标签 然后设置了 SC_ATTR , SC_ATTR_VERSION 属性。然后在父节点里的最后一个子节点的位置插入新的 style 节点。
sc 的基本核心实现源码就是这些了,源码分析暂时结束了,后面遇到其他api的疑问也可以通过查看源码的方式来解决。
写在最后
我是 AndyHu,目前暂时是一枚前端搬砖工程师。
文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注呀😊
未经许可禁止转载💌
speak less,do more.