Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

styled-components 源码阅读 #49

Open
wangpin34 opened this issue Oct 31, 2019 · 0 comments
Open

styled-components 源码阅读 #49

wangpin34 opened this issue Oct 31, 2019 · 0 comments

Comments

@wangpin34
Copy link
Owner

wangpin34 commented Oct 31, 2019

本次阅读的源码包括:
https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constructors/styled.js
https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constructors/constructWithOptions.js
https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/models/StyledComponent.js

约定一下简称,方便阅读:
SC - styled-component, 即一个 styled-component


详细解析

styled.js

styled.js 是 styled API 的入口,常用的 API 如 styled.div 或者 styled(MyComponent) 即是从这个文件开始的,主要代码很少,如下所示:

styled.js

import constructWithOptions from './constructWithOptions';
import StyledComponent from '../models/StyledComponent';
import domElements from '../utils/domElements';

import type { Target } from '../types';

const styled = (tag: Target) => constructWithOptions(StyledComponent, tag);

// Shorthands for all valid HTML Elements
domElements.forEach(domElement => {
  styled[domElement] = styled(domElement);
});

export default styled;

styled 函数接收一个 targe (预定义组件如 div 或者任何自定义组件),返回 constructWithOptions 函数的运算结果。后面我们会看到 constructWithOptions 的运算结果就是生成一个 SC。

constructWithOptions.js

constructWithOptions 的代码也不多,如下所示:
constructWithOptions.js

export default function constructWithOptions(
  componentConstructor: Function,
  tag: Target,
  options: Object = EMPTY_OBJECT
) {
  if (!isValidElementType(tag)) {
    throw new StyledError(1, String(tag));
  }

  /* This is callable directly as a template function */
  // $FlowFixMe: Not typed to avoid destructuring arguments
  const templateFunction = (...args) => componentConstructor(tag, options, css(...args));

  /* If config methods are called, wrap up a new template function and merge options */
  templateFunction.withConfig = config =>
    constructWithOptions(componentConstructor, tag, { ...options, ...config });

  /* Modify/inject new props at runtime */
  templateFunction.attrs = attrs =>
    constructWithOptions(componentConstructor, tag, {
      ...options,
      attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
    });

  return templateFunction;
}

我们可以看到函数返回一个叫做 templateFunction 的函数,它会把传入的 args 作为 css 内容,交由 css-helper 生成为 style-sheet,然后调用 componentConstructor 以生成 SC。在上一个文件中,我们已经知道这里的 componentConstructor 是指第三个文件 StyledComponent.js 的 default export 的函数。这里可以预先说明一下这个函数,主要功能就是根据 tag 和 css 生成 styled-component。

另外一个值得注意的是 withConfig 和 attrs 的实现,attrs 和 config 的内容会被合并到 options,最后依然递归调用 constructWithOptions。这也是为什么我们在定义 SC 时像 Jquery 那样写链式代码。有兴趣的可以搜一下关于 Jquery 链式操作的文章,这里不多说。

StyledComponent.js

这个文件比较长,还是按照调用顺序,我们先看一下上面提到的 default export。

export default function createStyledComponent(target: Target, options: Object, rules: RuleSet) {
  const isTargetStyledComp = isStyledComponent(target);
  const isClass = !isTag(target);

  const {
    displayName = generateDisplayName(target),
    componentId = generateId(ComponentStyle, options.displayName, options.parentComponentId),
    ParentComponent = StyledComponent,
    attrs = EMPTY_ARRAY,
  } = options;

  const styledComponentId =
    options.displayName && options.componentId
      ? `${escape(options.displayName)}-${options.componentId}`
      : options.componentId || componentId;

  // fold the underlying StyledComponent attrs up (implicit extend)
  const finalAttrs =
    // $FlowFixMe
    isTargetStyledComp && target.attrs
      ? Array.prototype.concat(target.attrs, attrs).filter(Boolean)
      : attrs;

  const componentStyle = new ComponentStyle(
    isTargetStyledComp
      ? // fold the underlying StyledComponent rules up (implicit extend)
        // $FlowFixMe
        target.componentStyle.rules.concat(rules)
      : rules,
    finalAttrs,
    styledComponentId
  );

  /**
   * forwardRef creates a new interim component, which we'll take advantage of
   * instead of extending ParentComponent to create _another_ interim class
   */
  let WrappedStyledComponent;
  const forwardRef = (props, ref) => (
    <ParentComponent {...props} forwardedComponent={WrappedStyledComponent} forwardedRef={ref} />
  );
  forwardRef.displayName = displayName;
  WrappedStyledComponent = React.forwardRef(forwardRef);
  WrappedStyledComponent.displayName = displayName;

  // $FlowFixMe
  WrappedStyledComponent.attrs = finalAttrs;
  // $FlowFixMe
  WrappedStyledComponent.componentStyle = componentStyle;

  // $FlowFixMe
  WrappedStyledComponent.foldedComponentIds = isTargetStyledComp
    ? // $FlowFixMe
      Array.prototype.concat(target.foldedComponentIds, target.styledComponentId)
    : EMPTY_ARRAY;

  // $FlowFixMe
  WrappedStyledComponent.styledComponentId = styledComponentId;

  // fold the underlying StyledComponent target up since we folded the styles
  // $FlowFixMe
  WrappedStyledComponent.target = isTargetStyledComp ? target.target : target;

  // $FlowFixMe
  WrappedStyledComponent.withComponent = function withComponent(tag: Target) {
    const { componentId: previousComponentId, ...optionsToCopy } = options;

    const newComponentId =
      previousComponentId &&
      `${previousComponentId}-${isTag(tag) ? tag : escape(getComponentName(tag))}`;

    const newOptions = {
      ...optionsToCopy,
      attrs: finalAttrs,
      componentId: newComponentId,
      ParentComponent,
    };

    return createStyledComponent(tag, newOptions, rules);
  };

  // $FlowFixMe
  Object.defineProperty(WrappedStyledComponent, 'defaultProps', {
    get() {
      return this._foldedDefaultProps;
    },

    set(obj) {
      // $FlowFixMe
      this._foldedDefaultProps = isTargetStyledComp ? merge(target.defaultProps, obj) : obj;
    },
  });

  if (process.env.NODE_ENV !== 'production') {
    // $FlowFixMe
    WrappedStyledComponent.warnTooManyClasses = createWarnTooManyClasses(displayName);
  }

  // $FlowFixMe
  WrappedStyledComponent.toString = () => `.${WrappedStyledComponent.styledComponentId}`;

  if (isClass) {
    hoist(WrappedStyledComponent, target, {
      // all SC-specific things should not be hoisted
      attrs: true,
      componentStyle: true,
      displayName: true,
      foldedComponentIds: true,
      styledComponentId: true,
      target: true,
      withComponent: true,
    });
  }

  return WrappedStyledComponent;
}

先不理最前面准备数据的部分,从 ** let WrappedStyledComponent; ** 看起。我们看到这里通过 React.forwardRef 将 ref 传递给 SC (也就是这个 ParentComponent)。比较特别的,WrappedStyledComponent 的构造函数也作为一个 prop 传递给了 SC。这里的真实目的是将后续赋予给 WrappedStyledComponent 诸多 props 如 id,attrs,css,捎带给 SC。

  const forwardRef = (props, ref) => (
    <ParentComponent {...props} forwardedComponent={WrappedStyledComponent} forwardedRef={ref} />
  );
  WrappedStyledComponent = React.forwardRef(forwardRef);

下面是 SC 的 render 方法中,使用这些 props 创建内容。期间历经多次的数据预处理,组合与变换。

  renderInner(theme?: Theme) {
    const {
      componentStyle,
      defaultProps,
      displayName,
      foldedComponentIds,
      styledComponentId,
      target,
    } = this.props.forwardedComponent;

    let generatedClassName;
    if (componentStyle.isStatic) {
      generatedClassName = this.generateAndInjectStyles(EMPTY_OBJECT, this.props);
    } else {
      generatedClassName = this.generateAndInjectStyles(
        determineTheme(this.props, theme, defaultProps) || EMPTY_OBJECT,
        this.props
      );
    }

    const elementToBeCreated = this.props.as || this.attrs.as || target;
    const isTargetTag = isTag(elementToBeCreated);

    const propsForElement = {};
    const computedProps = { ...this.props, ...this.attrs };

    let key;
    // eslint-disable-next-line guard-for-in
    for (key in computedProps) {
      if (process.env.NODE_ENV !== 'production' && key === 'innerRef' && isTargetTag) {
        this.warnInnerRef(displayName);
      }

      if (key === 'forwardedComponent' || key === 'as') {
        continue;
      } else if (key === 'forwardedRef') propsForElement.ref = computedProps[key];
      else if (key === 'forwardedAs') propsForElement.as = computedProps[key];
      else if (!isTargetTag || validAttr(key)) {
        // Don't pass through non HTML tags through to HTML elements
        propsForElement[key] = computedProps[key];
      }
    }

    if (this.props.style && this.attrs.style) {
      propsForElement.style = { ...this.attrs.style, ...this.props.style };
    }

    propsForElement.className = Array.prototype
      .concat(
         ,
        styledComponentId,
        generatedClassName !== styledComponentId ? generatedClassName : null,
        this.props.className,
        this.attrs.className
      )
      .filter(Boolean)
      .join(' ');

    return createElement(elementToBeCreated, propsForElement);
  }

比较有意思的几个点:

  1. 在开发模式下,styled-components 会尝试警告 innerRef 在 v4 中已经废弃
  2. 我们第一次注意到熟悉的 className。我们经常会发现明明只 assign 了一个 className,但实际的 html 标签上却有很多 className 连接在一起。这里说明了这些 className 的来源:
  • 根据 style 内容动态生成
  • 通过 attrs 手动添加的 className
  • Props 引入的 className
  • 其他:styledComponentId (display name + componentId) 和 foldedComponentIds(target
    SC 的 styledComponentId + foldedComponentIds)

总结

这三个文件虽然总代码量并不大,但基本算是 styled-components library 的核心了。我们看到了以下问题的答案:

  1. styled.div.css rules 是如何工作的?
    styled-components 官方介绍说 styled API 使用了 es6 的 Tagged Template Literals。其实这个东西我几乎每天都在用,比如输出 log :
console.log(`Today: ${Date.now()}`)

之所以我会觉得 styled 的方式难以理解,是因为我一直没有看到这个知识点:标签模版
。阅读完之后你会知道,以下写法是等价的:

styled.div`display: block;`
styled.div('display: block')

如果模板里包含变量,则会进行更复杂的转换:

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

这解释了为什么 constructWithOptions.js 中 args(css 内容)会以下面的方式传递进来。

  const templateFunction = (...args) => componentConstructor(tag, options, css(...args));
  1. SC 是如何与 style 绑定的?
    每个 SC 拥有自己的 className 数组,我们称为 list。首先,SC 会计算并生成自己的 className ,添加到 list 中。 这个 className 会 match 到自己的 style-sheet(即模板中定义的 css 内容)。然后,如果 target 也是 SC,则当前 SC 也会将 target 的 className 添加到 list 。这样,list 中就包含了自己 style 的 className,和 target SC 的 className。所以, SC 就能利用到 target 的 style,相应的, target 也会同样利用到自己 target(如果有) 的 style。这看起来很像是面向对象的继承,起码起到了继承的效果不是。

能够继承 style,是 styled-components 一个很强大的地方。另一个方面,每个 SC 的 style 是独占的,别的 SC 不可访问更不可修改,从这个角度来说,也算是具有一定的封装。你看,面向对象的特征嘛。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant