mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
2529aa3ef2
Here is a situation that can happen in some complicated case: 1. a parent component is rendered, which includes some children 2. it is then willPatched 3. the sub components are then mounted/willUnmounted 4. because of complicated business logic, this causes the parent component to be rerendered (before parent "patched" method is called) 5. owl will internally reset its currentfiber to null (but there is a pending rendering!) 6. subsequent rendering will ignore pending rendering 7. havoc ensues This is actually one of the reason why modifying a component state in a willPatch component is actually not a good idea. However, the good news is that this specific situation can be properly handled: we can simply make sure that we do not reset currentFiber to null if there is a new pending rendering. closes #728
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
import { h, VNode } from "../vdom/index";
|
|
import { Component, MountPosition } 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) {
|
|
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.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;
|
|
if (!this.target && !component.__owl__.isMounted) {
|
|
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
|
|
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;
|
|
}
|
|
}
|
|
if (fiber === component.__owl__.currentFiber) {
|
|
component.__owl__.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
|
|
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 if (this.target ? inDOM : true) {
|
|
component.__callMounted();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
let canCatch = false;
|
|
while (component && !(canCatch = !!component.catchError)) {
|
|
root = component;
|
|
component = component.__owl__.parent!;
|
|
}
|
|
qweb.trigger("error", error);
|
|
|
|
if (canCatch) {
|
|
component.catchError!(error);
|
|
} else {
|
|
// 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();
|
|
root.destroy();
|
|
}
|
|
}
|
|
}
|