antd原始碼解讀(3)- Button

markzzw發表於2019-03-04

Button

Button包括了兩個元件,ButtonButtonGroup

ButtonProps

看一個元件首先看的是他的傳參也就是props,所以我們這裡先看Button元件的ButtonProps

  export type ButtonType = 'primary' | 'ghost' | 'dashed' | 'danger';
  export type ButtonShape = 'circle' | 'circle-outline';
  export type ButtonSize = 'small' | 'large';

  // typescript語法,這裡表示的是一些引數,引數後面跟上 ? 是可選引數的意思,不跟就是必須引數
  // 引數後面所跟的就是引數的型別,型別可以是自定義的型別,就如‘ButtonType’,‘ButtonShape’,‘ButtonSize’
  // 也可以是函式或者類,如React.FormEventHandler<any> 
  // 詳情請看這裡 https://www.tslang.cn/docs/handbook/interfaces.html
  export interface ButtonProps {
    type?: ButtonType;
    htmlType?: string;
    icon?: string;
    shape?: ButtonShape;
    size?: ButtonSize;
    onClick?: React.FormEventHandler<any>;
    onMouseUp?: React.FormEventHandler<any>;
    onMouseDown?: React.FormEventHandler<any>;
    loading?: boolean | { delay?: number };
    disabled?: boolean;
    style?: React.CSSProperties;
    prefixCls?: string;
    className?: string;
    ghost?: boolean;
  }複製程式碼

Render()

看完其引數有哪些之後我們就直接跳過元件內部的其他的東西,直接看他的渲染函式,畢竟這裡是執行的入口
這裡順帶提一下這句程式碼

// 這裡的意思是將傳入兩個引數,React.Component的引數第一個是Props,第二個是state,
// 然後利用typescript的型別檢查,Props型別需要時上面定義的ButtonProps中的可選引數中的變數名
// state這裡傳入任意都行
export default class Button extends React.Component<ButtonProps, any>複製程式碼
// 接下來是render()
  render() {
    // 將引數從props解構出來
    const {
      type, shape, size = '', className, htmlType, children, icon, prefixCls, ghost, ...others,
    } = this.props;
    // 將loading和clicked兩個狀態從state解構
    const { loading, clicked } = this.state;

    // large => lg
    // small => sm
    let sizeCls = '';
    switch (size) {
      case 'large':
        sizeCls = 'lg';
        break;
      case 'small':
        sizeCls = 'sm';
      default:
        break;
    }
    // 組建樣式
    const classes = classNames(prefixCls, className, {
      [`${prefixCls}-${type}`]: type,
      [`${prefixCls}-${shape}`]: shape,
      [`${prefixCls}-${sizeCls}`]: sizeCls,
      [`${prefixCls}-icon-only`]: !children && icon,
      [`${prefixCls}-loading`]: loading,
      [`${prefixCls}-clicked`]: clicked,
      [`${prefixCls}-background-ghost`]: ghost,
    });

    // 是否需要載入
    const iconType = loading ? 'loading' : icon;
    // 是否需要新增Icon,不過官方給的是如果需要用到icon的話最好自己寫在裡面

    const iconNode = iconType ? <Icon type={iconType} /> : null; 

    const needInserted = React.Children.count(children) === 1 && (!iconType || iconType === 'loading');

    // 重點在這裡,敲黑板了
    // 這裡引用了React.Children.map這個函式來對這個包裹在這個Button元件中的內容渲染出來
    // 其中insertSpace()這個函式也有意思,這個函式主要是為了在當組建中間寫的是中文漢字的時
    // 候給其漢字之間新增一個空格作為分隔,這裡有的同學會問為什麼不用css裡面的letter-space
    // 屬性,這個我也不是很清楚。。。不過他不用的話可能是是不想在英文字母中間新增空格吧

    const kids = React.Children.map(children, child => insertSpace(child, needInserted));

    return (
      <button
        // 還是和Icon元件一樣,將用不到的props去除掉
        {...omit(others, ['loading', 'clicked'])}
        type={htmlType || 'button'}
        className={classes}
        onClick={this.handleClick}
      >
        {iconNode}{kids}
      </button>
    );
  }複製程式碼

InsertSpace()

上面講到了這個函式,這裡就來仔細看看是幹嘛的吧

  const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
  // 這裡的bind有必要好好的理解一下
  const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
  function isString(str: any) {
    return typeof str === 'string';
  }

  // Insert one space between two chinese characters automatically.
  function insertSpace(child: React.ReactChild, needInserted: boolean) {
    // Check the child if is undefined or null.
    if (child == null) {
      return;
    }
    const SPACE = needInserted ? ' ' : '';
    // strictNullChecks oops.
    // 這個判斷的意思是當這個child不是字串也不是數字並且child.type為字串並且child的children是漢字的情況下
    // 給其加上空格,上面說的是程式碼直譯,那麼程式碼意譯下來就是這樣的一個情況
    // 這種情況(所以這裡他才會有一個英文註釋,說的是不是嚴格意義的檢查,啊哈哈,尷尬的實現方法)
    // <Button>
    //   <div>你好啊</div>
    // </Button>
    // 這裡說明一下,child.type以及child.props.children是react在渲染的時候會給虛擬dom新增的一些屬性,如圖
    if (typeof child !== 'string' && typeof child !== 'number' &&
      isString(child.type) && isTwoCNChar(child.props.children)) {
      return React.cloneElement(child, {},
        child.props.children.split('').join(SPACE));
    }
    // 這種情況就很明瞭了 就是Button元件中寫的漢字
    if (typeof child === 'string') {
      if (isTwoCNChar(child)) {
        child = child.split('').join(SPACE);
      }
      return <span>{child}</span>;
    }
    return child;
  }複製程式碼

Button
Button

完整原始碼

剩下的都是一些簡單的東西,也沒有什麼可以講的了

  import React from 'react';
  import PropTypes from 'prop-types';
  import classNames from 'classnames';
  import omit from 'omit.js';
  import Icon from '../icon';
  import Group from './button-group';

  const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/;
  const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
  function isString(str: any) {
    return typeof str === 'string';
  }

  // Insert one space between two chinese characters automatically.
  function insertSpace(child: React.ReactChild, needInserted: boolean) {
    // Check the child if is undefined or null.
    if (child == null) {
      return;
    }
    const SPACE = needInserted ? ' ' : '';
    // strictNullChecks oops.
    if (typeof child !== 'string' && typeof child !== 'number' &&
      isString(child.type) && isTwoCNChar(child.props.children)) {
      return React.cloneElement(child, {},
        child.props.children.split('').join(SPACE));
    }
    if (typeof child === 'string') {
      if (isTwoCNChar(child)) {
        child = child.split('').join(SPACE);
      }
      return <span>{child}</span>;
    }
    return child;
  }

  export type ButtonType = 'primary' | 'ghost' | 'dashed' | 'danger';
  export type ButtonShape = 'circle' | 'circle-outline';
  export type ButtonSize = 'small' | 'large';

  export interface ButtonProps {
    type?: ButtonType;
    htmlType?: string;
    icon?: string;
    shape?: ButtonShape;
    size?: ButtonSize;
    onClick?: React.FormEventHandler<any>;
    onMouseUp?: React.FormEventHandler<any>;
    onMouseDown?: React.FormEventHandler<any>;
    loading?: boolean | { delay?: number };
    disabled?: boolean;
    style?: React.CSSProperties;
    prefixCls?: string;
    className?: string;
    ghost?: boolean;
  }

  export default class Button extends React.Component<ButtonProps, any> {
    // 這裡這樣子寫只是為了方便這樣子寫Button.Group來引用ButtonGroup這個元件,下一節將會講解這個元件
    static Group: typeof Group;
    static __ANT_BUTTON = true;

    static defaultProps = {
      prefixCls: 'ant-btn',
      loading: false,
      clicked: false,
      ghost: false,
    };

    static propTypes = {
      type: PropTypes.string,
      shape: PropTypes.oneOf(['circle', 'circle-outline']),
      size: PropTypes.oneOf(['large', 'default', 'small']),
      htmlType: PropTypes.oneOf(['submit', 'button', 'reset']),
      onClick: PropTypes.func,
      loading: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
      className: PropTypes.string,
      icon: PropTypes.string,
    };

    timeout: number;
    delayTimeout: number;

    constructor(props: ButtonProps) {
      super(props);
      this.state = {
        loading: props.loading,
      };
    }

    componentWillReceiveProps(nextProps: ButtonProps) {
      const currentLoading = this.props.loading;
      const loading = nextProps.loading;

      if (currentLoading) {
        clearTimeout(this.delayTimeout);
      }

      if (typeof loading !== 'boolean' && loading && loading.delay) {
        this.delayTimeout = setTimeout(() => this.setState({ loading }), loading.delay);
      } else {
        this.setState({ loading });
      }
    }

    // 在元件銷燬的時候一定要記得將定時器也一同銷燬
    componentWillUnmount() {
      if (this.timeout) {
        clearTimeout(this.timeout);
      }
      if (this.delayTimeout) {
        clearTimeout(this.delayTimeout);
      }
    }

    handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
      // Add click effect
      this.setState({ clicked: true });
      clearTimeout(this.timeout);
      this.timeout = setTimeout(() => this.setState({ clicked: false }), 500);

      const onClick = this.props.onClick;
      if (onClick) {
        onClick(e);
      }
    }

    render() {
      const {
        type, shape, size = '', className, htmlType, children, icon, prefixCls, ghost, ...others,
      } = this.props;

      const { loading, clicked } = this.state;

      // large => lg
      // small => sm
      let sizeCls = '';
      switch (size) {
        case 'large':
          sizeCls = 'lg';
          break;
        case 'small':
          sizeCls = 'sm';
        default:
          break;
      }

      const classes = classNames(prefixCls, className, {
        [`${prefixCls}-${type}`]: type,
        [`${prefixCls}-${shape}`]: shape,
        [`${prefixCls}-${sizeCls}`]: sizeCls,
        [`${prefixCls}-icon-only`]: !children && icon,
        [`${prefixCls}-loading`]: loading,
        [`${prefixCls}-clicked`]: clicked,
        [`${prefixCls}-background-ghost`]: ghost,
      });

      const iconType = loading ? 'loading' : icon;
      const iconNode = iconType ? <Icon type={iconType} /> : null;
      const needInserted = React.Children.count(children) === 1 && (!iconType || iconType === 'loading');
      const kids = React.Children.map(children, child => insertSpace(child, needInserted));

      return (
        <button
          {...omit(others, ['loading', 'clicked'])}
          type={htmlType || 'button'}
          className={classes}
          onClick={this.handleClick}
        >
          {iconNode}{kids}
        </button>
      );
    }
  }複製程式碼

相關文章