antd源碼解讀(8.2)- 番外篇 Trigger 之 index.js 完整篇

Trigger

這個組件的index文件就有不少代碼,590行代碼,並且在頭部引入的額外文件特別的多,因此咱們這一個組件就先從這些額外的組件中開始吧,先看看這些外部方法可以作些什麼。html

強烈建議把tigger的代碼下載下來自行查看,由於實在是太長了node

// index.js 頭部
  import PropTypes from 'prop-types';
  import { findDOMNode, createPortal } from 'react-dom';
  import createReactClass from 'create-react-class';
  import contains from 'rc-util/lib/Dom/contains';
  import addEventListener from 'rc-util/lib/Dom/addEventListener';
  import Popup from './Popup';
  import { getAlignFromPlacement, getPopupClassNameFromAlign } from './utils';
  import getContainerRenderMixin from 'rc-util/lib/getContainerRenderMixin';
  import Portal from 'rc-util/lib/Portal';複製代碼

createPortal

在官網這裏有這麼一個解釋react

ReactDOM.createPortal(child, container)複製代碼

Creates a portal. Portals provide a way to render children into a DOM node that exists outside the hierarchy of the DOM component.git

這個函數是用來建立一個portal,而這個Portal是提供一個方法來在指定的dom元素渲染一些組件的方法。es6

createReactClass

這個函數也是可以在官網這裏上找到的,是用來建立一個raect類而不是用es6語法的方法,在裏面可使用getDefaultProps()方法github

建立當前組件的默認props,可使用getInitialState()建立當前組件的初始state,而且在裏面寫的方法都會自動的綁定上this,api

也就是他所說的Autobinding,還有一個最有用的屬性Mixins,這個是可以在編寫不少的可以使用的外部方法傳入組件的屬性。數組

contains && addEventListener

這兩個函數都是rc-util/lib/Dom/裏面的工具函數,接下來咱們分辨看看這兩個函數可以作啥瀏覽器

// contains.js

// 這個函數是用來判斷傳入根節點root是否包含傳入節點n,
// 若是包含則返回true,否者返回false
  export default function contains(root, n) {
    let node = n;
    while (node) {
      if (node === root) {
        return true;
      }
      node = node.parentNode;
    }

    return false;
  }複製代碼
// addEventListener.js
// 這個函數主要的聚焦點是ReactDOM.unstable_batchedUpdates
// 這個api是沒有公開的一個api,可是可使用,爲了是想要將當前的組件狀態強制性的
// 更新到組件內部去而且,可是這樣作的目的可能有點粗暴。。
// 想要了解的能夠看這篇文章,或許你有新的想法
// https://zhuanlan.zhihu.com/p/20328570
  import addDOMEventListener from 'add-dom-event-listener';
  import ReactDOM from 'react-dom';

  export default function addEventListenerWrap(target, eventType, cb) {
    /* eslint camelcase: 2 */
    const callback = ReactDOM.unstable_batchedUpdates ? function run(e) {
      ReactDOM.unstable_batchedUpdates(cb, e);
    } : cb;
    return addDOMEventListener(target, eventType, callback);
  }複製代碼

getContainerRenderMixin && Portal

接下來是這兩個函數,都是來自於rc-util/lib/antd

// getContainerRenderMixin.js

  import ReactDOM from 'react-dom';

  function defaultGetContainer() {
    const container = document.createElement('div');
    document.body.appendChild(container);
    return container;
  }

  export default function getContainerRenderMixin(config) {
    const {
      autoMount = true,
      autoDestroy = true,
      isVisible,
      getComponent,
      getContainer = defaultGetContainer,
    } = config;

    let mixin;

    function renderComponent(instance, componentArg, ready) {
      if (!isVisible || instance._component || isVisible(instance)) {
        // 若是有isVisible,而且傳入的實例有_component,而且isVisible返回真則進行一下代碼
        if (!instance._container) {
          // 若是傳入實例沒有_container,則爲其添加一個默認的
          instance._container = getContainer(instance);
        }
        let component;
        if (instance.getComponent) {
          // 若是傳入實例有getComponent,則將傳入的參數傳入實例的getComponent函數
          component = instance.getComponent(componentArg);
        } else {
          // 不然就進行就是用傳入參數中的getComponent方法構造一個Component
          component = getComponent(instance, componentArg);
        }
        // unstable_renderSubtreeIntoContainer是更新組件到傳入的DOM節點上
        // 可使用它完成在組件內部實現跨組件的DOM操做
        // ReactComponent unstable_renderSubtreeIntoContainer(
        // parentComponent component,
        // ReactElement element,
        // DOMElement container,
        // [function callback]
        // )
        ReactDOM.unstable_renderSubtreeIntoContainer(instance,
          component, instance._container,
          function callback() {
            instance._component = this;
            if (ready) {
              ready.call(this);
            }
          });
      }
    }

    if (autoMount) {
      mixin = {
        ...mixin,
        // 若是是自動渲染組件,那就在DidMount和DidUpdate渲染組件
        componentDidMount() {
          renderComponent(this);
        },
        componentDidUpdate() {
          renderComponent(this);
        },
      };
    }

    if (!autoMount || !autoDestroy) {
      mixin = {
        // 若是不是自動渲染的,那就在mixin中添加一個渲染函數
        ...mixin,
        renderComponent(componentArg, ready) {
          renderComponent(this, componentArg, ready);
        },
      };
    }

    function removeContainer(instance) {
      // 用於在掛載節點remove掉添加的組件
      if (instance._container) {
        const container = instance._container;
        // 先將組件unmount
        ReactDOM.unmountComponentAtNode(container);
        // 而後在刪除掛載點
        container.parentNode.removeChild(container);
        instance._container = null;
      }
    }

    if (autoDestroy) {
      // 若是是自動銷燬的,那就在WillUnmount的時候銷燬
      mixin = {
        ...mixin,
        componentWillUnmount() {
          removeContainer(this);
        },
      };
    } else {
      mixin = {
        // 若是不是自動銷燬,那就只是在mixin中添加一個銷燬的函數
        ...mixin,
        removeContainer() {
          removeContainer(this);
        },
      };
    }
    // 最後返回構建好的mixin
    return mixin;
  }複製代碼
// Portal.js
// 這個函數就像咱們剛纔上面所提到的Potal組件的一個編寫,這樣的組件很是有用
// 咱們能夠利用這個組件建立在一些咱們所須要建立組件的地方,好比在body節點建立
// 模態框,或者在窗口節點建立fixed的定位的彈出框之類的。
// 還有就是在用完這個組件也就是在componentWillUnmount的時候必定要將節點移除
  import React from 'react';
  import PropTypes from 'prop-types';
  import { createPortal } from 'react-dom';

  export default class Portal extends React.Component {
    static propTypes = {
      getContainer: PropTypes.func.isRequired,
      children: PropTypes.node.isRequired,
    }

    componentDidMount() {
      this.createContainer();
    }

    componentWillUnmount() {
      this.removeContainer();
    }

    createContainer() {
      this._container = this.props.getContainer();
      this.forceUpdate();
    }

    removeContainer() {
      if (this._container) {
        this._container.parentNode.removeChild(this._container);
      }
    }

    render() {
      if (this._container) {
        return createPortal(this.props.children, this._container);
      }
      return null;
    }
  }複製代碼

在組件開始以前

在組件開始以前還有一些輔助的東西須要瞭解到

// 函數體的默認值
  function noop() {
  }

  function returnEmptyString() {
    return '';
  }

  function returnDocument() {
    return window.document;
  }
  // 設置容許的事件,onContextMenu是右鍵菜單事件
  const ALL_HANDLERS = ['onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter',
    'onMouseLeave', 'onFocus', 'onBlur', 'onContextMenu'];
  // 判斷一下react的版本是否是react16
  const IS_REACT_16 = !!createPortal;
  // 判斷是不是手機查看,
  // Navigator 對象包含有關瀏覽器的信息。 詳情能夠看這裏http://www.w3school.com.cn/jsref/dom_obj_navigator.asp
  // 這裏判斷一下瀏覽器代理是否是移動端的代理。
  const isMobile = typeof navigator !== 'undefined' && !!navigator.userAgent.match(
    /(Android|iPhone|iPad|iPod|iOS|UCWEB)/i
  );

  const mixins = [];
  // 判斷一下,若是不是react16,就在mixin中本身添加一個相似於createPortal的函數
  if (!IS_REACT_16) {
    mixins.push(
      getContainerRenderMixin({
        autoMount: false,

        isVisible(instance) {
          return instance.state.popupVisible;
        },

        getContainer(instance) {
          return instance.getContainer();
        },
      })
    );
  }複製代碼

Props

這個組件的傳入參數很是的多,爲了作兼容或者適應更多的使用者。

propTypes: {
    children: PropTypes.any,
    // 還記得我在dropdown裏面留下的問題麼,當時我問的是爲什
    // 麼觸發能夠試試一個數組,這裏這個參數將會告訴你爲何,
    // 是可讓寫在數組中的事件都成爲其觸發的事件。
    action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
    showAction: PropTypes.any,
    hideAction: PropTypes.any,
    getPopupClassNameFromAlign: PropTypes.any,
    onPopupVisibleChange: PropTypes.func,
    afterPopupVisibleChange: PropTypes.func,
    popup: PropTypes.oneOfType([
      PropTypes.node,
      PropTypes.func,
    ]).isRequired,
    popupStyle: PropTypes.object,
    prefixCls: PropTypes.string,
    popupClassName: PropTypes.string,
    popupPlacement: PropTypes.string,
    builtinPlacements: PropTypes.object,
    popupTransitionName: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
    ]),
    popupAnimation: PropTypes.any,
    mouseEnterDelay: PropTypes.number,
    mouseLeaveDelay: PropTypes.number,
    zIndex: PropTypes.number,
    focusDelay: PropTypes.number,
    blurDelay: PropTypes.number,
    getPopupContainer: PropTypes.func,
    getDocument: PropTypes.func,
    destroyPopupOnHide: PropTypes.bool,
    mask: PropTypes.bool,
    maskClosable: PropTypes.bool,
    onPopupAlign: PropTypes.func,
    popupAlign: PropTypes.object,
    popupVisible: PropTypes.bool,
    maskTransitionName: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
    ]),
    maskAnimation: PropTypes.string,
  }複製代碼

參數不少,我直接將其參數做用拷貝過來了。

name type default description
popupClassName string additional className added to popup
destroyPopupOnHide boolean false whether destroy popup when hide
getPopupClassNameFromAlign getPopupClassNameFromAlign(align: Object):String additional className added to popup according to align
action string[] ['hover'] which actions cause popup shown. enum of 'hover','click','focus','contextMenu'
mouseEnterDelay number 0 delay time to show when mouse enter. unit: s.
mouseLeaveDelay number 0.1 delay time to hide when mouse leave. unit: s.
popupStyle Object additional style of popup
prefixCls String rc-trigger-popup prefix class name
popupTransitionName String|Object github.com/react-compo…
maskTransitionName String|Object github.com/react-compo…
onPopupVisibleChange Function call when popup visible is changed
mask boolean false whether to support mask
maskClosable boolean true whether to support click mask to hide
popupVisible boolean whether popup is visible
zIndex number popup's zIndex
defaultPopupVisible boolean whether popup is visible initially
popupAlign Object: alignConfig of dom-align popup 's align config
onPopupAlign function(popupDomNode, align) callback when popup node is aligned
popup React.Element | function() => React.Element popup content
getPopupContainer getPopupContainer(): HTMLElement function returning html node which will act as popup container
getDocument getDocument(): HTMLElement function returning document node which will be attached click event to close trigger
popupPlacement string use preset popup align config from builtinPlacements, can be merged by popupAlign prop
builtinPlacements object builtin placement align map. used by placement prop

Render()

咱們依然仍是從他的render函數做爲突破點

render() {
    const { popupVisible } = this.state;
    const props = this.props;
    const children = props.children;
    // react.children.only 是檢查是否只包含一個孩子節點,不然這個函數拋出錯誤
    const child = React.Children.only(children);
    // 這裏添加key這個屬性爲了在後面返回數組的時候可以有一個key
    const newChildProps = { key: 'trigger' };

    // 下面的全部的操做是給傳出的trigger綁定事件

    if (this.isContextMenuToShow()) {
      newChildProps.onContextMenu = this.onContextMenu;
    } else {
      newChildProps.onContextMenu = this.createTwoChains('onContextMenu');
    }

    if (this.isClickToHide() || this.isClickToShow()) {
      newChildProps.onClick = this.onClick;
      newChildProps.onMouseDown = this.onMouseDown;
      newChildProps.onTouchStart = this.onTouchStart;
    } else {
      newChildProps.onClick = this.createTwoChains('onClick');
      newChildProps.onMouseDown = this.createTwoChains('onMouseDown');
      newChildProps.onTouchStart = this.createTwoChains('onTouchStart');
    }
    if (this.isMouseEnterToShow()) {
      newChildProps.onMouseEnter = this.onMouseEnter;
    } else {
      newChildProps.onMouseEnter = this.createTwoChains('onMouseEnter');
    }
    if (this.isMouseLeaveToHide()) {
      newChildProps.onMouseLeave = this.onMouseLeave;
    } else {
      newChildProps.onMouseLeave = this.createTwoChains('onMouseLeave');
    }
    if (this.isFocusToShow() || this.isBlurToHide()) {
      newChildProps.onFocus = this.onFocus;
      newChildProps.onBlur = this.onBlur;
    } else {
      newChildProps.onFocus = this.createTwoChains('onFocus');
      newChildProps.onBlur = this.createTwoChains('onBlur');
    }
    // 利用新的props構建一個新的trigger
    const trigger = React.cloneElement(child, newChildProps);

    // 判斷是不是react16版本 不是就直接返回trigger
    if (!IS_REACT_16) {
      return trigger;
    }

    let portal;
    // prevent unmounting after it's rendered
    if (popupVisible || this._component) {
      portal = (
        <Portal key="portal" getContainer={this.getContainer} > {this.getComponent()} </Portal>
      );
    }

    return [
      trigger,
      portal,
    ];
  },複製代碼

在上面的代碼中咱們看這些函數

this.createTwoChains();

  this.isContextMenuToShow();

  this.isClickToHide();

  this.isClickToShow();

  this.isMouseEnterToShow();

  this.isMouseLeaveToHide();

  this.isFocusToShow();

  this.isBlurToHide();複製代碼

那麼咱們未來了解這些函數都幹了什麼

  • this.createTwoChains()

    // 這個函數是給trigger組件綁定對應事件
      createTwoChains(event) {
        // 獲取包裹元素的props
        const childPros = this.props.children.props;
        // 獲取當前組件的props
        const props = this.props;
        // 若是子元素有這個事件類型而且trigger組件有這個事件類型
        // 就返回trigger組件中的對應的事件觸發函數
        // 若是二者中有一方沒有的話,就返回有的那一方的事件
        if (childPros[event] && props[event]) {
          return this[`fire${event}`];
        }
        return childPros[event] || props[event];
      }複製代碼
  • 判斷事件是否須要添加

    // 這幾個函數的結構都是同樣的
      this.isContextMenuToShow();
    
      this.isClickToHide();
    
      this.isClickToShow();
    
      this.isMouseEnterToShow();
    
      this.isMouseLeaveToHide();
    
      this.isFocusToShow();
    
      this.isBlurToHide();
    
      // 這個函數是經過事件觸發action來判斷是否須要給組件綁定對應事件類型
      // 下面是僞代碼
      isSomeEventToShowOrHide() {
        // 從傳入props中的action和showAction中查詢是否有這個事件類型
        // 有就返回true,不然返回false
        const { action, showActionOrHideAction } = this.props;
        return action.indexOf(event) !== -1 || showActionOrHideAction.indexOf(event) !== -1;
      }複製代碼

生命週期

在createTwoChains函數中咱們又看見了一個新的函數'this[fire${event}]',

這些函數都是在componentDidMount的時候構建成的,那麼記下來瓜熟蒂落的咱們應該轉接到組件的生命週期

// 首先設置一個popupVisible做爲state中的一個變量,方便下面使用
  getInitialState() {
    const props = this.props;
    let popupVisible;
    if ('popupVisible' in props) {
      popupVisible = !!props.popupVisible;
    } else {
      popupVisible = !!props.defaultPopupVisible;
    }
    return {
      popupVisible,
    };
  },

  componentWillMount() {
    // 給每個事件都寫上默認事件
    ALL_HANDLERS.forEach((h) => {
      this[`fire${h}`] = (e) => {
        this.fireEvents(h, e);
      };
    });
  },

  componentDidMount() {
    // 在第一次渲染的時候強制性調用一下更新狀態
    this.componentDidUpdate({}, {
      popupVisible: this.state.popupVisible,
    });
  },

  componentWillReceiveProps({ popupVisible }) {
    if (popupVisible !== undefined) {
      this.setState({
        popupVisible,
      });
    }
  },

  componentDidUpdate(_, prevState) {
    const props = this.props;
    const state = this.state;
    const triggerAfterPopupVisibleChange = () => {
      if (prevState.popupVisible !== state.popupVisible) {
        props.afterPopupVisibleChange(state.popupVisible);
      }
    };
    if (!IS_REACT_16) {
      // 若是不是react16版本就使用mixin中的函數渲染組件,而且可以執行外部afterPopupVisibleChange函數的回調
      this.renderComponent(null, triggerAfterPopupVisibleChange);
    } else {
      // 不然直接執行回調
      triggerAfterPopupVisibleChange();
    }

    // We must listen to `mousedown`, edge case:
    // https://github.com/ant-design/ant-design/issues/5804
    // https://github.com/react-component/calendar/issues/250
    // https://github.com/react-component/trigger/issues/50
    if (state.popupVisible) {
      let currentDocument;
      if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextMenuToShow())) {
        currentDocument = props.getDocument();
        this.clickOutsideHandler = addEventListener(currentDocument,
          'mousedown', this.onDocumentClick);
      }
      // always hide on mobile
      // `isMobile` fix: mask clicked will cause below element events triggered
      // https://github.com/ant-design/ant-design-mobile/issues/1909
      // https://github.com/ant-design/ant-design-mobile/issues/1928
      if (!this.touchOutsideHandler && isMobile) {
        currentDocument = currentDocument || props.getDocument();
        this.touchOutsideHandler = addEventListener(currentDocument,
          'click', this.onDocumentClick);
      }
      // close popup when trigger type contains 'onContextMenu' and document is scrolling.
      if (!this.contextMenuOutsideHandler1 && this.isContextMenuToShow()) {
        currentDocument = currentDocument || props.getDocument();
        this.contextMenuOutsideHandler1 = addEventListener(currentDocument,
          'scroll', this.onContextMenuClose);
      }
      // close popup when trigger type contains 'onContextMenu' and window is blur.
      if (!this.contextMenuOutsideHandler2 && this.isContextMenuToShow()) {
        this.contextMenuOutsideHandler2 = addEventListener(window,
          'blur', this.onContextMenuClose);
      }
      return;
    }
    // 清除全部外部的事件,由於上面爲了解決一些issue而添加的事件
    this.clearOutsideHandler();
  },

  componentWillUnmount() {
    this.clearDelayTimer();
    this.clearOutsideHandler();
  },複製代碼

但是看到如今也仍是沒有設麼頭緒,別忙這裏先講清楚一件事情,就是trigger這個組件的實現

trigger組件因爲其中的展現內容須要絕對定位,可是這些定位若是放在已經存在的dom結構中會很複雜很難實現統一,因而這裏就將全部的須要定位的元素所有渲染在body的最後,這樣子計算定位就很方便了,因此trigger組件的目的就是須要將呈現的東西給渲染在body以後,可是你們都知道,react的render只要一個入口,也就是最初的id爲root的div,而後就是在這個div裏面進行react開發,因此react爲你們提供了一個函數,咱們在上面的renderComponent()這個函數中也講到unstable_renderSubtreeIntoContainer(),可使用這個函數就可以將組件中的內容渲染在創造出的節點上而且追加在任何地方,通常是追加在body,也能夠追加在指定的dom節點後面

接下來就是另外一個分析的思路,由於我在看這些代碼的時候開始也是混亂的,在通過查資料的過程當中我也在思考,發現到一個點那就是這個組件在判斷當前react版本是否是react16,而且根據上面所講的trigger組件的實現原理,我恍然大悟,由於在react16以前沒有createPortal這個API的,這個API其實就是trigger的原理實現,因此我就知道了,判斷若是是react16版本的就使用react本身的API來建立掛載點,若是不是就利用mixin中的renderComponent()函數中的老的react的方法unstable_renderSubtreeIntoContainer()來建立掛載點以及掛載組件,那麼接下來咱們就來分析一下他的思路。

ISREACT16?

上面既然說到了要從當前版原本進行操做,那麼我就按照是與不是分別看看這個組件都作了哪些處理

IS

首先就是從當前react版本是16開始,從render函數開始,在render函數中咱們就談到有一個判斷

if (!IS_REACT_16) {
    return trigger;
  }

  let portal;
  // prevent unmounting after it's rendered
  if (popupVisible || this._component) {
    portal = (
      <Portal key="portal" getContainer={this.getContainer} > {this.getComponent()} </Portal>
    );
  }

  return [
    trigger,
    portal,
  ];複製代碼

也就是在使用cloneElement生成完trigger組件以後,若是不是react16版本就直接返回了trigger,而後若是是react16版本就使用Protal組件將須要掛載的dom元素渲染出來,使用getContainer進行dom節點的建立,使用getComponent將彈出層渲染,最終掛載在getContainer建立的dom節點,而後append在body,這就是使用了react16版本的一個建立過程,其中Protal組件中就是用了react16中的createPortal,剩下的就是Popup,又是antd的另外一個基層組件,須要去了解。

NOT IS

若是你們和我同樣看了源碼以後也許會納悶,若是不是react16版本的時候,就直接返回了trigger組件,那麼他是在什麼時刻去渲染彈出層以及彈出層的掛載節點的呢?接下來就是揭祕時間:

componentDidUpdate(_, prevState) {
    const props = this.props;
    const state = this.state;
    const triggerAfterPopupVisibleChange = () => {
      if (prevState.popupVisible !== state.popupVisible) {
        props.afterPopupVisibleChange(state.popupVisible);
      }
    };
    if (!IS_REACT_16) {
      // 若是不是react16版本就使用mixin中的函數渲染組件,而且可以執行外部afterPopupVisibleChange函數的回調
      this.renderComponent(null, triggerAfterPopupVisibleChange);
    } else {
      // 不然直接執行回調
      triggerAfterPopupVisibleChange();
    }

    // 一些可有可無的code ...
  }複製代碼

在componentDidUpdate中在不是react16的時候使用了一個renderComponent函數,那麼這個函數又是哪裏來的呢,咱們繼續往上追溯,咱們發如今上面講到的getContainerRenderMixin中有這樣的一斷代碼

if (!autoMount || !autoDestroy) {
    mixin = {
      // 若是不是自動渲染的,那就在mixin中添加一個渲染函數
      ...mixin,
      renderComponent(componentArg, ready) {
        renderComponent(this, componentArg, ready);
      },
    };
  }複製代碼

那麼知道mixin做用的同窗就應該知道了,上面的componentDidUpdate中使用的renderComponent函數是在哪裏定義的了,接下來就直接分析這個mixin中幹了什麼

首先是咱們在使用的時候傳入了這些參數;

getContainerRenderMixin({
  autoMount: false,

  isVisible(instance) {
    return instance.state.popupVisible;
  },

  getContainer(instance) {
    return instance.getContainer();
  },
})複製代碼

這裏不得再也不講一遍這個getContainerRenderMixin

import ReactDOM from 'react-dom';

  function defaultGetContainer() {
    const container = document.createElement('div');
    document.body.appendChild(container);
    return container;
  }

  export default function getContainerRenderMixin(config) {
    // 首先傳了三個參數進來,autoMount = false, isVisible(func), getContainer(func)
    const {
      autoMount = true,
      autoDestroy = true,
      isVisible,
      getComponent,
      getContainer = defaultGetContainer,
    } = config;

    let mixin;

    function renderComponent(instance, componentArg, ready) {
      // 當外部傳入的狀態爲顯示,而且外部的實例有_component(這個_component是在傳入外部的Popup組件的ref所指向的節點)
      if (!isVisible || instance._component || isVisible(instance)) {
        if (!instance._container) {
          // trigger組件沒有_container,默認建立一個
          instance._container = getContainer(instance);
        }
        let component;
        if (instance.getComponent) {
          // 若是傳入實例有getComponent,則將傳入的參數傳入實例的getComponent函數
          component = instance.getComponent(componentArg);
        } else {
          // 不然就進行就是用傳入參數中的getComponent方法構造一個Component
          component = getComponent(instance, componentArg);
        }
        // unstable_renderSubtreeIntoContainer是更新組件到傳入的DOM節點上
        // 可使用它完成在組件內部實現跨組件的DOM操做
        // ReactComponent unstable_renderSubtreeIntoContainer(
        // parentComponent component,
        // ReactElement element,
        // DOMElement container,
        // [function callback]
        // )
        // 最終使用這個方法將彈出層掛載點以及彈出層進行渲染,而後還可以觸發一個彈出層彈出以後的回調,感受這個回調走得好繞。。。
        ReactDOM.unstable_renderSubtreeIntoContainer(instance,
          component, instance._container,
          function callback() {
            instance._component = this;
            if (ready) {
              ready.call(this);
            }
          });
      }
    }

    // trigger組件傳入的autoMount爲false因此這一段咱們不須要再看
    if (autoMount) {
      mixin = {
        ...mixin,
        // 若是是自動渲染組件,那就在DidMount和DidUpdate渲染組件
        componentDidMount() {
          renderComponent(this);
        },
        componentDidUpdate() {
          renderComponent(this);
        },
      };
    }

    // 這裏是入口,
    if (!autoMount || !autoDestroy) {
      mixin = {
        // 若是不是自動渲染的,那就在mixin中添加一個渲染函數
        ...mixin,
        renderComponent(componentArg, ready) {
          // 這裏的this也就是當前mixin插入的類,componentArg是外部傳入的null,raedy是外部傳入的callback
          // 再次回到上面的renderComponent函數
          renderComponent(this, componentArg, ready);
        },
      };
    }

    function removeContainer(instance) {
      // 用於在掛載節點remove掉添加的組件
      if (instance._container) {
        const container = instance._container;
        // 先將組件unmount
        ReactDOM.unmountComponentAtNode(container);
        // 而後在刪除掛載點
        container.parentNode.removeChild(container);
        instance._container = null;
      }
    }

    if (autoDestroy) {
      // 若是是自動銷燬的,那就在WillUnmount的時候銷燬
      mixin = {
        ...mixin,
        componentWillUnmount() {
          removeContainer(this);
        },
      };
    } else {
      mixin = {
        // 若是不是自動銷燬,那就只是在mixin中添加一個銷燬的函數
        ...mixin,
        removeContainer() {
          removeContainer(this);
        },
      };
    }
    // 最後返回構建好的mixin
    return mixin;
  }複製代碼

這樣trigger組件的一個大體構造思路以及大部分代碼就已經進行了解讀,剩餘的部分都是進行的狀態控制,antd爲了適應手機還因此在狀態控制上面寫了不少函數,很少都是簡單的函數,並且有的函數僅僅只是爲了出一些出現的issue,感受有點hotfix的意味,反正但願看完這一節對於你們製做react彈出層有必定的瞭解,這裏我就提出兩點

  1. react16版本以前的須要本身寫一個彈出層掛載點
  2. react16版本以後的可使用react提供的createPortal進行掛載點的處理

固然這個彈出層不只僅是小的彈出層,能夠製做不少東西,模態框,提醒框,下拉菜單,tooltip等等只要是須要絕對定位在某一個元素的某一個位置的場景,儘可能發揮想象吧。

相關文章
相關標籤/搜索