svelte 5.45.4 → 5.45.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /** @import { EachItem, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
1
+ /** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
2
2
  /** @import { Batch } from '../../reactivity/batch.js'; */
3
3
  import {
4
4
  EACH_INDEX_REACTIVE,
@@ -29,20 +29,22 @@ import {
29
29
  block,
30
30
  branch,
31
31
  destroy_effect,
32
- run_out_transitions,
33
- pause_children,
34
32
  pause_effect,
35
33
  resume_effect
36
34
  } from '../../reactivity/effects.js';
37
35
  import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
38
36
  import { array_from, is_array } from '../../../shared/utils.js';
39
- import { COMMENT_NODE, INERT } from '#client/constants';
37
+ import { COMMENT_NODE, EFFECT_OFFSCREEN, INERT } from '#client/constants';
40
38
  import { queue_micro_task } from '../task.js';
41
39
  import { get } from '../../runtime.js';
42
40
  import { DEV } from 'esm-env';
43
41
  import { derived_safe_equal } from '../../reactivity/deriveds.js';
44
42
  import { current_batch } from '../../reactivity/batch.js';
45
43
 
44
+ // When making substantive changes to this file, validate them with the each block stress test:
45
+ // https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b
46
+ // This test also exists in this repo, as `packages/svelte/tests/manual/each-stress-test`
47
+
46
48
  /**
47
49
  * @param {any} _
48
50
  * @param {number} i
@@ -55,7 +57,7 @@ export function index(_, i) {
55
57
  * Pause multiple effects simultaneously, and coordinate their
56
58
  * subsequent destruction. Used in each blocks
57
59
  * @param {EachState} state
58
- * @param {EachItem[]} to_destroy
60
+ * @param {Effect[]} to_destroy
59
61
  * @param {null | Node} controlled_anchor
60
62
  */
61
63
  function pause_effects(state, to_destroy, controlled_anchor) {
@@ -63,19 +65,44 @@ function pause_effects(state, to_destroy, controlled_anchor) {
63
65
  var transitions = [];
64
66
  var length = to_destroy.length;
65
67
 
68
+ /** @type {EachOutroGroup} */
69
+ var group;
70
+ var remaining = to_destroy.length;
71
+
66
72
  for (var i = 0; i < length; i++) {
67
- pause_children(to_destroy[i].e, transitions, true);
73
+ let effect = to_destroy[i];
74
+
75
+ pause_effect(
76
+ effect,
77
+ () => {
78
+ if (group) {
79
+ group.pending.delete(effect);
80
+ group.done.add(effect);
81
+
82
+ if (group.pending.size === 0) {
83
+ var groups = /** @type {Set<EachOutroGroup>} */ (state.outrogroups);
84
+
85
+ destroy_effects(array_from(group.done));
86
+ groups.delete(group);
87
+
88
+ if (groups.size === 0) {
89
+ state.outrogroups = null;
90
+ }
91
+ }
92
+ } else {
93
+ remaining -= 1;
94
+ }
95
+ },
96
+ false
97
+ );
68
98
  }
69
99
 
70
- run_out_transitions(transitions, () => {
100
+ if (remaining === 0) {
71
101
  // If we're in a controlled each block (i.e. the block is the only child of an
72
102
  // element), and we are removing all items, _and_ there are no out transitions,
73
103
  // we can use the fast path — emptying the element and replacing the anchor
74
104
  var fast_path = transitions.length === 0 && controlled_anchor !== null;
75
105
 
76
- // TODO only destroy effects if no pending batch needs them. otherwise,
77
- // just set `item.o` back to `false`
78
-
79
106
  if (fast_path) {
80
107
  var anchor = /** @type {Element} */ (controlled_anchor);
81
108
  var parent_node = /** @type {Element} */ (anchor.parentNode);
@@ -84,26 +111,34 @@ function pause_effects(state, to_destroy, controlled_anchor) {
84
111
  parent_node.append(anchor);
85
112
 
86
113
  state.items.clear();
87
- link(state, to_destroy[0].prev, to_destroy[length - 1].next);
88
114
  }
89
115
 
90
- for (var i = 0; i < length; i++) {
91
- var item = to_destroy[i];
92
-
93
- if (!fast_path) {
94
- state.items.delete(item.k);
95
- link(state, item.prev, item.next);
96
- }
116
+ destroy_effects(to_destroy, !fast_path);
117
+ } else {
118
+ group = {
119
+ pending: new Set(to_destroy),
120
+ done: new Set()
121
+ };
97
122
 
98
- destroy_effect(item.e, !fast_path);
99
- }
123
+ (state.outrogroups ??= new Set()).add(group);
124
+ }
125
+ }
100
126
 
101
- if (state.first === to_destroy[0]) {
102
- state.first = to_destroy[0].prev;
103
- }
104
- });
127
+ /**
128
+ * @param {Effect[]} to_destroy
129
+ * @param {boolean} remove_dom
130
+ */
131
+ function destroy_effects(to_destroy, remove_dom = true) {
132
+ // TODO only destroy effects if no pending batch needs them. otherwise,
133
+ // just re-add the `EFFECT_OFFSCREEN` flag
134
+ for (var i = 0; i < to_destroy.length; i++) {
135
+ destroy_effect(to_destroy[i], remove_dom);
136
+ }
105
137
  }
106
138
 
139
+ /** @type {TemplateNode} */
140
+ var offscreen_anchor;
141
+
107
142
  /**
108
143
  * @template V
109
144
  * @param {Element | Comment} node The next sibling node, or the parent node if this is a 'controlled' block
@@ -120,18 +155,13 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
120
155
  /** @type {Map<any, EachItem>} */
121
156
  var items = new Map();
122
157
 
123
- /** @type {EachItem | null} */
124
- var first = null;
125
-
126
158
  var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
127
- var is_reactive_value = (flags & EACH_ITEM_REACTIVE) !== 0;
128
- var is_reactive_index = (flags & EACH_INDEX_REACTIVE) !== 0;
129
159
 
130
160
  if (is_controlled) {
131
161
  var parent_node = /** @type {Element} */ (node);
132
162
 
133
163
  anchor = hydrating
134
- ? set_hydrate_node(/** @type {Comment | Text} */ (get_first_child(parent_node)))
164
+ ? set_hydrate_node(get_first_child(parent_node))
135
165
  : parent_node.appendChild(create_text());
136
166
  }
137
167
 
@@ -139,7 +169,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
139
169
  hydrate_next();
140
170
  }
141
171
 
142
- /** @type {{ fragment: DocumentFragment | null, effect: Effect } | null} */
172
+ /** @type {Effect | null} */
143
173
  var fallback = null;
144
174
 
145
175
  // TODO: ideally we could use derived for runes mode but because of the ability
@@ -157,20 +187,19 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
157
187
  var first_run = true;
158
188
 
159
189
  function commit() {
190
+ state.fallback = fallback;
160
191
  reconcile(state, array, anchor, flags, get_key);
161
192
 
162
193
  if (fallback !== null) {
163
194
  if (array.length === 0) {
164
- if (fallback.fragment) {
165
- anchor.before(fallback.fragment);
166
- fallback.fragment = null;
195
+ if ((fallback.f & EFFECT_OFFSCREEN) === 0) {
196
+ resume_effect(fallback);
167
197
  } else {
168
- resume_effect(fallback.effect);
198
+ fallback.f ^= EFFECT_OFFSCREEN;
199
+ move(fallback, null, anchor);
169
200
  }
170
-
171
- effect.first = fallback.effect;
172
201
  } else {
173
- pause_effect(fallback.effect, () => {
202
+ pause_effect(fallback, () => {
174
203
  // TODO only null out if no pending batch needs it,
175
204
  // otherwise re-add `fallback.fragment` and move the
176
205
  // effect into it
@@ -202,10 +231,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
202
231
 
203
232
  var keys = new Set();
204
233
  var batch = /** @type {Batch} */ (current_batch);
205
- var prev = null;
206
234
  var defer = should_defer_append();
207
235
 
208
- for (var i = 0; i < length; i += 1) {
236
+ for (var index = 0; index < length; index += 1) {
209
237
  if (
210
238
  hydrating &&
211
239
  hydrate_node.nodeType === COMMENT_NODE &&
@@ -218,46 +246,33 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
218
246
  set_hydrating(false);
219
247
  }
220
248
 
221
- var value = array[i];
222
- var key = get_key(value, i);
249
+ var value = array[index];
250
+ var key = get_key(value, index);
223
251
 
224
252
  var item = first_run ? null : items.get(key);
225
253
 
226
254
  if (item) {
227
255
  // update before reconciliation, to trigger any async updates
228
- if (is_reactive_value) {
229
- internal_set(item.v, value);
230
- }
231
-
232
- if (is_reactive_index) {
233
- internal_set(/** @type {Value<number>} */ (item.i), i);
234
- }
256
+ if (item.v) internal_set(item.v, value);
257
+ if (item.i) internal_set(item.i, index);
235
258
 
236
259
  if (defer) {
237
260
  batch.skipped_effects.delete(item.e);
238
261
  }
239
262
  } else {
240
263
  item = create_item(
241
- first_run ? anchor : null,
242
- prev,
264
+ items,
265
+ first_run ? anchor : (offscreen_anchor ??= create_text()),
243
266
  value,
244
267
  key,
245
- i,
268
+ index,
246
269
  render_fn,
247
270
  flags,
248
271
  get_collection
249
272
  );
250
273
 
251
- if (first_run) {
252
- item.o = true;
253
-
254
- if (prev === null) {
255
- first = item;
256
- } else {
257
- prev.next = item;
258
- }
259
-
260
- prev = item;
274
+ if (!first_run) {
275
+ item.e.f |= EFFECT_OFFSCREEN;
261
276
  }
262
277
 
263
278
  items.set(key, item);
@@ -268,19 +283,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
268
283
 
269
284
  if (length === 0 && fallback_fn && !fallback) {
270
285
  if (first_run) {
271
- fallback = {
272
- fragment: null,
273
- effect: branch(() => fallback_fn(anchor))
274
- };
286
+ fallback = branch(() => fallback_fn(anchor));
275
287
  } else {
276
- var fragment = document.createDocumentFragment();
277
- var target = create_text();
278
- fragment.append(target);
279
-
280
- fallback = {
281
- fragment,
282
- effect: branch(() => fallback_fn(target))
283
- };
288
+ fallback = branch(() => fallback_fn((offscreen_anchor ??= create_text())));
289
+ fallback.f |= EFFECT_OFFSCREEN;
284
290
  }
285
291
  }
286
292
 
@@ -321,7 +327,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
321
327
  });
322
328
 
323
329
  /** @type {EachState} */
324
- var state = { effect, flags, items, first };
330
+ var state = { effect, flags, items, outrogroups: null, fallback };
325
331
 
326
332
  first_run = false;
327
333
 
@@ -345,21 +351,21 @@ function reconcile(state, array, anchor, flags, get_key) {
345
351
 
346
352
  var length = array.length;
347
353
  var items = state.items;
348
- var current = state.first;
354
+ var current = state.effect.first;
349
355
 
350
- /** @type {undefined | Set<EachItem>} */
356
+ /** @type {undefined | Set<Effect>} */
351
357
  var seen;
352
358
 
353
- /** @type {EachItem | null} */
359
+ /** @type {Effect | null} */
354
360
  var prev = null;
355
361
 
356
- /** @type {undefined | Set<EachItem>} */
362
+ /** @type {undefined | Set<Effect>} */
357
363
  var to_animate;
358
364
 
359
- /** @type {EachItem[]} */
365
+ /** @type {Effect[]} */
360
366
  var matched = [];
361
367
 
362
- /** @type {EachItem[]} */
368
+ /** @type {Effect[]} */
363
369
  var stashed = [];
364
370
 
365
371
  /** @type {V} */
@@ -368,8 +374,8 @@ function reconcile(state, array, anchor, flags, get_key) {
368
374
  /** @type {any} */
369
375
  var key;
370
376
 
371
- /** @type {EachItem | undefined} */
372
- var item;
377
+ /** @type {Effect | undefined} */
378
+ var effect;
373
379
 
374
380
  /** @type {number} */
375
381
  var i;
@@ -378,13 +384,13 @@ function reconcile(state, array, anchor, flags, get_key) {
378
384
  for (i = 0; i < length; i += 1) {
379
385
  value = array[i];
380
386
  key = get_key(value, i);
381
- item = /** @type {EachItem} */ (items.get(key));
387
+ effect = /** @type {EachItem} */ (items.get(key)).e;
382
388
 
383
389
  // offscreen == coming in now, no animation in that case,
384
390
  // else this would happen https://github.com/sveltejs/svelte/issues/17181
385
- if (item.o) {
386
- item.e.nodes?.a?.measure();
387
- (to_animate ??= new Set()).add(item);
391
+ if ((effect.f & EFFECT_OFFSCREEN) === 0) {
392
+ effect.nodes?.a?.measure();
393
+ (to_animate ??= new Set()).add(effect);
388
394
  }
389
395
  }
390
396
  }
@@ -393,38 +399,53 @@ function reconcile(state, array, anchor, flags, get_key) {
393
399
  value = array[i];
394
400
  key = get_key(value, i);
395
401
 
396
- item = /** @type {EachItem} */ (items.get(key));
402
+ effect = /** @type {EachItem} */ (items.get(key)).e;
397
403
 
398
- state.first ??= item;
404
+ if (state.outrogroups !== null) {
405
+ for (const group of state.outrogroups) {
406
+ group.pending.delete(effect);
407
+ group.done.delete(effect);
408
+ }
409
+ }
399
410
 
400
- if (!item.o) {
401
- item.o = true;
411
+ if ((effect.f & EFFECT_OFFSCREEN) !== 0) {
412
+ effect.f ^= EFFECT_OFFSCREEN;
402
413
 
403
- var next = prev ? prev.next : current;
414
+ if (effect === current) {
415
+ move(effect, null, anchor);
416
+ } else {
417
+ var next = prev ? prev.next : current;
404
418
 
405
- link(state, prev, item);
406
- link(state, item, next);
419
+ if (effect === state.effect.last) {
420
+ state.effect.last = effect.prev;
421
+ }
407
422
 
408
- move(item, next, anchor);
409
- prev = item;
423
+ if (effect.prev) effect.prev.next = effect.next;
424
+ if (effect.next) effect.next.prev = effect.prev;
425
+ link(state, prev, effect);
426
+ link(state, effect, next);
410
427
 
411
- matched = [];
412
- stashed = [];
428
+ move(effect, next, anchor);
429
+ prev = effect;
430
+
431
+ matched = [];
432
+ stashed = [];
413
433
 
414
- current = prev.next;
415
- continue;
434
+ current = prev.next;
435
+ continue;
436
+ }
416
437
  }
417
438
 
418
- if ((item.e.f & INERT) !== 0) {
419
- resume_effect(item.e);
439
+ if ((effect.f & INERT) !== 0) {
440
+ resume_effect(effect);
420
441
  if (is_animated) {
421
- item.e.nodes?.a?.unfix();
422
- (to_animate ??= new Set()).delete(item);
442
+ effect.nodes?.a?.unfix();
443
+ (to_animate ??= new Set()).delete(effect);
423
444
  }
424
445
  }
425
446
 
426
- if (item !== current) {
427
- if (seen !== undefined && seen.has(item)) {
447
+ if (effect !== current) {
448
+ if (seen !== undefined && seen.has(effect)) {
428
449
  if (matched.length < stashed.length) {
429
450
  // more efficient to move later items to the front
430
451
  var start = stashed[0];
@@ -455,14 +476,14 @@ function reconcile(state, array, anchor, flags, get_key) {
455
476
  stashed = [];
456
477
  } else {
457
478
  // more efficient to move earlier items to the back
458
- seen.delete(item);
459
- move(item, current, anchor);
479
+ seen.delete(effect);
480
+ move(effect, current, anchor);
460
481
 
461
- link(state, item.prev, item.next);
462
- link(state, item, prev === null ? state.first : prev.next);
463
- link(state, prev, item);
482
+ link(state, effect.prev, effect.next);
483
+ link(state, effect, prev === null ? state.effect.first : prev.next);
484
+ link(state, prev, effect);
464
485
 
465
- prev = item;
486
+ prev = effect;
466
487
  }
467
488
 
468
489
  continue;
@@ -471,12 +492,8 @@ function reconcile(state, array, anchor, flags, get_key) {
471
492
  matched = [];
472
493
  stashed = [];
473
494
 
474
- while (current !== null && current !== item) {
475
- // If the each block isn't inert and an item has an effect that is already inert,
476
- // skip over adding it to our seen Set as the item is already being handled
477
- if ((current.e.f & INERT) === 0) {
478
- (seen ??= new Set()).add(current);
479
- }
495
+ while (current !== null && current !== effect) {
496
+ (seen ??= new Set()).add(current);
480
497
  stashed.push(current);
481
498
  current = current.next;
482
499
  }
@@ -484,42 +501,62 @@ function reconcile(state, array, anchor, flags, get_key) {
484
501
  if (current === null) {
485
502
  continue;
486
503
  }
504
+ }
487
505
 
488
- item = current;
506
+ if ((effect.f & EFFECT_OFFSCREEN) === 0) {
507
+ matched.push(effect);
489
508
  }
490
509
 
491
- matched.push(item);
492
- prev = item;
493
- current = item.next;
510
+ prev = effect;
511
+ current = effect.next;
494
512
  }
495
513
 
496
- let has_offscreen_items = items.size > length;
514
+ if (state.outrogroups !== null) {
515
+ for (const group of state.outrogroups) {
516
+ if (group.pending.size === 0) {
517
+ destroy_effects(array_from(group.done));
518
+ state.outrogroups?.delete(group);
519
+ }
520
+ }
521
+
522
+ if (state.outrogroups.size === 0) {
523
+ state.outrogroups = null;
524
+ }
525
+ }
497
526
 
498
527
  if (current !== null || seen !== undefined) {
499
- var to_destroy = seen === undefined ? [] : array_from(seen);
528
+ /** @type {Effect[]} */
529
+ var to_destroy = [];
530
+
531
+ if (seen !== undefined) {
532
+ for (effect of seen) {
533
+ if ((effect.f & INERT) === 0) {
534
+ to_destroy.push(effect);
535
+ }
536
+ }
537
+ }
500
538
 
501
539
  while (current !== null) {
502
540
  // If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished
503
- if ((current.e.f & INERT) === 0) {
541
+ if ((current.f & INERT) === 0 && current !== state.fallback) {
504
542
  to_destroy.push(current);
505
543
  }
544
+
506
545
  current = current.next;
507
546
  }
508
547
 
509
548
  var destroy_length = to_destroy.length;
510
549
 
511
- has_offscreen_items = items.size - destroy_length > length;
512
-
513
550
  if (destroy_length > 0) {
514
551
  var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
515
552
 
516
553
  if (is_animated) {
517
554
  for (i = 0; i < destroy_length; i += 1) {
518
- to_destroy[i].e.nodes?.a?.measure();
555
+ to_destroy[i].nodes?.a?.measure();
519
556
  }
520
557
 
521
558
  for (i = 0; i < destroy_length; i += 1) {
522
- to_destroy[i].e.nodes?.a?.fix();
559
+ to_destroy[i].nodes?.a?.fix();
523
560
  }
524
561
  }
525
562
 
@@ -527,23 +564,11 @@ function reconcile(state, array, anchor, flags, get_key) {
527
564
  }
528
565
  }
529
566
 
530
- // Append offscreen items at the end
531
- if (has_offscreen_items) {
532
- for (const item of items.values()) {
533
- if (!item.o) {
534
- link(state, prev, item);
535
- prev = item;
536
- }
537
- }
538
- }
539
-
540
- state.effect.last = prev && prev.e;
541
-
542
567
  if (is_animated) {
543
568
  queue_micro_task(() => {
544
569
  if (to_animate === undefined) return;
545
- for (item of to_animate) {
546
- item.e.nodes?.a?.apply();
570
+ for (effect of to_animate) {
571
+ effect.nodes?.a?.apply();
547
572
  }
548
573
  });
549
574
  }
@@ -551,8 +576,8 @@ function reconcile(state, array, anchor, flags, get_key) {
551
576
 
552
577
  /**
553
578
  * @template V
554
- * @param {Node | null} anchor
555
- * @param {EachItem | null} prev
579
+ * @param {Map<any, EachItem>} items
580
+ * @param {Node} anchor
556
581
  * @param {V} value
557
582
  * @param {unknown} key
558
583
  * @param {number} index
@@ -561,96 +586,81 @@ function reconcile(state, array, anchor, flags, get_key) {
561
586
  * @param {() => V[]} get_collection
562
587
  * @returns {EachItem}
563
588
  */
564
- function create_item(anchor, prev, value, key, index, render_fn, flags, get_collection) {
565
- var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
566
- var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0;
589
+ function create_item(items, anchor, value, key, index, render_fn, flags, get_collection) {
590
+ var v =
591
+ (flags & EACH_ITEM_REACTIVE) !== 0
592
+ ? (flags & EACH_ITEM_IMMUTABLE) === 0
593
+ ? mutable_source(value, false, false)
594
+ : source(value)
595
+ : null;
567
596
 
568
- var v = reactive ? (mutable ? mutable_source(value, false, false) : source(value)) : value;
569
- var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
597
+ var i = (flags & EACH_INDEX_REACTIVE) !== 0 ? source(index) : null;
570
598
 
571
- if (DEV && reactive) {
599
+ if (DEV && v) {
572
600
  // For tracing purposes, we need to link the source signal we create with the
573
601
  // collection + index so that tracing works as intended
574
- /** @type {Value} */ (v).trace = () => {
575
- var collection_index = typeof i === 'number' ? index : i.v;
602
+ v.trace = () => {
576
603
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
577
- get_collection()[collection_index];
604
+ get_collection()[i?.v ?? index];
578
605
  };
579
606
  }
580
607
 
581
- /** @type {EachItem} */
582
- var item = {
583
- i,
608
+ return {
584
609
  v,
585
- k: key,
586
- // @ts-expect-error
587
- e: null,
588
- o: false,
589
- prev,
590
- next: null
591
- };
592
-
593
- if (anchor === null) {
594
- var fragment = document.createDocumentFragment();
595
- fragment.append((anchor = create_text()));
596
- }
597
-
598
- item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection));
599
-
600
- if (prev !== null) {
601
- // we only need to set `prev.next = item`, because
602
- // `item.prev = prev` was set on initialization.
603
- // the effects themselves are already linked
604
- prev.next = item;
605
- }
610
+ i,
611
+ e: branch(() => {
612
+ render_fn(anchor, v ?? value, i ?? index, get_collection);
606
613
 
607
- return item;
614
+ return () => {
615
+ items.delete(key);
616
+ };
617
+ })
618
+ };
608
619
  }
609
620
 
610
621
  /**
611
- * @param {EachItem} item
612
- * @param {EachItem | null} next
622
+ * @param {Effect} effect
623
+ * @param {Effect | null} next
613
624
  * @param {Text | Element | Comment} anchor
614
625
  */
615
- function move(item, next, anchor) {
616
- if (!item.e.nodes) return;
626
+ function move(effect, next, anchor) {
627
+ if (!effect.nodes) return;
617
628
 
618
- var end = item.next ? /** @type {EffectNodes} */ (item.next.e.nodes).start : anchor;
629
+ var node = effect.nodes.start;
630
+ var end = effect.nodes.end;
619
631
 
620
- var dest = next ? /** @type {EffectNodes} */ (next.e.nodes).start : anchor;
621
- var node = /** @type {TemplateNode} */ (item.e.nodes.start);
632
+ var dest =
633
+ next && (next.f & EFFECT_OFFSCREEN) === 0
634
+ ? /** @type {EffectNodes} */ (next.nodes).start
635
+ : anchor;
622
636
 
623
- while (node !== null && node !== end) {
637
+ while (node !== null) {
624
638
  var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
625
639
  dest.before(node);
640
+
641
+ if (node === end) {
642
+ return;
643
+ }
644
+
626
645
  node = next_node;
627
646
  }
628
647
  }
629
648
 
630
649
  /**
631
650
  * @param {EachState} state
632
- * @param {EachItem | null} prev
633
- * @param {EachItem | null} next
651
+ * @param {Effect | null} prev
652
+ * @param {Effect | null} next
634
653
  */
635
654
  function link(state, prev, next) {
636
655
  if (prev === null) {
637
- state.first = next;
638
- state.effect.first = next && next.e;
656
+ state.effect.first = next;
639
657
  } else {
640
- if (prev.e.next) {
641
- prev.e.next.prev = null;
642
- }
643
-
644
658
  prev.next = next;
645
- prev.e.next = next && next.e;
646
659
  }
647
660
 
648
- if (next !== null) {
649
- if (next.e.prev) {
650
- next.e.prev.next = null;
651
- }
652
-
661
+ if (next === null) {
662
+ state.effect.last = prev;
663
+ } else {
653
664
  next.prev = prev;
654
- next.e.prev = prev && prev.e;
655
665
  }
656
666
  }