|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | + |
| 9 | +import { |
| 10 | + EmbeddedViewRef, |
| 11 | + IterableChangeRecord, |
| 12 | + IterableChanges, |
| 13 | + IterableDiffer, |
| 14 | + ViewContainerRef |
| 15 | +} from '@angular/core'; |
| 16 | +import { |
| 17 | + _viewRepeater, |
| 18 | + _ViewRepeaterItemChanged, |
| 19 | + _ViewRepeaterItemContext, |
| 20 | + _ViewRepeaterItemContextFactory, |
| 21 | + _ViewRepeaterItemInsertArgs, |
| 22 | + _ViewRepeaterItemValueResolver, |
| 23 | + _ViewRepeaterOperation |
| 24 | +} from './_view-repeater'; |
| 25 | + |
| 26 | + |
| 27 | +/** |
| 28 | + * A repeater that caches views when they are removed from a |
| 29 | + * {@link ViewContainerRef}. When new items are inserted into the container, |
| 30 | + * the repeater will reuse one of the cached views instead of creating a new |
| 31 | + * embedded view. Recycling cached views reduces the quantity of expensive DOM |
| 32 | + * inserts. |
| 33 | + * |
| 34 | + * @template T The type for the embedded view's $implicit property. |
| 35 | + * @template R The type for the item in each IterableDiffer change record. |
| 36 | + * @template C The type for the context passed to each embedded view. |
| 37 | + */ |
| 38 | +export class _recycleViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>> |
| 39 | + implements _viewRepeater<T, R, C> { |
| 40 | + differ: IterableDiffer<C>; |
| 41 | + |
| 42 | + /** |
| 43 | + * The size of the cache used to store unused views. |
| 44 | + * Setting the cache size to `0` will disable caching. Defaults to 20 views. |
| 45 | + */ |
| 46 | + viewCacheSize: number = 20; |
| 47 | + |
| 48 | + /** |
| 49 | + * View cache that stores embedded view instances that have been previously stamped out, |
| 50 | + * but don't are not currently rendered. The view repeater will reuse these views rather than |
| 51 | + * creating brand new ones. |
| 52 | + * |
| 53 | + * TODO(michaeljamesparsons) Investigate whether using a linked list would improve performance. |
| 54 | + */ |
| 55 | + private _viewCache: EmbeddedViewRef<C>[] = []; |
| 56 | + |
| 57 | + /** Apply changes to the DOM. */ |
| 58 | + applyChanges(changes: IterableChanges<R>, |
| 59 | + viewContainerRef: ViewContainerRef, |
| 60 | + itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>, |
| 61 | + itemValueResolver: _ViewRepeaterItemValueResolver<T, R>, |
| 62 | + itemViewChanged?: _ViewRepeaterItemChanged<R, C>) { |
| 63 | + // Rearrange the views to put them in the right location. |
| 64 | + changes.forEachOperation((record: IterableChangeRecord<R>, |
| 65 | + adjustedPreviousIndex: number | null, |
| 66 | + currentIndex: number | null) => { |
| 67 | + let view: EmbeddedViewRef<C> | undefined; |
| 68 | + let operation: _ViewRepeaterOperation; |
| 69 | + if (record.previousIndex == null) { // Item added. |
| 70 | + const viewArgsFactory = () => itemContextFactory( |
| 71 | + record, adjustedPreviousIndex, currentIndex); |
| 72 | + view = this._insertView(viewArgsFactory, currentIndex!, viewContainerRef, |
| 73 | + itemValueResolver(record)); |
| 74 | + operation = view ? _ViewRepeaterOperation.INSERTED : _ViewRepeaterOperation.REPLACED; |
| 75 | + } else if (currentIndex == null) { // Item removed. |
| 76 | + this._detachAndCacheView(adjustedPreviousIndex!, viewContainerRef); |
| 77 | + operation = _ViewRepeaterOperation.REMOVED; |
| 78 | + } else { // Item moved. |
| 79 | + view = this._moveView(adjustedPreviousIndex!, currentIndex!, viewContainerRef, |
| 80 | + itemValueResolver(record)); |
| 81 | + operation = _ViewRepeaterOperation.MOVED; |
| 82 | + } |
| 83 | + |
| 84 | + if (itemViewChanged) { |
| 85 | + itemViewChanged({ |
| 86 | + context: view?.context, |
| 87 | + operation, |
| 88 | + record, |
| 89 | + }); |
| 90 | + } |
| 91 | + }); |
| 92 | + } |
| 93 | + |
| 94 | + detach() { |
| 95 | + for (const view of this._viewCache) { |
| 96 | + view.destroy(); |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Inserts a view for a new item, either from the cache or by creating a new |
| 102 | + * one. Returns `undefined` if the item was inserted into a cached view. |
| 103 | + */ |
| 104 | + private _insertView(viewArgsFactory: () => _ViewRepeaterItemInsertArgs<C>, currentIndex: number, |
| 105 | + viewContainerRef: ViewContainerRef, |
| 106 | + value: T): EmbeddedViewRef<C> | undefined { |
| 107 | + let cachedView = this._insertViewFromCache(currentIndex!, viewContainerRef); |
| 108 | + if (cachedView) { |
| 109 | + cachedView.context.$implicit = value; |
| 110 | + return undefined; |
| 111 | + } |
| 112 | + |
| 113 | + const viewArgs = viewArgsFactory(); |
| 114 | + return viewContainerRef.createEmbeddedView( |
| 115 | + viewArgs.templateRef, viewArgs.context, viewArgs.index); |
| 116 | + } |
| 117 | + |
| 118 | + /** Detaches the view at the given index and inserts into the view cache. */ |
| 119 | + private _detachAndCacheView(index: number, viewContainerRef: ViewContainerRef) { |
| 120 | + const detachedView = this._detachView(index, viewContainerRef); |
| 121 | + this._maybeCacheView(detachedView, viewContainerRef); |
| 122 | + } |
| 123 | + |
| 124 | + /** Moves view at the previous index to the current index. */ |
| 125 | + private _moveView(adjustedPreviousIndex: number, currentIndex: number, |
| 126 | + viewContainerRef: ViewContainerRef, value: T): EmbeddedViewRef<C> { |
| 127 | + const view = viewContainerRef.get(adjustedPreviousIndex!) as |
| 128 | + EmbeddedViewRef<C>; |
| 129 | + viewContainerRef.move(view, currentIndex); |
| 130 | + view.context.$implicit = value; |
| 131 | + return view; |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * Cache the given detached view. If the cache is full, the view will be |
| 136 | + * destroyed. |
| 137 | + */ |
| 138 | + private _maybeCacheView(view: EmbeddedViewRef<C>, viewContainerRef: ViewContainerRef) { |
| 139 | + if (this._viewCache.length < this.viewCacheSize) { |
| 140 | + this._viewCache.push(view); |
| 141 | + } else { |
| 142 | + const index = viewContainerRef.indexOf(view); |
| 143 | + |
| 144 | + // The host component could remove views from the container outside of |
| 145 | + // the view repeater. It's unlikely this will occur, but just in case, |
| 146 | + // destroy the view on its own, otherwise destroy it through the |
| 147 | + // container to ensure that all the references are removed. |
| 148 | + if (index === -1) { |
| 149 | + view.destroy(); |
| 150 | + } else { |
| 151 | + viewContainerRef.remove(index); |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + /** Inserts a recycled view from the cache at the given index. */ |
| 157 | + private _insertViewFromCache(index: number, |
| 158 | + viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> | null { |
| 159 | + const cachedView = this._viewCache.pop(); |
| 160 | + if (cachedView) { |
| 161 | + viewContainerRef.insert(cachedView, index); |
| 162 | + } |
| 163 | + return cachedView || null; |
| 164 | + } |
| 165 | + |
| 166 | + /** Detaches the embedded view at the given index. */ |
| 167 | + private _detachView(index: number, viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> { |
| 168 | + return viewContainerRef.detach(index) as EmbeddedViewRef<C>; |
| 169 | + } |
| 170 | +} |
0 commit comments