Angular Material —— MatSnackModule 源码解析(二)

惯例,以下开始正题;


在上一节我们说了DomPortalOutlet.attach方法,接下来说下overlayRef.attach;

  // 为了方便解释,这是OverlayRef的构造函数;
  constructor(
      private _portalOutlet: PortalOutlet,
      private _host: HTMLElement,
      private _pane: HTMLElement,
      private _config: ImmutableObject<OverlayConfig>,
      private _ngZone: NgZone,
      private _keyboardDispatcher: OverlayKeyboardDispatcher,
      private _document: Document,
      // @breaking-change 8.0.0 `_location` parameter to be made required.
      private _location?: Location) {
        if (_config.scrollStrategy) {
          _config.scrollStrategy.attach(this);
        }

        this._positionStrategy = _config.positionStrategy;
      }

  // 这里开始是attach方法 
  attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
  attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
  attach(portal: any): any;

  /**
   * Attaches content, given via a Portal, to the overlay.
   * If the overlay is configured to have a backdrop, it will be created.
   *
   * @param portal Portal instance to which to attach the overlay.
   * @returns The portal attachment result.
   */
  attach(portal: Portal<any>): any {
    //这里实际上就是调用了DomPortalOutlet.attach方法;
    let attachResult = this._portalOutlet.attach(portal);

    // 这里的这个我不是很理解为什么方法名为attach。。
    // 具体的在后面说(实际上这个也不是很复杂)
    // 个人认为是为了防止用户在创建overlayRef的时候没有设置好位置所做的措施
    // 当然,在当前模式下只是单纯的给该overlayRef._host添加一个'cdk-global-overlay-wrapper'的class
    // 至于为什么在当前模式下无效会在后面解释)
    // ps: 这里的当前模式,是指通过MatSnackBarModule的第一种运行方法;
    if (this._positionStrategy) {
      this._positionStrategy.attach(this);
    }

    // Update the pane element with the given configuration.
    if (!this._host.parentElement && this._previousHostParent) {
      this._previousHostParent.appendChild(this._host);
    }

    // 这个跟上面的那个具体做什么的我也不太懂,不过可以确定的是在目前模式下是没用的
    // 在后面发现了会补充上的
    this._updateStackingOrder();

    // 这俩不解释了, 跟名字一样
    this._updateElementSize();
    this._updateElementDirection();

    // 显示层是否跟着页面一起滚动
    if (this._config.scrollStrategy) {
      this._config.scrollStrategy.enable();
    }

    // Update the position once the zone is stable so that the overlay will be fully rendered
    // before attempting to position it, as the position may depend on the size of the rendered
    // content.
    this._ngZone.onStable
      .asObservable()
      .pipe(take(1))
      .subscribe(() => {
        // The overlay could've been detached before the zone has stabilized.
        if (this.hasAttached()) {
          // 更新位置,会在后面解释
          this.updatePosition();
        }
      });

    // Enable pointer events for the overlay pane element.
    this._togglePointerEvents(true);

    // 是否需要添加模态层(其实就是是否添加一个半透明的背景层)
    if (this._config.hasBackdrop) {
      // 这个后面会说
      this._attachBackdrop();
    }

    // 有需要额外添加的className话, 添加上去
    if (this._config.panelClass) {
      // 源码很简单,这里不多解释
      this._toggleClasses(this._pane, this._config.panelClass, true);
    }

    // Only emit the `attachments` event once all other setup is done.
    this._attachments.next();

    // Track this overlay by the keyboard dispatcher
    // 键盘事件监听,这个还是会放在后面说
    this._keyboardDispatcher.add(this);

    // @breaking-change 8.0.0 remove the null check for `_location`
    // once the constructor parameter is made required.
    // 下面是官方的注释
        /**
   * Whether the overlay should be disposed of when the user goes backwards/forwards in history.
   * Note that this usually doesn't include clicking on links (unless the user is using
   * the `HashLocationStrategy`).
   */
    if (this._config.disposeOnNavigation && this._location) {
      this._locationChanges = this._location.subscribe(() => this.dispose());
    }

    return attachResult;
  }

以上便是overlayRef.attach的解释了,上面一些没有说的会在文章的结尾补上,或者是在用到的时候补上;

到这里,要显示的DOM Element基本上已经全部都挂载到PortalOutlet上,且PortalOutlet也已经挂载到DOM上了,因为这个模式,是将要显示的内容挂载到Angular上下文之外的地方,所以可以看到这个DOM树是附加在document.body上,且是最后一个元素;

不过,虽然是挂载上了,但是在这个时候,数据什么的并没有进行处理。我们继续往下看;

  /**
   * Attaches the snack bar container component to the overlay.
   */
  private _attachSnackBarContainer(overlayRef: OverlayRef,
                                   config: MatSnackBarConfig): MatSnackBarContainer {
    // 如果config里的有指定viewContainerRef值的话,那么获取该视图的注入项;
    const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
    // 创建依赖注入器;
    const injector = new PortalInjector(userInjector || this._injector, new WeakMap([
      [MatSnackBarConfig, config]
    ]));
    // 创建一个内容容器(emmm,这里其实不知道该怎么说);
    // 其实大概就是这里新建了一个视图(ComponentPortal);
    // 我们之后传入的组件是附加在这个视图中的;
    const containerPortal =
        new ComponentPortal(MatSnackBarContainer, config.viewContainerRef, injector);
     // 接着把上面创建的视图附加到OverlayRef里
    const containerRef: ComponentRef<MatSnackBarContainer> = overlayRef.attach(containerPortal);
    containerRef.instance.snackBarConfig = config;
    return containerRef.instance;
  }

里面调用的方法在上面已经说了,接下来我们继续看MatSnackBar._attach剩下的内容;

/**
   * Places a new component or a template as the content of the snack bar container.
   */
  private _attach<T>(content: ComponentType<T> | TemplateRef<T>, userConfig?: MatSnackBarConfig):
    MatSnackBarRef<T | EmbeddedViewRef<any>> {

    const config = {...new MatSnackBarConfig(), ...this._defaultConfig, ...userConfig};
    const overlayRef = this._createOverlay(config);
    // 创建一个虚拟的视图容器(其实就是个ng-template);
    // 我们要显示的内容是放到这里面的;
    // 而这个,又是挂载到overlayRef上的
    const container = this._attachSnackBarContainer(overlayRef, config);
    // 创建一个新的MatSnackBarRef实例,这个会在后面讲
    const snackBarRef = new MatSnackBarRef<T | EmbeddedViewRef<any>>(container, overlayRef);

    // 类型判断;
    if (content instanceof TemplateRef) {
      // 下面会解释
      const portal = new TemplatePortal(content, null!, {
        $implicit: config.data,
        snackBarRef
      } as any);

      snackBarRef.instance = container.attachTemplatePortal(portal);
    } else {
      // 如果是个组件的话,那么创建注入器
      const injector = this._createInjector(config, snackBarRef);
      // 创建一个ComponentPortal实例
      const portal = new ComponentPortal(content, undefined, injector);
      // 添加到宿主元素中
      const contentRef = container.attachComponentPortal<T>(portal);

      // We can't pass this via the injector, because the injector is created earlier.
      snackBarRef.instance = contentRef.instance;
    }

    // Subscribe to the breakpoint observer and attach the mat-snack-bar-handset class as
    // appropriate. This class is applied to the overlay element because the overlay must expand to
    // fill the width of the screen for full width snackbars.
    // 通过cdk里的layout来监听页面的大小变化
    // 如果是手机端的话,给overlayRef添加上对应的class
    this._breakpointObserver.observe(Breakpoints.Handset).pipe(
      takeUntil(overlayRef.detachments().pipe(take(1)))
    ).subscribe(state => {
      if (state.matches) {
        overlayRef.overlayElement.classList.add('mat-snack-bar-handset');
      } else {
        overlayRef.overlayElement.classList.remove('mat-snack-bar-handset');
      }
    });

    // 开始动画
    this._animateSnackBar(snackBarRef, config);
    this._openedSnackBarRef = snackBarRef;
    return this._openedSnackBarRef;
  }

到这里,我们要显示的内容已经全部挂载到dom上了,也就是,已经正常显示在页面上了;

总结下过程,其实无论是使用哪种方法打开MatSnackBar,在内部都会调用_attach来把内容进行挂载到DOM上;

如果使用open方法的话,在里面会使用默认的组件来进行显示;

接着,在_attach里面进行创建显示层;

其实这里面稍微有点乱,通俗点说就是创建一个新的overlayRef,接着把这个overlayRef挂载(添加)到overlay-container(这个即为容器的最顶层,再上一层便是body了); 而overlayRef里面则创建了一个pane用来挂载我们要显示的组件;然后创建一个MatSnackBarRef,这个主要是用来处理事件交互的,然后传入的类型,分别调用BasePortalOutlet对应的方法,将我们要显示内容挂载到container里(这里的container是const container = this._attachSnackBarContainer(overlayRef, config);这个container,不是上面的那个,要分清楚);接下来其实就是判断分辨率来添加/删除对应的class什么的;

接下来说一下上面没有说的代码;

updatePosition这个实际上直接调用了GlobalPositionStrategy.apply;

// GlobalPositionStrategy.apply  
// 这个其实也没什么好解释的;
apply(): void {
    // Since the overlay ref applies the strategy asynchronously, it could
    // have been disposed before it ends up being applied. If that is the
    // case, we shouldn't do anything.
    if (!this._overlayRef || !this._overlayRef.hasAttached()) {
      return;
    }
    // 获取pane的style
    const styles = this._overlayRef.overlayElement.style;
    // 获取host的style
    const parentStyles = this._overlayRef.hostElement.style;
    const config = this._overlayRef.getConfig();

    styles.position = this._cssPosition;
    styles.marginLeft = config.width === '100%' ? '0' : this._leftOffset;
    styles.marginTop = config.height === '100%' ? '0' : this._topOffset;
    styles.marginBottom = this._bottomOffset;
    styles.marginRight = this._rightOffset;

    if (config.width === '100%') {
      parentStyles.justifyContent = 'flex-start';
    } else if (this._justifyContent === 'center') {
      parentStyles.justifyContent = 'center';
    } else if (this._overlayRef.getConfig().direction === 'rtl') {
      // In RTL the browser will invert `flex-start` and `flex-end` automatically, but we
      // don't want that because our positioning is explicitly `left` and `right`, hence
      // why we do another inversion to ensure that the overlay stays in the same position.
      // TODO: reconsider this if we add `start` and `end` methods.
      if (this._justifyContent === 'flex-start') {
        parentStyles.justifyContent = 'flex-end';
      } else if (this._justifyContent === 'flex-end') {
        parentStyles.justifyContent = 'flex-start';
      }
    } else {
      parentStyles.justifyContent = this._justifyContent;
    }

    parentStyles.alignItems = config.height === '100%' ? 'flex-start' : this._alignItems;
  }

_attachBackdrop:

 /** Attaches a backdrop for this overlay. */
  private _attachBackdrop() {
    const showingClass = 'cdk-overlay-backdrop-showing';

    this._backdropElement = this._document.createElement('div');
    this._backdropElement.classList.add('cdk-overlay-backdrop');

    //  如果配置对象里有需要额外添加的class, 那么添加他
    if (this._config.backdropClass) {
      this._toggleClasses(this._backdropElement, this._config.backdropClass, true);
    }

    // Insert the backdrop before the pane in the DOM order,
    // in order to handle stacked overlays properly.
    this._host.parentElement!.insertBefore(this._backdropElement, this._host);

    // Forward backdrop clicks such that the consumer of the overlay can perform whatever
    // action desired when such a click occurs (usually closing the overlay).
    this._backdropElement.addEventListener('click',
        (event: MouseEvent) => this._backdropClick.next(event));

    // Add class to fade-in the backdrop after one frame.
    if (typeof requestAnimationFrame !== 'undefined') {
      // 在Angular之外运行里面的函数,为了防止触发Angular的变更检测
      this._ngZone.runOutsideAngular(() => {
        requestAnimationFrame(() => {
          if (this._backdropElement) {
            this._backdropElement.classList.add(showingClass);
          }
        });
      });
    } else {
      this._backdropElement.classList.add(showingClass);
    }
  }

TemplatePortal:

// 这个其实没啥好说的
export class TemplatePortal<C = any> extends Portal<C> {
  /** The embedded template that will be used to instantiate an embedded View in the host. */
  templateRef: TemplateRef<C>;

  /** Reference to the ViewContainer into which the template will be stamped out. */
  viewContainerRef: ViewContainerRef;

  /** Contextual data to be passed in to the embedded view. */
  context: C | undefined;

  constructor(template: TemplateRef<C>, viewContainerRef: ViewContainerRef, context?: C) {
    super();
    this.templateRef = template;
    this.viewContainerRef = viewContainerRef;
    this.context = context;
  }

  get origin(): ElementRef {
    return this.templateRef.elementRef;
  }

  /**
   * Attach the the portal to the provided `PortalOutlet`.
   * When a context is provided it will override the `context` property of the `TemplatePortal`
   * instance.
   */
  attach(host: PortalOutlet, context: C | undefined = this.context): C {
    this.context = context;
    return super.attach(host);
  }

  detach(): void {
    this.context = undefined;
    return super.detach();
  }
}

对应的compoentPortal其实也差不多,最主要还是所继承的Portal的:

export abstract class Portal<T> {
  private _attachedHost: PortalOutlet | null;

  /** Attach this portal to a host. */
  attach(host: PortalOutlet): T {
    if (host == null) {
      throwNullPortalOutletError();
    }

    if (host.hasAttached()) {
      throwPortalAlreadyAttachedError();
    }

    // 保存宿主元素,然后把自己挂载到宿主元素中国
    this._attachedHost = host;
    return <T> host.attach(this);
  }

  /** Detach this portal from its host */
  detach(): void {
    let host = this._attachedHost;

    if (host == null) {
      throwNoPortalAttachedError();
    } else {
      // 设置宿主元素为空,然后分离开来
      this._attachedHost = null;
      host.detach();
    }
  }

  /** Whether this portal is attached to a host. */
  get isAttached(): boolean {
    return this._attachedHost != null;
  }

  /**
   * Sets the PortalOutlet reference without performing `attach()`. This is used directly by
   * the PortalOutlet when it is performing an `attach()` or `detach()`.
   */
  setAttachedHost(host: PortalOutlet | null) {
    this._attachedHost = host;
  }
}

剩下的内容,在之后再写吧,其实怎么显示的基本上已经说完了,只剩下事件以及怎么取消显示没有说了

Angular Material —— MatSnackModule 源码解析(一)

yande.re 309004

惯例一张图,Angular Material版本为7.0.2,之后的文章版本默认为这个,以下开始正题。


首先,在官网上可以看到该模块有两种使用方式:

 // Simple message.
 let snackBarRef = snackBar.open('Message archived');

 // Simple message with an action.
 let snackBarRef = snackBar.open('Message archived', 'Undo');

 // Load the given component into the snack-bar.
 let snackBarRef = snackbar.openFromComponent(MessageArchivedComponent);

第二行跟第五行实际上是一样的,为第一种使用方式,第八行为第二种使用方式。

我们先来看第一种使用方式:

    /**
     * Opens a snackbar with a message and an optional action.
     * @param message The message to show in the snackbar.
     * @param action The label for the snackbar action.
     * @param config Additional configuration options for the snackbar.
     */
    open(message: string, action: string = '', config?: MatSnackBarConfig):
        MatSnackBarRef<SimpleSnackBar {
      const _config = {...this._defaultConfig, ...config};

      // Since the user doesn't have access to the component, we can
      // override the data to pass in our own message and action.
      _config.data = {message, action};

      if (!_config.announcementMessage) {
        _config.announcementMessage = message;
      }

      return this.openFromComponent(SimpleSnackBar, _config);

  }

我们实际上调用的是该方法,我们可以发现,实际上还有第三个参数可以传入,该参数是该SnackBar的一些配置,具体如下

 /** Injection token that can be used to access the data that was passed in to a snack bar. */
 export const MAT_SNACK_BAR_DATA = new InjectionToken<any('MatSnackBarData');

 /** Possible values for horizontalPosition on MatSnackBarConfig. */
 export type MatSnackBarHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right';

 /** Possible values for verticalPosition on MatSnackBarConfig. */
 export type MatSnackBarVerticalPosition = 'top' | 'bottom';

 /**
  * Configuration used when opening a snack-bar.
  */
 export class MatSnackBarConfig<D = any {
   /** The politeness level for the MatAriaLiveAnnouncer announcement. */
   politeness?: AriaLivePoliteness = 'assertive';

   /**
    * Message to be announced by the LiveAnnouncer. When opening a snackbar without a custom
    * component or template, the announcement message will default to the specified message.
    */
   announcementMessage?: string = '';

   /** The view container to place the overlay for the snack bar into. */
   viewContainerRef?: ViewContainerRef;

   /** The length of time in milliseconds to wait before automatically dismissing the snack bar. */
   duration?: number = 0;

   /** Extra CSS classes to be added to the snack bar container. */
   panelClass?: string | string[];

   /** Text layout direction for the snack bar. */
   direction?: Direction;

   /** Data being injected into the child component. */
   data?: D | null = null;

   /** The horizontal position to place the snack bar. */
   horizontalPosition?: MatSnackBarHorizontalPosition = 'center';

   /** The vertical position to place the snack bar. */
   verticalPosition?: MatSnackBarVerticalPosition = 'bottom';
 }

回到正题,首先,合并配置参数

 const _config = {...this._defaultConfig, ...config};

因为使用的是MatSnackModule自己的组件,所以,会将用户在config里传入的data属性进行覆盖

  _config.data = {message, action};

第一个参数为显示的内容, 第二个则是按钮的值。 接着判断config里的anniuncementMessage是否为空,为空的话,则把message的值赋给它;

 if (!_config.announcementMessage) {
      _config.announcementMessage = message;
 }

最后,在内部使用openFromCompoent方法打开自带的组件;

 return this.openFromComponent(SimpleSnackBar, _config);

以上,便是open方法的简单说明,实际上,只是将我们传入的数据进行转换,最后使用openFromCompoent调用自带的组件进行显示;

openFromCompoent这个方法实际上也很简单,调用了内部的私有方法_attach

   /**
    * Creates and dispatches a snack bar with a custom component for the content, removing any
    * currently opened snack bars.
    *
    * @param component Component to be instantiated.
    * @param config Extra configuration for the snack bar.
    */
   openFromComponent<T(component: ComponentType<T, config?: MatSnackBarConfig):
     MatSnackBarRef<T {
     return this._attach(component, config) as MatSnackBarRef<T;
   }

因此,无论我们是使用哪一种方法来显示,实际上最终都是在内部调用_attch方法; 在这里补充说明一下,MatSnackModule实际上是调用了CDK里的OverlayModulePortalModule,前者是生成一个显示层,后者则是把前者生成的显示层挂载到DOM里;

 /**
   * Places a new component or a template as the content of the snack bar container.
   */
  private _attach<T(content: ComponentType<T | TemplateRef<T, userConfig?: MatSnackBarConfig):
    MatSnackBarRef<T | EmbeddedViewRef<any {

    const config = {...new MatSnackBarConfig(), ...this._defaultConfig, ...userConfig};
    const overlayRef = this._createOverlay(config);
    const container = this._attachSnackBarContainer(overlayRef, config);
    const snackBarRef = new MatSnackBarRef<T | EmbeddedViewRef<any(container, overlayRef);

    if (content instanceof TemplateRef) {
      const portal = new TemplatePortal(content, null!, {
        $implicit: config.data,
        snackBarRef
      } as any);

      snackBarRef.instance = container.attachTemplatePortal(portal);
    } else {
      const injector = this._createInjector(config, snackBarRef);
      const portal = new ComponentPortal(content, undefined, injector);
      const contentRef = container.attachComponentPortal<T(portal);

      // We can't pass this via the injector, because the injector is created earlier.
      snackBarRef.instance = contentRef.instance;
    }

    // Subscribe to the breakpoint observer and attach the mat-snack-bar-handset class as
    // appropriate. This class is applied to the overlay element because the overlay must expand to
    // fill the width of the screen for full width snackbars.
    this._breakpointObserver.observe(Breakpoints.Handset).pipe(
      takeUntil(overlayRef.detachments().pipe(take(1)))
    ).subscribe(state = {
      if (state.matches) {
        overlayRef.overlayElement.classList.add('mat-snack-bar-handset');
      } else {
        overlayRef.overlayElement.classList.remove('mat-snack-bar-handset');
      }
    });

    this._animateSnackBar(snackBarRef, config);
    this._openedSnackBarRef = snackBarRef;
    return this._openedSnackBarRef;
  }

以上是_attch的代码,下面开始解释。 首先从传入的第一个参数上来看,可以知道支持传入ComponentType或者TemplateRef;因此,除了最上面的三种官网所说的三种使用方法外,还可以使用openFromTemplateRef来进行模板的显示;

  openFromTemplate(template: TemplateRef<any, config?: MatSnackBarConfig):
    MatSnackBarRef<EmbeddedViewRef<any {
    return this._attach(template, config);
  }

回到正题,首先,合并config;接着,调用_createOverlay来创建显示层;

 /**
   * Creates a new overlay and places it in the correct location.
   * @param config The user-specified snack bar config.
   */
  private _createOverlay(config: MatSnackBarConfig): OverlayRef {
    const overlayConfig = new OverlayConfig();
    overlayConfig.direction = config.direction;

    let positionStrategy = this._overlay.position().global();
    // Set horizontal position.
    const isRtl = config.direction === 'rtl';
    const isLeft = (
      config.horizontalPosition === 'left' ||
      (config.horizontalPosition === 'start' && !isRtl) ||
      (config.horizontalPosition === 'end' && isRtl));
    const isRight = !isLeft && config.horizontalPosition !== 'center';
    if (isLeft) {
      positionStrategy.left('0');
    } else if (isRight) {
      positionStrategy.right('0');
    } else {
      positionStrategy.centerHorizontally();
    }
    // Set horizontal position.
    if (config.verticalPosition === 'top') {
      positionStrategy.top('0');
    } else {
      positionStrategy.bottom('0');
    }

    overlayConfig.positionStrategy = positionStrategy;
    return this._overlay.create(overlayConfig);
  }

这里首先是创建一个新的overlay的默认配置对象;接着根据传入的config来进行合并,首先是直接覆盖direction,这个是代表文字的显示方向,属性值为rtlltr;接下来是创建位置信息的属性,这个是用来进行设置SnackBar显示的显示位置;接下来则是根据传入的config来判断snackBar是显示在左边还是右边还是中间;接着进行判断是显示在页面的顶部还是页面的底部,最后,把这个位置的配置赋给overlayConfig.positionStrategy,最后根据这个配置信息创建一个OverlayRef;

 /**
   * Creates an overlay.
   * @param config Configuration applied to the overlay.
   * @returns Reference to the created overlay.
   */
  create(config?: OverlayConfig): OverlayRef {
    // 创建HostElement;
    // _createHostElement里面通过依赖注入获取对应的document;
    // 接着创建一个全新的div元素;
    // 然后通过 this._overlayContainer.getContainerElement().appendChild(host);
    // 获取对应的父级元素,接着把上面的div添加到里面去
    // 而 this._overlayContainer.getContainerElement()这个也很简单
    // 首先判断内部的_containerElement是否存在,如果存在的话则直接返回,
    // 不存在的话则调用内部的_createContainer方法;
    // 这个方法也很简单,直接通过注入的document来创建一个div,然后给这个div添加‘cdk-overlay-container’这个class名,最后添加到document.body里面,接着把这个div赋给_containerElement;
    const host = this._createHostElement();
    // 这个也很简单,在内部创建一个div,然后将该div的id设置为`cdk-overlay-${nextUniqueId++}`;
    // nextUniqueId默认是0;这个是为了解决多个overlay的情况下的问题;
    // 然后,给这个div添加'cdk-overlay-pane'这个class,最后,把这个div添加到host(即传入的值)里    
    const pane = this._createPaneElement(host);
    // 在内部,首先判断_appRef是否存在,不存在的话通过注入器获取‘ApplicationRef’;
    // 接着创建一个DomPortalOutlet的实例;
    // DomPortalOutlet是为了能在Angular上下文之外挂载该视图(附加dom)
    // 这里还是推介大家去看一下这个大大写的帖子(https://juejin.im/post/5b0d119a518825158e173f97)
    const portalOutlet = this._createPortalOutlet(pane);
    // 配置对象
    const overlayConfig = new OverlayConfig(config);

    overlayConfig.direction = overlayConfig.direction || this._directionality.value;
    return new OverlayRef(portalOutlet, host, pane, overlayConfig, this._ngZone,
      this._keyboardDispatcher, this._document, this._location);
  }

上面是this._overlay.create的解析;

接下来说下OverlayRef.attchDomPortalOutlet.attch方法,下面会用到;

首先是DomPortalOutlet.attch;

export class DomPortalOutlet extends BasePortalOutlet {
 constructor(
      /** Element into which the content is projected. */
      public outletElement: Element,
      private _componentFactoryResolver: ComponentFactoryResolver,
      private _appRef: ApplicationRef,
      private _defaultInjector: Injector) {
    super();
  }
  // ………… 其他的方法
 }
 // DomPortalOutlet.attch实际上是调用了BasePortalOutlet.attch,下面是BasePortalOutlet.attch的代码;
      attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
  attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
  attach(portal: any): any;

  /** Attaches a portal. */
  attach(portal: Portal<any>): any {
    if (!portal) {
      throwNullPortalError();
    }
    // this.hasAttached()源码如下,很简单,不做解释
    //  hasAttached(): boolean {
    //      return !!this._attachedPortal;
    //  }
    // 是否已经有了添加的Portal;
    if (this.hasAttached()) {
      throwPortalAlreadyAttachedError();
    }
    // 自身是否被销毁了;
    if (this._isDisposed) {
      throwPortalOutletAlreadyDisposedError();
    }
    // 根据对应的类型,把portal附加到视图(PortalOutlet)中;
    if (portal instanceof ComponentPortal) {
      this._attachedPortal = portal;
      return this.attachComponentPortal(portal);
    } else if (portal instanceof TemplatePortal) {
      this._attachedPortal = portal;
      return this.attachTemplatePortal(portal);
    }

    throwUnknownPortalTypeError();
  }
  // 而对应的attachComponentPortal以及attachTemplatePortal都是抽象方法;

  // abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;

  // abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;

  // 以下 是DomPortalOutlet里的实现方法;
  /**
   * Attach the given ComponentPortal to DOM element using the ComponentFactoryResolver.
   * @param portal Portal to be attached
   * @returns Reference to the created component.
   */
  attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
    // 优先使用自定义的componentFactoryResolver,如果没有的话使用默认的;
    const resolver = portal.componentFactoryResolver || this._componentFactoryResolver;
    // 解析出对应的component
    const componentFactory = resolver.resolveComponentFactory(portal.component);
    let componentRef: ComponentRef<T>;

    // If the portal specifies a ViewContainerRef, we will use that as the attachment point
    // for the component (in terms of Angular's component tree, not rendering).
    // When the ViewContainerRef is missing, we use the factory to create the component directly
    // and then manually attach the view to the application.
    // 如果指定了viewContainerRef(即附加到的视图容器),那么将其附加到上去
    if (portal.viewContainerRef) {
      componentRef = portal.viewContainerRef.createComponent(
          componentFactory,
          portal.viewContainerRef.length,
          portal.injector || portal.viewContainerRef.injector);
      // 销毁时的处理函数
      this.setDisposeFn(() => componentRef.destroy());
    } else {
      // 如果没有指定ViewContainerRef的话,那么首先创建组件视图
      componentRef = componentFactory.create(portal.injector || this._defaultInjector);
      // 把componmentRef的挂载到视图树上;
      // ps:就是挂载到angular的根视图上;
      this._appRef.attachView(componentRef.hostView);
      // 销毁时的处理函数
      // 因为挂载到了视图树上,所以销毁是除了销毁组件视图外,还需要从组件树中把
      this.setDisposeFn(() => {
        this._appRef.detachView(componentRef.hostView);
        componentRef.destroy();
      });
    }
    // At this point the component has been instantiated, so we move it to the location in the DOM
    // where we want it to be rendered.
    // 把组件渲染出来(即添加到DOM里)
    this.outletElement.appendChild(this._getComponentRootNode(componentRef));

    return componentRef;
  }

  /**
   * Attaches a template portal to the DOM as an embedded view.
   * @param portal Portal to be attached.
   * @returns Reference to the created embedded view.
   */
  attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
    // 创建视图容器
    let viewContainer = portal.viewContainerRef;
    let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);
    // 手动强制进行变更检测
    viewRef.detectChanges();

    // The method `createEmbeddedView` will add the view as a child of the viewContainer.
    // But for the DomPortalOutlet the view can be added everywhere in the DOM
    // (e.g Overlay Container) To move the view to the specified host element. We just
    // re-append the existing root nodes.
    // 添加到dom里
    viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));

    this.setDisposeFn((() => {
      let index = viewContainer.indexOf(viewRef);
      if (index !== -1) {
        viewContainer.remove(index);
      }
    }));

    // TODO(jelbourn): Return locals from view.
    return viewRef;
  }

这次先写到到这里,剩下的有空再写;

  • 组件通信父组件获取子组件传递的数据以及获取event对线时,必须使用$event作为参数
<child-component (customEvent)="eventHandle($event)" </child-component>

<button (click)="eventHandle($event)" >Btn</button>

  • 解决service worker 无法工作在Chromium < 60 的版本中(因为一个bug导致无法正常工作)
ngsw-worker.js:1989 Uncaught (in promise) DOMException: Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished.
    at Driver.handleFetch (https://gsc.gamebean.net/v3/ngsw-worker.js:1989:15)
    at <anonymous>

解决方法为在sw脚本中加一个polyfill


  • Material OverlayRef的keydownEvents()会优先于触发元素的keydown事件触发,从而导致时间流无法按期运行
_overlayRef.keydownEvents().subscribe(()=>console.log('b'))

// 触发元素
<a (keydown)="testKeydownEvent()" href="" tabindex>Test</a>

testKeydownEvent(){
  console.log('a');
}

// 输出结果为 b,a  
// 可以使用delay操作符来延迟调用keydownEvents的事件

源码解析

首先 在 overlay-ref.ts#L34-L42 里进行声明,我们调用的时候返回这个 Observable,这个observable实际上调用了一个Subject作为自己的信息源,而这个Subjectoverlay-keyboard-dispatcher.ts文件的L80-L96发射数据,在L50进行监听。以上便是overlaykeydownEvents调用的过程。

我们可以发现, keydownEvents实际上是监听了document.bodykeydown事件,然后把这个事件转换成Observable接着返回给我们,因为采用了事件捕获机制,所以,如果在keydownEvents里面使用datech则永远不会触发触发元素的keydown事件 这个时候如果在_overlayRef的keydownEvents中调用detach方法 那么 触发元素的事件将永运无法运行