mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
9b1ec5dc0e
This commit moves OwlError to its own file in the new common folder such that it is no longer associated with the runtime.
266 lines
7.6 KiB
TypeScript
266 lines
7.6 KiB
TypeScript
import { BDom, mount } from "./blockdom";
|
|
import type { ComponentNode } from "./component_node";
|
|
import { fibersInError } from "./error_handling";
|
|
import { OwlError } from "../common/owl_error";
|
|
import { STATUS } from "./status";
|
|
|
|
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
|
let current = node.fiber;
|
|
if (current) {
|
|
cancelFibers(current.children);
|
|
current.root = null;
|
|
}
|
|
return new Fiber(node, parent);
|
|
}
|
|
|
|
export function makeRootFiber(node: ComponentNode): Fiber {
|
|
let current = node.fiber;
|
|
if (current) {
|
|
let root = current.root!;
|
|
// lock root fiber because canceling children fibers may destroy components,
|
|
// which means any arbitrary code can be run in onWillDestroy, which may
|
|
// trigger new renderings
|
|
root.locked = true;
|
|
root.setCounter(root.counter + 1 - cancelFibers(current.children));
|
|
root.locked = false;
|
|
current.children = [];
|
|
current.childrenMap = {};
|
|
current.bdom = null;
|
|
if (fibersInError.has(current)) {
|
|
fibersInError.delete(current);
|
|
fibersInError.delete(root);
|
|
current.appliedToDom = false;
|
|
}
|
|
return current;
|
|
}
|
|
const fiber = new RootFiber(node, null);
|
|
if (node.willPatch.length) {
|
|
fiber.willPatch.push(fiber);
|
|
}
|
|
if (node.patched.length) {
|
|
fiber.patched.push(fiber);
|
|
}
|
|
return fiber;
|
|
}
|
|
|
|
function throwOnRender() {
|
|
throw new OwlError("Attempted to render cancelled fiber");
|
|
}
|
|
|
|
/**
|
|
* @returns number of not-yet rendered fibers cancelled
|
|
*/
|
|
function cancelFibers(fibers: Fiber[]): number {
|
|
let result = 0;
|
|
for (let fiber of fibers) {
|
|
let node = fiber.node;
|
|
fiber.render = throwOnRender;
|
|
if (node.status === STATUS.NEW) {
|
|
node.cancel();
|
|
}
|
|
node.fiber = null;
|
|
if (fiber.bdom) {
|
|
// if fiber has been rendered, this means that the component props have
|
|
// been updated. however, this fiber will not be patched to the dom, so
|
|
// it could happen that the next render compare the current props with
|
|
// the same props, and skip the render completely. With the next line,
|
|
// we kindly request the component code to force a render, so it works as
|
|
// expected.
|
|
node.forceNextRender = true;
|
|
} else {
|
|
result++;
|
|
}
|
|
result += cancelFibers(fiber.children);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export class Fiber {
|
|
node: ComponentNode;
|
|
bdom: BDom | null = null;
|
|
root: RootFiber | null; // A Fiber that has been replaced by another has no root
|
|
parent: Fiber | null;
|
|
children: Fiber[] = [];
|
|
appliedToDom = false;
|
|
deep: boolean = false;
|
|
childrenMap: ComponentNode["children"] = {};
|
|
|
|
constructor(node: ComponentNode, parent: Fiber | null) {
|
|
this.node = node;
|
|
this.parent = parent;
|
|
if (parent) {
|
|
this.deep = parent.deep;
|
|
const root = parent.root!;
|
|
root.setCounter(root.counter + 1);
|
|
this.root = root;
|
|
parent.children.push(this);
|
|
} else {
|
|
this.root = this as any;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
// if some parent has a fiber => register in followup
|
|
let prev = this.root!.node;
|
|
let scheduler = prev.app.scheduler;
|
|
let current = prev.parent;
|
|
while (current) {
|
|
if (current.fiber) {
|
|
let root = current.fiber.root!;
|
|
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
|
|
current = root.node;
|
|
} else {
|
|
scheduler.delayedRenders.push(this);
|
|
return;
|
|
}
|
|
}
|
|
prev = current;
|
|
current = current.parent;
|
|
}
|
|
|
|
// there are no current rendering from above => we can render
|
|
this._render();
|
|
}
|
|
|
|
_render() {
|
|
const node = this.node;
|
|
const root = this.root;
|
|
if (root) {
|
|
try {
|
|
(this.bdom as any) = true;
|
|
this.bdom = node.renderFn();
|
|
} catch (e) {
|
|
node.app.handleError({ node, error: e });
|
|
}
|
|
root.setCounter(root.counter - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class RootFiber extends Fiber {
|
|
counter: number = 1;
|
|
|
|
// only add stuff in this if they have registered some hooks
|
|
willPatch: Fiber[] = [];
|
|
patched: Fiber[] = [];
|
|
mounted: Fiber[] = [];
|
|
// A fiber is typically locked when it is completing and the patch has not, or is being applied.
|
|
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
|
|
locked: boolean = false;
|
|
|
|
complete() {
|
|
const node = this.node;
|
|
this.locked = true;
|
|
let current: Fiber | undefined = undefined;
|
|
try {
|
|
// Step 1: calling all willPatch lifecycle hooks
|
|
for (current of this.willPatch) {
|
|
// because of the asynchronous nature of the rendering, some parts of the
|
|
// UI may have been rendered, then deleted in a followup rendering, and we
|
|
// do not want to call onWillPatch in that case.
|
|
let node = current.node;
|
|
if (node.fiber === current) {
|
|
const component = node.component;
|
|
for (let cb of node.willPatch) {
|
|
cb.call(component);
|
|
}
|
|
}
|
|
}
|
|
current = undefined;
|
|
|
|
// Step 2: patching the dom
|
|
node._patch();
|
|
this.locked = false;
|
|
|
|
// Step 4: calling all mounted lifecycle hooks
|
|
let mountedFibers = this.mounted;
|
|
while ((current = mountedFibers.pop())) {
|
|
current = current;
|
|
if (current.appliedToDom) {
|
|
for (let cb of current.node.mounted) {
|
|
cb();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 5: calling all patched hooks
|
|
let patchedFibers = this.patched;
|
|
while ((current = patchedFibers.pop())) {
|
|
current = current;
|
|
if (current.appliedToDom) {
|
|
for (let cb of current.node.patched) {
|
|
cb();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.locked = false;
|
|
node.app.handleError({ fiber: current || this, error: e });
|
|
}
|
|
}
|
|
|
|
setCounter(newValue: number) {
|
|
this.counter = newValue;
|
|
if (newValue === 0) {
|
|
this.node.app.scheduler.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
type Position = "first-child" | "last-child";
|
|
|
|
export interface MountOptions {
|
|
position?: Position;
|
|
}
|
|
|
|
export class MountFiber extends RootFiber {
|
|
target: HTMLElement;
|
|
position: Position;
|
|
|
|
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
|
|
super(node, null);
|
|
this.target = target;
|
|
this.position = options.position || "last-child";
|
|
}
|
|
complete() {
|
|
let current: Fiber | undefined = this;
|
|
try {
|
|
const node = this.node;
|
|
node.children = this.childrenMap;
|
|
(node.app.constructor as any).validateTarget(this.target);
|
|
if (node.bdom) {
|
|
// this is a complicated situation: if we mount a fiber with an existing
|
|
// bdom, this means that this same fiber was already completed, mounted,
|
|
// but a crash occurred in some mounted hook. Then, it was handled and
|
|
// the new rendering is being applied.
|
|
node.updateDom();
|
|
} else {
|
|
node.bdom = this.bdom;
|
|
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
|
mount(node.bdom!, this.target);
|
|
} else {
|
|
const firstChild = this.target.childNodes[0];
|
|
mount(node.bdom!, this.target, firstChild);
|
|
}
|
|
}
|
|
|
|
// unregistering the fiber before mounted since it can do another render
|
|
// and that the current rendering is obviously completed
|
|
node.fiber = null;
|
|
|
|
node.status = STATUS.MOUNTED;
|
|
this.appliedToDom = true;
|
|
let mountedFibers = this.mounted;
|
|
while ((current = mountedFibers.pop())) {
|
|
if (current.appliedToDom) {
|
|
for (let cb of current.node.mounted) {
|
|
cb();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.node.app.handleError({ fiber: current as Fiber, error: e });
|
|
}
|
|
}
|
|
}
|