mirror of
https://github.com/imezx/Warp.git
synced 2025-04-25 07:30:02 +00:00
127 lines
4.4 KiB
JavaScript
127 lines
4.4 KiB
JavaScript
import { Component, toChildArray } from 'preact';
|
|
import { suspended } from './suspense.js';
|
|
|
|
// Indexes to linked list nodes (nodes are stored as arrays to save bytes).
|
|
const SUSPENDED_COUNT = 0;
|
|
const RESOLVED_COUNT = 1;
|
|
const NEXT_NODE = 2;
|
|
|
|
// Having custom inheritance instead of a class here saves a lot of bytes.
|
|
export function SuspenseList() {
|
|
this._next = null;
|
|
this._map = null;
|
|
}
|
|
|
|
// Mark one of child's earlier suspensions as resolved.
|
|
// Some pending callbacks may become callable due to this
|
|
// (e.g. the last suspended descendant gets resolved when
|
|
// revealOrder === 'together'). Process those callbacks as well.
|
|
const resolve = (list, child, node) => {
|
|
if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) {
|
|
// The number a child (or any of its descendants) has been suspended
|
|
// matches the number of times it's been resolved. Therefore we
|
|
// mark the child as completely resolved by deleting it from ._map.
|
|
// This is used to figure out when *all* children have been completely
|
|
// resolved when revealOrder is 'together'.
|
|
list._map.delete(child);
|
|
}
|
|
|
|
// If revealOrder is falsy then we can do an early exit, as the
|
|
// callbacks won't get queued in the node anyway.
|
|
// If revealOrder is 'together' then also do an early exit
|
|
// if all suspended descendants have not yet been resolved.
|
|
if (
|
|
!list.props.revealOrder ||
|
|
(list.props.revealOrder[0] === 't' && list._map.size)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Walk the currently suspended children in order, calling their
|
|
// stored callbacks on the way. Stop if we encounter a child that
|
|
// has not been completely resolved yet.
|
|
node = list._next;
|
|
while (node) {
|
|
while (node.length > 3) {
|
|
node.pop()();
|
|
}
|
|
if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) {
|
|
break;
|
|
}
|
|
list._next = node = node[NEXT_NODE];
|
|
}
|
|
};
|
|
|
|
// Things we do here to save some bytes but are not proper JS inheritance:
|
|
// - call `new Component()` as the prototype
|
|
// - do not set `Suspense.prototype.constructor` to `Suspense`
|
|
SuspenseList.prototype = new Component();
|
|
|
|
SuspenseList.prototype._suspended = function (child) {
|
|
const list = this;
|
|
const delegated = suspended(list._vnode);
|
|
|
|
let node = list._map.get(child);
|
|
node[SUSPENDED_COUNT]++;
|
|
|
|
return unsuspend => {
|
|
const wrappedUnsuspend = () => {
|
|
if (!list.props.revealOrder) {
|
|
// Special case the undefined (falsy) revealOrder, as there
|
|
// is no need to coordinate a specific order or unsuspends.
|
|
unsuspend();
|
|
} else {
|
|
node.push(unsuspend);
|
|
resolve(list, child, node);
|
|
}
|
|
};
|
|
if (delegated) {
|
|
delegated(wrappedUnsuspend);
|
|
} else {
|
|
wrappedUnsuspend();
|
|
}
|
|
};
|
|
};
|
|
|
|
SuspenseList.prototype.render = function (props) {
|
|
this._next = null;
|
|
this._map = new Map();
|
|
|
|
const children = toChildArray(props.children);
|
|
if (props.revealOrder && props.revealOrder[0] === 'b') {
|
|
// If order === 'backwards' (or, well, anything starting with a 'b')
|
|
// then flip the child list around so that the last child will be
|
|
// the first in the linked list.
|
|
children.reverse();
|
|
}
|
|
// Build the linked list. Iterate through the children in reverse order
|
|
// so that `_next` points to the first linked list node to be resolved.
|
|
for (let i = children.length; i--; ) {
|
|
// Create a new linked list node as an array of form:
|
|
// [suspended_count, resolved_count, next_node]
|
|
// where suspended_count and resolved_count are numeric counters for
|
|
// keeping track how many times a node has been suspended and resolved.
|
|
//
|
|
// Note that suspended_count starts from 1 instead of 0, so we can block
|
|
// processing callbacks until componentDidMount has been called. In a sense
|
|
// node is suspended at least until componentDidMount gets called!
|
|
//
|
|
// Pending callbacks are added to the end of the node:
|
|
// [suspended_count, resolved_count, next_node, callback_0, callback_1, ...]
|
|
this._map.set(children[i], (this._next = [1, 0, this._next]));
|
|
}
|
|
return props.children;
|
|
};
|
|
|
|
SuspenseList.prototype.componentDidUpdate =
|
|
SuspenseList.prototype.componentDidMount = function () {
|
|
// Iterate through all children after mounting for two reasons:
|
|
// 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases
|
|
// each node[RELEASED_COUNT] by 1, therefore balancing the counters.
|
|
// The nodes can now be completely consumed from the linked list.
|
|
// 2. Handle nodes that might have gotten resolved between render and
|
|
// componentDidMount.
|
|
this._map.forEach((node, child) => {
|
|
resolve(this, child, node);
|
|
});
|
|
};
|