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

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;
  }
}

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

阅读:
247
评论:
0
转载:
0
时间:
2018-11-2 17:11

×你还没有登录,登陆后有更多特权喔!点我登录