mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
6950f8e628
Currently in some cases, adding an attachment via lognote creates a traceback. Error : shouldPatch is true while `vnode` is not defined, so `patch()` failed This is a hotfix correcting this problem by calling `patch()` only if `shouldPatch` is true **and** the `vnode` is set. opw-2645203
367 lines
12 KiB
TypeScript
367 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) {
|
|
f.component.__owl__.currentFiber = null;
|
|
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 {
|
|
const vnode = component.__owl__.vnode;
|
|
if (fiber.shouldPatch && vnode) {
|
|
component.__patch(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {}
|
|
}
|
|
}
|
|
}
|