UNPKG

4.49 kBJavaScriptView Raw
1import { Component, toChildArray } from 'preact';
2import { suspended } from './suspense.js';
3
4// Indexes to linked list nodes (nodes are stored as arrays to save bytes).
5const SUSPENDED_COUNT = 0;
6const RESOLVED_COUNT = 1;
7const NEXT_NODE = 2;
8
9// Having custom inheritance instead of a class here saves a lot of bytes.
10export function SuspenseList() {
11 this._next = null;
12 this._map = null;
13}
14
15// Mark one of child's earlier suspensions as resolved.
16// Some pending callbacks may become callable due to this
17// (e.g. the last suspended descendant gets resolved when
18// revealOrder === 'together'). Process those callbacks as well.
19const resolve = (list, child, node) => {
20 if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) {
21 // The number a child (or any of its descendants) has been suspended
22 // matches the number of times it's been resolved. Therefore we
23 // mark the child as completely resolved by deleting it from ._map.
24 // This is used to figure out when *all* children have been completely
25 // resolved when revealOrder is 'together'.
26 list._map.delete(child);
27 }
28
29 // If revealOrder is falsy then we can do an early exit, as the
30 // callbacks won't get queued in the node anyway.
31 // If revealOrder is 'together' then also do an early exit
32 // if all suspended descendants have not yet been resolved.
33 if (
34 !list.props.revealOrder ||
35 (list.props.revealOrder[0] === 't' && list._map.size)
36 ) {
37 return;
38 }
39
40 // Walk the currently suspended children in order, calling their
41 // stored callbacks on the way. Stop if we encounter a child that
42 // has not been completely resolved yet.
43 node = list._next;
44 while (node) {
45 while (node.length > 3) {
46 node.pop()();
47 }
48 if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) {
49 break;
50 }
51 list._next = node = node[NEXT_NODE];
52 }
53};
54
55// Things we do here to save some bytes but are not proper JS inheritance:
56// - call `new Component()` as the prototype
57// - do not set `Suspense.prototype.constructor` to `Suspense`
58SuspenseList.prototype = new Component();
59
60SuspenseList.prototype._suspended = function(child) {
61 const list = this;
62 const delegated = suspended(list._vnode);
63
64 let node = list._map.get(child);
65 node[SUSPENDED_COUNT]++;
66
67 return unsuspend => {
68 const wrappedUnsuspend = () => {
69 if (!list.props.revealOrder) {
70 // Special case the undefined (falsy) revealOrder, as there
71 // is no need to coordinate a specific order or unsuspends.
72 unsuspend();
73 } else {
74 node.push(unsuspend);
75 resolve(list, child, node);
76 }
77 };
78 if (delegated) {
79 delegated(wrappedUnsuspend);
80 } else {
81 wrappedUnsuspend();
82 }
83 };
84};
85
86SuspenseList.prototype.render = function(props) {
87 this._next = null;
88 this._map = new Map();
89
90 const children = toChildArray(props.children);
91 if (props.revealOrder && props.revealOrder[0] === 'b') {
92 // If order === 'backwards' (or, well, anything starting with a 'b')
93 // then flip the child list around so that the last child will be
94 // the first in the linked list.
95 children.reverse();
96 }
97 // Build the linked list. Iterate through the children in reverse order
98 // so that `_next` points to the first linked list node to be resolved.
99 for (let i = children.length; i--; ) {
100 // Create a new linked list node as an array of form:
101 // [suspended_count, resolved_count, next_node]
102 // where suspended_count and resolved_count are numeric counters for
103 // keeping track how many times a node has been suspended and resolved.
104 //
105 // Note that suspended_count starts from 1 instead of 0, so we can block
106 // processing callbacks until componentDidMount has been called. In a sense
107 // node is suspended at least until componentDidMount gets called!
108 //
109 // Pending callbacks are added to the end of the node:
110 // [suspended_count, resolved_count, next_node, callback_0, callback_1, ...]
111 this._map.set(children[i], (this._next = [1, 0, this._next]));
112 }
113 return props.children;
114};
115
116SuspenseList.prototype.componentDidUpdate = SuspenseList.prototype.componentDidMount = function() {
117 // Iterate through all children after mounting for two reasons:
118 // 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases
119 // each node[RELEASED_COUNT] by 1, therefore balancing the counters.
120 // The nodes can now be completely consumed from the linked list.
121 // 2. Handle nodes that might have gotten resolved between render and
122 // componentDidMount.
123 this._map.forEach((node, child) => {
124 resolve(this, child, node);
125 });
126};