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 源码解析(一)
惯例一张图,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里的OverlayModule
和PortalModule
,前者是生成一个显示层,后者则是把前者生成的显示层挂载到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
,这个是代表文字的显示方向,属性值为rtl
和ltr
;接下来是创建位置信息的属性,这个是用来进行设置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.attch
和DomPortalOutlet.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作为自己的信息源,而这个Subject
在overlay-keyboard-dispatcher.ts
文件的L80-L96发射数据,在L50进行监听。以上便是overlay
的keydownEvents
调用的过程。
我们可以发现, keydownEvents
实际上是监听了document.body
的keydown
事件,然后把这个事件转换成Observable
接着返回给我们,因为采用了事件捕获机制,所以,如果在keydownEvents
里面使用datech
则永远不会触发触发元素的keydown
事件
这个时候如果在_overlayRef的keydownEvents中调用detach方法 那么 触发元素的事件将永运无法运行