mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
3a93370ab6
Before this commit, the error handling code simply destroyed the application whenever an unhandled error occured in the owl rendering process. This is perfectly fine, except that since the application is potentially corrupted, the destroy code may crash as well. We simply catch those errors to avoid shadowing the main issue. closes #866
369 lines
12 KiB
TypeScript
369 lines
12 KiB
TypeScript
import { h, VNode } from "../vdom/index";
|
|
import { Component, MountPosition, STATUS } from "./component";
|
|
import { scheduler } from "./scheduler";
|
|
|
|
/**
|
|
* Owl Fiber Class
|
|
*
|
|
* Fibers are small abstractions designed to contain all the internal state
|
|
* associated with a "rendering work unit", relative to a specific component.
|
|
*
|
|
* A rendering will cause the creation of a fiber for each impacted components.
|
|
*
|
|
* Fibers capture all that necessary information, which is critical to owl
|
|
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
|
|
* states and in general determine the state of the rendering.
|
|
*/
|
|
|
|
export class Fiber {
|
|
static nextId: number = 1;
|
|
id: number = Fiber.nextId++;
|
|
|
|
// The force attribute determines if a rendering should bypass the `shouldUpdate`
|
|
// method potentially implemented by a component. It is usually set to false.
|
|
force: boolean;
|
|
|
|
// isCompleted means that the rendering corresponding to this fiber's work is
|
|
// done, either because the component has been mounted or patched, or because
|
|
// fiber has been cancelled.
|
|
isCompleted: boolean = false;
|
|
|
|
// the fibers corresponding to component updates (updateProps) need to call
|
|
// the willPatch and patched hooks from the corresponding component. However,
|
|
// fibers corresponding to a new component do not need to do that. So, the
|
|
// shouldPatch hook is the boolean that we check whenever we need to apply
|
|
// a patch.
|
|
shouldPatch: boolean = true;
|
|
|
|
// isRendered is the last state of a fiber. If true, this means that it has
|
|
// been rendered and is inert (so, it should not be taken into account when
|
|
// counting the number of active fibers).
|
|
isRendered: boolean = false;
|
|
|
|
// the counter number is a critical information. It is only necessary for a
|
|
// root fiber. For that fiber, this number counts the number of active sub
|
|
// fibers. When that number reaches 0, the fiber can be applied by the
|
|
// scheduler.
|
|
counter: number = 0;
|
|
|
|
target: HTMLElement | DocumentFragment | null;
|
|
position: MountPosition | null;
|
|
|
|
scope: any;
|
|
|
|
component: Component;
|
|
vnode: VNode | null = null;
|
|
|
|
root: Fiber;
|
|
child: Fiber | null = null;
|
|
sibling: Fiber | null = null;
|
|
lastChild: Fiber | null = null;
|
|
parent: Fiber | null = null;
|
|
|
|
error?: Error;
|
|
|
|
constructor(
|
|
parent: Fiber | null,
|
|
component: Component,
|
|
force: boolean,
|
|
target: HTMLElement | DocumentFragment | null,
|
|
position: MountPosition | null
|
|
) {
|
|
this.component = component;
|
|
this.force = force;
|
|
this.target = target;
|
|
this.position = position;
|
|
|
|
const __owl__ = component.__owl__;
|
|
this.scope = __owl__.scope;
|
|
|
|
this.root = parent ? parent.root : this;
|
|
this.parent = parent;
|
|
|
|
let oldFiber = __owl__.currentFiber;
|
|
if (oldFiber && !oldFiber.isCompleted) {
|
|
this.force = true;
|
|
if (oldFiber.root === oldFiber && !parent) {
|
|
// both oldFiber and this fiber are root fibers
|
|
this._reuseFiber(oldFiber);
|
|
return oldFiber;
|
|
} else {
|
|
this._remapFiber(oldFiber);
|
|
}
|
|
}
|
|
|
|
this.root.counter++;
|
|
|
|
__owl__.currentFiber = this;
|
|
}
|
|
|
|
/**
|
|
* When the oldFiber is not completed yet, and both oldFiber and this fiber
|
|
* are root fibers, we want to reuse the oldFiber instead of creating a new
|
|
* one. Doing so will guarantee that the initiator(s) of those renderings will
|
|
* be notified (the promise will resolve) when the last rendering will be done.
|
|
*
|
|
* This function thus assumes that oldFiber is a root fiber.
|
|
*/
|
|
_reuseFiber(oldFiber: Fiber) {
|
|
oldFiber.cancel(); // cancel children fibers
|
|
oldFiber.target = this.target || oldFiber.target;
|
|
oldFiber.position = this.position || oldFiber.position;
|
|
oldFiber.isCompleted = false; // keep the root fiber alive
|
|
oldFiber.isRendered = false; // the fiber has to be re-rendered
|
|
if (oldFiber.child) {
|
|
// remove relation to children
|
|
oldFiber.child.parent = null;
|
|
oldFiber.child = null;
|
|
oldFiber.lastChild = null;
|
|
}
|
|
oldFiber.counter = 1; // re-initialize counter
|
|
oldFiber.id = Fiber.nextId++;
|
|
}
|
|
|
|
/**
|
|
* In some cases, a rendering initiated at some component can detect that it
|
|
* should be part of a larger rendering initiated somewhere up the component
|
|
* tree. In that case, it needs to cancel the previous rendering and
|
|
* remap itself as a part of the current parent rendering.
|
|
*/
|
|
_remapFiber(oldFiber: Fiber) {
|
|
oldFiber.cancel();
|
|
this.shouldPatch = oldFiber.shouldPatch;
|
|
if (oldFiber === oldFiber.root) {
|
|
oldFiber.counter++;
|
|
}
|
|
if (oldFiber.parent && !this.parent) {
|
|
// re-map links
|
|
this.parent = oldFiber.parent;
|
|
this.root = this.parent.root;
|
|
this.sibling = oldFiber.sibling;
|
|
if (this.parent.lastChild === oldFiber) {
|
|
this.parent.lastChild = this;
|
|
}
|
|
if (this.parent.child === oldFiber) {
|
|
this.parent.child = this;
|
|
} else {
|
|
let current = this.parent.child!;
|
|
while (true) {
|
|
if (current.sibling === oldFiber) {
|
|
current.sibling = this;
|
|
break;
|
|
}
|
|
current = current.sibling!;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function has been taken from
|
|
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
|
|
*/
|
|
_walk(doWork: (f: Fiber) => Fiber | null) {
|
|
let root = this;
|
|
let current: Fiber = this;
|
|
while (true) {
|
|
const child = doWork(current);
|
|
if (child) {
|
|
current = child;
|
|
continue;
|
|
}
|
|
if (current === root) {
|
|
return;
|
|
}
|
|
while (!current.sibling) {
|
|
if (!current.parent || current.parent === root) {
|
|
return;
|
|
}
|
|
current = current.parent;
|
|
}
|
|
current = current.sibling;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Successfully complete the work of the fiber: call the mount or patch hooks
|
|
* and patch the DOM. This function is called once the fiber and its children
|
|
* are ready, and the scheduler decides to process it.
|
|
*/
|
|
complete() {
|
|
let component = this.component;
|
|
this.isCompleted = true;
|
|
const status = component.__owl__.status;
|
|
if (status === STATUS.DESTROYED) {
|
|
return;
|
|
}
|
|
|
|
// build patchQueue
|
|
const patchQueue: Fiber[] = [];
|
|
const doWork: (Fiber) => Fiber | null = function (f) {
|
|
patchQueue.push(f);
|
|
return f.child;
|
|
};
|
|
this._walk(doWork);
|
|
const patchLen = patchQueue.length;
|
|
|
|
// call willPatch hook on each fiber of patchQueue
|
|
if (status === STATUS.MOUNTED) {
|
|
for (let i = 0; i < patchLen; i++) {
|
|
const fiber = patchQueue[i];
|
|
if (fiber.shouldPatch) {
|
|
component = fiber.component;
|
|
if (component.__owl__.willPatchCB) {
|
|
component.__owl__.willPatchCB();
|
|
}
|
|
component.willPatch();
|
|
}
|
|
}
|
|
}
|
|
|
|
// call __patch on each fiber of (reversed) patchQueue
|
|
for (let i = patchLen - 1; i >= 0; i--) {
|
|
const fiber = patchQueue[i];
|
|
component = fiber.component;
|
|
if (fiber.target && i === 0) {
|
|
let target;
|
|
if (fiber.position === "self") {
|
|
target = fiber.target;
|
|
if ((target as HTMLElement).tagName.toLowerCase() !== fiber.vnode!.sel) {
|
|
throw new Error(
|
|
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
|
|
);
|
|
}
|
|
// In self mode, we *know* we are to take possession of the target
|
|
// Hence we manually create the corresponding VNode and copy the "key" in data
|
|
const selfVnodeData = fiber.vnode!.data ? { key: fiber.vnode!.data.key } : {};
|
|
const selfVnode = h(fiber.vnode!.sel, selfVnodeData);
|
|
selfVnode.elm = target;
|
|
target = selfVnode;
|
|
} else {
|
|
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
|
|
}
|
|
component.__patch(target!, fiber.vnode!);
|
|
} else {
|
|
if (fiber.shouldPatch) {
|
|
component.__patch(component.__owl__.vnode!, fiber.vnode!);
|
|
// When updating a Component's props (in directive),
|
|
// the component has a pvnode AND should be patched.
|
|
// However, its pvnode.elm may have changed if it is a High Order Component
|
|
if (component.__owl__.pvnode) {
|
|
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
|
|
}
|
|
} else {
|
|
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
|
|
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
|
|
}
|
|
}
|
|
const compOwl = component.__owl__;
|
|
if (fiber === compOwl.currentFiber) {
|
|
compOwl.currentFiber = null;
|
|
}
|
|
}
|
|
|
|
// insert into the DOM (mount case)
|
|
let inDOM = false;
|
|
if (this.target) {
|
|
switch (this.position) {
|
|
case "first-child":
|
|
this.target.prepend(this.component.el!);
|
|
break;
|
|
case "last-child":
|
|
this.target.appendChild(this.component.el!);
|
|
break;
|
|
}
|
|
inDOM = document.body.contains(this.component.el);
|
|
this.component.env.qweb.trigger("dom-appended");
|
|
}
|
|
|
|
// call patched/mounted hook on each fiber of (reversed) patchQueue
|
|
if (status === STATUS.MOUNTED || inDOM) {
|
|
for (let i = patchLen - 1; i >= 0; i--) {
|
|
const fiber = patchQueue[i];
|
|
component = fiber.component;
|
|
if (fiber.shouldPatch && !this.target) {
|
|
component.patched();
|
|
if (component.__owl__.patchedCB) {
|
|
component.__owl__.patchedCB();
|
|
}
|
|
} else {
|
|
component.__callMounted();
|
|
}
|
|
}
|
|
} else {
|
|
for (let i = patchLen - 1; i >= 0; i--) {
|
|
const fiber = patchQueue[i];
|
|
component = fiber.component;
|
|
component.__owl__.status = STATUS.UNMOUNTED;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel a fiber and all its children.
|
|
*/
|
|
cancel() {
|
|
this._walk((f) => {
|
|
if (!f.isRendered) {
|
|
f.root.counter--;
|
|
}
|
|
f.isCompleted = true;
|
|
return f.child;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This is the global error handler for errors occurring in Owl main lifecycle
|
|
* methods. Caught errors are triggered on the QWeb instance, and are
|
|
* potentially given to some parent component which implements `catchError`.
|
|
*
|
|
* If there are no such component, we destroy everything. This is better than
|
|
* being in a corrupted state.
|
|
*/
|
|
handleError(error: Error) {
|
|
let component = this.component;
|
|
this.vnode = component.__owl__.vnode || h("div");
|
|
|
|
const qweb = component.env.qweb;
|
|
let root = component;
|
|
|
|
function handle(error) {
|
|
let canCatch = false;
|
|
qweb.trigger("error", error);
|
|
while (component && !(canCatch = !!component.catchError)) {
|
|
root = component;
|
|
component = component.__owl__.parent!;
|
|
}
|
|
if (canCatch) {
|
|
try {
|
|
component.catchError!(error);
|
|
} catch (e) {
|
|
root = component;
|
|
component = component.__owl__.parent!;
|
|
return handle(e);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let isHandled = handle(error);
|
|
|
|
if (!isHandled) {
|
|
// the 3 next lines aim to mark the root fiber as being in error, and
|
|
// to force it to end, without waiting for its children
|
|
this.root.counter = 0;
|
|
this.root.error = error;
|
|
scheduler.flush();
|
|
// at this point, the state of the application is corrupted and we could
|
|
// have a lot of issues or crashes. So we destroy the application in a try
|
|
// catch and swallow these errors because the fiber is already in error,
|
|
// and this is the actual issue that needs to be solved, not those followup
|
|
// errors.
|
|
try {
|
|
root.destroy();
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
}
|