preact
Version:
Fast 3kb React-compatible Virtual DOM library.
259 lines (225 loc) • 6.92 kB
JavaScript
/* istanbul ignore file */
import {
getData,
getChildren,
getInstance,
hasDataChanged,
isRoot
} from './custom';
/**
* Custom renderer tailored for Preact. It converts updated vnode trees
* to events the devtools can understand.
* @class Renderer
*/
export class Renderer {
constructor(hook, rid) {
/** @type {string} */
this.rid = rid;
this.hook = hook;
/** @type {Array<import('../internal').DevtoolsEvent>} */
this.pending = [];
/**
* Store the instance of a vnode. This will be later used to decide if a
* vnode needs to be mounted or updated. For components the instance refers
* to the actual class instance whereas for dom nodes it refers to the
* underlying dom element.
* @type {WeakMap<import('../internal').Component | import('../internal').PreactElement | HTMLElement | Text, import('../internal').VNode>}
*/
this.inst2vnode = new WeakMap();
this.connected = false;
}
/**
* Mark the connection status as ready so that we can proceed
* to actually flush events.
*/
markConnected() {
this.connected = true;
this.flushPendingEvents();
}
/**
* Flush all queued events
*/
flushPendingEvents() {
if (!this.connected) return;
let events = this.pending;
this.pending = [];
for (let i = 0; i < events.length; i++) {
let event = events[i];
this.hook.emit(event.type, event);
}
}
/**
* Recursively mount a vnode tree. Note that the devtools expects the tree to
* be mounted from the bottom up, otherwise the order will be messed up.
* Therefore we mount children prior to mounting the vnode itself.
* @param {import('../internal').VNode} vnode
*/
mount(vnode) {
this.inst2vnode.set(getInstance(vnode), vnode);
let data = getData(vnode);
/** @type {Array<import('../internal').DevtoolsEvent>} */
let work = [
{
internalInstance: vnode,
data,
renderer: this.rid,
type: 'mount'
}
];
// Children must be mounted first
if (Array.isArray(data.children)) {
let stack = data.children.slice();
let item;
while ((item = stack.pop()) != null) {
let children = getChildren(item);
stack.push(...children);
this.inst2vnode.set(getInstance(item), item);
let data = getData(item);
work.push({
internalInstance: item,
data,
renderer: this.rid,
type: 'mount'
});
}
}
for (let i = work.length; --i >= 0; ) {
this.pending.push(work[i]);
}
// Special event if we have a root
if (isRoot(vnode)) {
this.pending.push({
internalInstance: vnode,
data,
renderer: this.rid,
type: 'root'
});
}
}
/**
* Update a vnode tree
* @param {import('../internal').VNode} vnode
*/
update(vnode) {
let data = getData(vnode);
// Children must be updated first
if (Array.isArray(data.children)) {
for (let i = 0; i < data.children.length; i++) {
let child = data.children[i];
let inst = getInstance(child);
let prevChild = this.inst2vnode.get(inst);
if (prevChild == null) this.mount(child);
else this.update(child);
// Mutate child to keep referential equality intact
data.children[i] = this.inst2vnode.get(inst);
}
}
let prev = this.inst2vnode.get(data.publicInstance);
// The `updateProfileTimes` event is a faster version of `updated` and
// is processed much quicker inside the devtools extension.
if (!hasDataChanged(prev, vnode)) {
// Always assume profiling data has changed. When we skip an event here
// the devtools element picker will somehow break.
this.pending.push({
// This property is only used as an id inside the devtools. The
// relevant data will be read from `.data` instead which is a
// normalized structure that every react release adheres to. This
// makes backwards-compatibility easier instead of relying on internal
// vnode/fiber shape.
internalInstance: prev,
data,
renderer: this.rid,
type: 'updateProfileTimes'
});
return;
}
this.pending.push({
// This property is only used as an id inside the devtools. The
// relevant data will be read from `.data` instead which is a
// normalized structure that every react release adheres to. This
// makes backwards-compatibility easier instead of relying on internal
// vnode/fiber shape.
internalInstance: prev,
data,
renderer: this.rid,
type: 'update'
});
}
/**
* Pass a rendered tree to the devtools. At this point elements have already
* unmounted, so we don't need to check for removals and only update vs mount
* instead.
* @param {import('../internal').VNode} vnode
*/
handleCommitFiberRoot(vnode) {
let inst = getInstance(vnode);
if (this.inst2vnode.has(inst)) this.update(vnode);
else this.mount(vnode);
// The devtools checks via the existence of this property if the devtools
// profiler should be enabled or not. If it is missing from the first root
// node the "Profiler" tab won't show up.
/** @type {import('../internal').VNode} */
let root = null;
if (isRoot(vnode)) {
/** @type {*} */
(vnode).treeBaseDuration = 0;
root = vnode;
} else {
// "rootCommitted" always needs the actual root node for the profiler
// to be able to collect timings. The `_parent` property will
// point to a vnode for a root node.
root = vnode;
while (root._parent != null) {
root = root._parent;
}
}
this.pending.push({
internalInstance: root,
renderer: this.rid,
data: getData(root),
type: 'rootCommitted'
});
this.flushPendingEvents();
return vnode;
}
/**
* Unmount a vnode recursively. Contrary to mounting or updating unmounting needs
* to push the events in parent-first order. Because `options.unmount` is
* already fired in parent-first order we don't need to traverse anything here.
* @param {import('../internal').VNode} vnode
*/
handleCommitFiberUnmount(vnode) {
let inst = getInstance(vnode);
this.inst2vnode.delete(inst);
// Special case when unmounting a root (most prominently caused by webpack's
// `hot-module-reloading`). If this happens we need to unmount the virtual
// `Fragment` we're wrapping around each root just for the devtools.
this.pending.push({
internalInstance: vnode,
renderer: this.rid,
type: 'unmount'
});
}
/**
* Get the dom element by a vnode
* @param {import('../internal').VNode} vnode
* @returns {import('../internal').PreactElement | Text}
*/
getNativeFromReactElement(vnode) {
return vnode._dom;
}
/**
* Get a vnode by a dom element
* @param {import('../internal').PreactElement | Text} dom
* @returns {import('../internal').VNode | null}
*/
getReactElementFromNative(dom) {
return this.inst2vnode.get(dom) || null;
}
// Unused, but devtools expects it to be there
/* istanbul ignore next */
walkTree() {}
// Unused, but devtools expects it to be there
/* istanbul ignore next */
cleanup() {}
}