antd button源码解析

1,202 阅读5分钟

前言

想要学习react的优雅写法,所以翻出antd的源码来读。记录一下button的源码解读

组件库涉及的通用库:

源码分析

// 这里是概要
const InternalButton = (props, ref) => {
  // 从props中解析属性值
  const {xx, xxx, ...} = props;
  // size和config通过createontext的方式来传递
  const size = React.useContext(SizeContext);
  const { xx, xxx } = React.useContext(ConfigContext);
  // 设置过渡态和是否要在俩中文字插入空格的标志位
  const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
  const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
  // 设置button的ref
  const buttonRef = (ref as any) || React.createRef<HTMLElement>();
  // 设置保存延迟倒计时需求的定时器,通过useRef保存,可以“跨渲染周期”保存数据
  // 这样即使重新渲染也能保存定时器,后续方便清理
  // mark(这个是楼主以前没有用到过的技巧)
  const delayTimeoutRef = React.useRef<number>();
  // 是否需要在字间插入空格
  const isNeedInserted = () => {}
  // 确认按钮内容是否是俩中文字符
  const fixTwoCNChar = () => {}
  // 确认是否设置loading或延迟标志位
  let loadingOrDelay = xxx
  
  // 观测loadingOrDelay设置定时器
  // 观测buttonRef嗲调用fixTwoCNChar
  React.useEffect(() => {})
  
  // 监听点击事件
  const handleClick = () => {}
  
  // 利用classNames获取样式名称
  
  // 获取图标节点和btn里面放的内容
  const iconNode =
  const kids = 
  
  // 定义button,将样式和事件附加
  const = buttonNode
  
  // 条件决定是否又wave组件包裹
}

// 这里是源码详解
const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
  const {
    loading = false,
    prefixCls: customizePrefixCls,
    type,
    danger,
    shape,
    size: customizeSize,
    className,
    children,
    icon,
    ghost = false,
    block = false,
    /** If we extract items here, we don't need use omit.js */
    // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
    htmlType = 'button' as ButtonProps['htmlType'],
    ...rest
  } = props;

  const size = React.useContext(SizeContext);
  // 用于控制异步返回函数的loading态
  const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
  // 是否是两个中文字符,如果是的话,自动填充空格在两个字之间
  const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
  const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext);
  const buttonRef = (ref as any) || React.createRef<HTMLElement>();
  // 计时器的ref
  const delayTimeoutRef = React.useRef<number>();

  const isNeedInserted = () =>
    React.Children.count(children) === 1 && !icon && !isUnborderedButtonType(type);

  // 需要插入并且是两个中文字,标志位还未被置位时更新标志位
  const fixTwoCNChar = () => {
    // Fix for HOC usage like <FormatMessage />
    if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) {
      return;
    }
    const buttonText = buttonRef.current.textContent;
    if (isNeedInserted() && isTwoCNChar(buttonText)) {
      if (!hasTwoCNChar) {
        setHasTwoCNChar(true);
      }
    } else if (hasTwoCNChar) {
      setHasTwoCNChar(false);
    }
  };

  // =============== Update Loading ===============
  // 如果loading是对象又设定了延迟标志,就确定设置标志位
  let loadingOrDelay: Loading;
  if (typeof loading === 'object' && loading.delay) {
    loadingOrDelay = loading.delay || true;
  } else {
    loadingOrDelay = !!loading;
  }

  // 更新loading态。如果有设置延迟就设置定时器,并保存在ref中便于清理
  React.useEffect(() => {
    clearTimeout(delayTimeoutRef.current);
    if (typeof loadingOrDelay === 'number') {
      delayTimeoutRef.current = window.setTimeout(() => {
        setLoading(loadingOrDelay);
      }, loadingOrDelay);
    } else {
      setLoading(loadingOrDelay);
    }
  }, [loadingOrDelay]);

  // 当按钮发生变化时候
  React.useEffect(fixTwoCNChar, [buttonRef]);

  // 按钮点击时只有非loading非禁止才调用函数
  const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
    const { onClick, disabled } = props;
    // https://github.com/ant-design/ant-design/issues/30207
    if (innerLoading || disabled) {
      e.preventDefault();
      return;
    }
    (onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)?.(e);
  };

  devWarning(
    !(typeof icon === 'string' && icon.length > 2),
    'Button',
    `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
  );

  devWarning(
    !(ghost && isUnborderedButtonType(type)),
    'Button',
    "`link` or `text` button can't be a `ghost` button.",
  );

  const prefixCls = getPrefixCls('btn', customizePrefixCls);
  const autoInsertSpace = autoInsertSpaceInButton !== false;

  // large => lg
  // small => sm
  // 根据size得到对应的class描述
  let sizeCls = '';
  switch (customizeSize || size) {
    case 'large':
      sizeCls = 'lg';
      break;
    case 'small':
      sizeCls = 'sm';
      break;
    default:
      break;
  }

  const iconType = innerLoading ? 'loading' : icon;

  const classes = classNames(
    prefixCls,
    {
      [`${prefixCls}-${type}`]: type,
      [`${prefixCls}-${shape}`]: shape,
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
      [`${prefixCls}-background-ghost`]: ghost && !isUnborderedButtonType(type),
      [`${prefixCls}-loading`]: innerLoading,
      [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace,
      [`${prefixCls}-block`]: block,
      [`${prefixCls}-dangerous`]: !!danger,
      [`${prefixCls}-rtl`]: direction === 'rtl',
    },
    className,
  );

  const iconNode =
    icon && !innerLoading ? (
      icon
    ) : (
      <LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={!!innerLoading} />
    );

  const kids =
    children || children === 0
      ? spaceChildren(children, isNeedInserted() && autoInsertSpace)
      : null;

  // moit移出对象的指定属性,而实现浅拷贝
  const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);
  if (linkButtonRestProps.href !== undefined) {
    return (
      <a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
        {iconNode}
        {kids}
      </a>
    );
  }

  const buttonNode = (
    <button
      {...(rest as NativeButtonProps)}
      type={htmlType}
      className={classes}
      onClick={handleClick}
      ref={buttonRef}
    >
      {iconNode}
      {kids}
    </button>
  );

  if (isUnborderedButtonType(type)) {
    return buttonNode;
  }

  return <Wave>{buttonNode}</Wave>;
};

const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent;

Button.displayName = 'Button';

Button.Group = Group;
Button.__ANT_BUTTON = true;

分析详解最后提到的wave组件,其实就是给传入的组件添加动画效果的一个包裹组件

关于颜色样式的less写法 因为这里的按钮有几种基本样式基于色调在变,所以特地试了下less的一些函数写法。 包括each生成一组颜色;lighten,darken生成相近颜色;contrast生成和背景差异大的字颜色

@prefixcls: shell;

@primary-color: #001F97;
@error-color: #680506;
@success-color: #033017;

.get-color(@key-name) {
  each(range(4), {
    .@{prefixcls}-@{key-name}-@{key} {
      color: contrast(@@key-name);
      background: lighten(@@key-name, @value * 8%);
      &.active, &:active {
        background: lighten(@@key-name, @value * 12%);
      }
      &.hover, &:hover {
        background: darken(@@key-name, @value * 4%);
      }
      background-color: @@key-name;
    }
  })
}

.get-color(primary-color);
.get-color(error-color);
.get-color(success-color);

生成css如下

.shell-primary-color-1 {
  color: #ffffff;
  background: #0027c0;
  background-color: #001F97;
}
.shell-primary-color-1.active,
.shell-primary-color-1:active {
  background: #002cd4;
}
.shell-primary-color-1.hover,
.shell-primary-color-1:hover {
  background: #001b83;
}
.shell-primary-color-2 {
  color: #ffffff;
  background: #0030e9;
  background-color: #001F97;
}
.shell-primary-color-2.active,
.shell-primary-color-2:active {
  background: #1243ff;
}
.shell-primary-color-2.hover,
.shell-primary-color-2:hover {
  background: #00176e;
}
.shell-primary-color-3 {
  color: #ffffff;
  background: #1243ff;
  background-color: #001F97;
}
.shell-primary-color-3.active,
.shell-primary-color-3:active {
  background: #5074ff;
}
.shell-primary-color-3.hover,
.shell-primary-color-3:hover {
  background: #00125a;
}
.shell-primary-color-4 {
  color: #ffffff;
  background: #3b63ff;
  background-color: #001F97;
}
.shell-primary-color-4.active,
.shell-primary-color-4:active {
  background: #8da4ff;
}
.shell-primary-color-4.hover,
.shell-primary-color-4:hover {
  background: #000e45;
}
.shell-error-color-1 {
  color: #ffffff;
  background: #8f0708;
  background-color: #680506;
}
.shell-error-color-1.active,
.shell-error-color-1:active {
  background: #a20809;
}
.shell-error-color-1.hover,
.shell-error-color-1:hover {
  background: #550405;
}
.shell-error-color-2 {
  color: #ffffff;
  background: #b6090a;
  background-color: #680506;
}
.shell-error-color-2.active,
.shell-error-color-2:active {
  background: #dd0b0d;
}
.shell-error-color-2.hover,
.shell-error-color-2:hover {
  background: #410304;
}
.shell-error-color-3 {
  color: #ffffff;
  background: #dd0b0d;
  background-color: #680506;
}
.shell-error-color-3.active,
.shell-error-color-3:active {
  background: #f53032;
}
.shell-error-color-3.hover,
.shell-error-color-3:hover {
  background: #2e0203;
}
.shell-error-color-4 {
  color: #ffffff;
  background: #f41c1e;
  background-color: #680506;
}
.shell-error-color-4.active,
.shell-error-color-4:active {
  background: #f86a6b;
}
.shell-error-color-4.hover,
.shell-error-color-4:hover {
  background: #1a0102;
}
.shell-success-color-1 {
  color: #ffffff;
  background: #055629;
  background-color: #033017;
}
.shell-success-color-1.active,
.shell-success-color-1:active {
  background: #076a33;
}
.shell-success-color-1.hover,
.shell-success-color-1:hover {
  background: #021d0e;
}
.shell-success-color-2 {
  color: #ffffff;
  background: #087d3c;
  background-color: #033017;
}
.shell-success-color-2.active,
.shell-success-color-2:active {
  background: #0aa34e;
}
.shell-success-color-2.hover,
.shell-success-color-2:hover {
  background: #010a05;
}
.shell-success-color-3 {
  color: #ffffff;
  background: #0aa34e;
  background-color: #033017;
}
.shell-success-color-3.active,
.shell-success-color-3:active {
  background: #0edd6a;
}
.shell-success-color-3.hover,
.shell-success-color-3:hover {
  background: #000000;
}
.shell-success-color-4 {
  color: #ffffff;
  background: #0dca61;
  background-color: #033017;
}
.shell-success-color-4.active,
.shell-success-color-4:active {
  background: #35f289;
}
.shell-success-color-4.hover,
.shell-success-color-4:hover {
  background: #000000;
}