[FIX] component: concurrent calls to mount and render

Closes #450
This commit is contained in:
Aaron Bohy
2019-11-05 14:23:06 +01:00
committed by Géry Debongnie
parent bbdc9d90d7
commit e5940b4b6b
11 changed files with 292 additions and 112 deletions
+29 -53
View File
@@ -1,6 +1,6 @@
import { Observer } from "../core/observer";
import { CompiledTemplate, QWeb } from "../qweb/index";
import { h, patch, VNode } from "../vdom/index";
import { patch, VNode } from "../vdom/index";
import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
@@ -282,32 +282,19 @@ export class Component<T extends Env, Props extends {}> {
return Promise.resolve();
}
if (!(target instanceof HTMLElement)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
let message = `Component '${
this.constructor.name
}' cannot be mounted: the target is not a valid DOM node.`;
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
return new Promise((resolve, reject) => {
const fiber = new Fiber(null, this, undefined, undefined, false);
scheduler.addFiber(fiber, err => {
if (err) {
reject(err);
return;
}
if (!__owl__.isDestroyed) {
this.__patch(fiber.vnode);
target.appendChild(this.el!);
if (document.body.contains(target)) {
this.__callMounted();
}
}
resolve();
});
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
});
const fiber = new Fiber(null, this, undefined, undefined, false, target);
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
} else {
this.__render(fiber);
}
return scheduler.addFiber(fiber);
}
/**
@@ -332,26 +319,18 @@ export class Component<T extends Env, Props extends {}> {
*/
async render(force: boolean = false): Promise<void> {
const __owl__ = this.__owl__;
if (
(!__owl__.isMounted && !__owl__.currentFiber) ||
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)
) {
if (!__owl__.isMounted && !__owl__.currentFiber) {
// if we get here, this means that the component was either never mounted,
// or was unmounted and some state change triggered a render. Either way,
// we do not want to actually render anything in this case.
return;
}
return new Promise((resolve, reject) => {
const fiber = new Fiber(null, this, undefined, undefined, force);
scheduler.addFiber(fiber.root, err => {
if (err) {
reject(err);
return;
}
if (__owl__.isMounted && fiber === fiber.root) {
fiber.patchComponents();
}
resolve();
});
this.__render(fiber);
});
if (__owl__.currentFiber && !__owl__.currentFiber.isRendered) {
return scheduler.addFiber(__owl__.currentFiber.root);
}
const fiber = new Fiber(null, this, undefined, undefined, force, null);
this.__render(fiber);
return scheduler.addFiber(fiber);
}
/**
@@ -437,7 +416,7 @@ export class Component<T extends Env, Props extends {}> {
__owl__.isDestroyed = true;
delete __owl__.vnode;
if (__owl__.currentFiber) {
__owl__.currentFiber.isCancelled = true;
__owl__.currentFiber.isCompleted = true;
}
}
@@ -492,7 +471,7 @@ export class Component<T extends Env, Props extends {}> {
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
if (!parentFiber.child) {
parentFiber.child = fiber;
} else {
@@ -510,7 +489,7 @@ export class Component<T extends Env, Props extends {}> {
this.willUpdateProps(nextProps),
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
]);
if (fiber.isCancelled) {
if (fiber.isCompleted) {
return;
}
this.props = nextProps;
@@ -535,7 +514,7 @@ export class Component<T extends Env, Props extends {}> {
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force);
const fiber = new Fiber(parentFiber, this, scope, vars, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
parentFiber.child = fiber;
@@ -569,13 +548,12 @@ export class Component<T extends Env, Props extends {}> {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
fiber.handleError(e);
fiber.vnode = h("div"); // -> we render this div at the end
return Promise.resolve();
}
if (this.__owl__.isDestroyed) {
return Promise.resolve();
}
if (!fiber.isCancelled) {
if (!fiber.isCompleted) {
this.__render(fiber);
}
}
@@ -585,17 +563,14 @@ export class Component<T extends Env, Props extends {}> {
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
let vnode;
try {
vnode = __owl__.renderFn!(this, {
fiber.vnode = __owl__.renderFn!(this, {
handlers: __owl__.boundHandlers,
fiber: fiber
});
} catch (e) {
vnode = __owl__.vnode || h("div");
fiber.handleError(e);
}
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
@@ -604,7 +579,8 @@ export class Component<T extends Env, Props extends {}> {
// template (so, something like <MyComponent class="..."/>) to the actual
// root vnode
if (__owl__.classObj) {
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
const data = fiber.vnode!.data!;
data.class = Object.assign(data.class || {}, __owl__.classObj);
}
fiber.root.counter--;
fiber.isRendered = true;
+1 -1
View File
@@ -455,7 +455,7 @@ QWeb.addDirective({
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () {if (fiber.isCancelled) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
`def${defID}.then(function () {if (fiber.isCompleted) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
+65 -13
View File
@@ -1,4 +1,4 @@
import { VNode } from "../vdom/index";
import { h, VNode } from "../vdom/index";
import { Component } from "./component";
import { scheduler } from "./scheduler";
@@ -20,9 +20,10 @@ export class Fiber {
// method potentially implemented by a component. It is usually set to false.
force: boolean;
// isCancelled means that the rendering corresponding to this fiber and its
// children is cancelled. No extra work should be done.
isCancelled: boolean = false;
// 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,
@@ -42,6 +43,8 @@ export class Fiber {
// scheduler.
counter: number = 0;
target: HTMLElement | null;
scope: any;
vars: any;
@@ -55,18 +58,25 @@ export class Fiber {
error?: Error;
constructor(parent: Fiber | null, component: Component<any, any>, scope, vars, force) {
constructor(parent: Fiber | null, component: Component<any, any>, scope, vars, force, target) {
this.force = force;
this.scope = scope;
this.vars = vars;
this.component = component;
this.target = target;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = component.__owl__.currentFiber;
if (oldFiber && !oldFiber.isCancelled) {
this._remapFiber(oldFiber);
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++;
@@ -74,6 +84,25 @@ export class Fiber {
component.__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
if (oldFiber.child) {
// remove relation to children
oldFiber.child.parent = null;
oldFiber.child = null;
}
oldFiber.counter = 1; // re-initialize counter
}
/**
* 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
@@ -83,7 +112,7 @@ export class Fiber {
_remapFiber(oldFiber: Fiber) {
oldFiber.cancel();
if (oldFiber === oldFiber.root) {
oldFiber.root.counter++;
oldFiber.counter++;
}
if (oldFiber.parent && !this.parent) {
// re-map links
@@ -132,7 +161,28 @@ export class Fiber {
}
/**
* Apply the given patch queue from a fiber.
* 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() {
const component = this.component;
if (this.target) {
component.__patch(this.vnode);
this.target.appendChild(component.el!);
if (document.body.contains(this.target)) {
component.__callMounted();
}
} else {
if (component.__owl__.isMounted && this === this.root) {
this.patchComponents();
}
}
this.isCompleted = true;
}
/**
* Compute and apply the patch queue of the fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in reverse order
@@ -142,8 +192,8 @@ export class Fiber {
const doWork: (Fiber) => Fiber | null = function(f) {
if (f.shouldPatch) {
patchQueue.push(f);
return f.child;
}
return f.child;
};
this._walk(doWork);
let component: Component<any, any> = this.component;
@@ -186,7 +236,7 @@ export class Fiber {
if (!f.isRendered) {
f.root.counter--;
}
f.isCancelled = true;
f.isCompleted = true;
return f.child;
});
}
@@ -200,10 +250,12 @@ export class Fiber {
* being in a corrupted state.
*/
handleError(error: Error) {
let canCatch = false;
let component = this.component;
let qweb = component.env.qweb;
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!;
+25 -8
View File
@@ -25,12 +25,25 @@ export class Scheduler {
this.requestAnimationFrame = requestAnimationFrame;
}
addFiber(fiber, callback) {
this.tasks.push({ fiber, callback });
if (this.isRunning) {
return;
}
this.scheduleTasks();
addFiber(fiber): Promise<void> {
return new Promise((resolve, reject) => {
if (fiber.error) {
return reject(fiber.error);
}
this.tasks.push({
fiber,
callback: () => {
if (fiber.error) {
reject(fiber.error);
return;
}
resolve();
}
});
if (!this.isRunning) {
this.scheduleTasks();
}
});
}
/**
@@ -41,11 +54,15 @@ export class Scheduler {
let tasks = this.tasks;
this.tasks = [];
tasks = tasks.filter(task => {
if (task.fiber.isCancelled) {
if (task.fiber.isCompleted) {
task.callback();
return false;
}
if (task.fiber.counter === 0) {
task.callback(task.fiber.error);
if (!task.fiber.error) {
task.fiber.complete();
}
task.callback();
return false;
}
return true;
+11 -1
View File
@@ -24,9 +24,19 @@ import { Observer } from "./core/observer";
export function useState<T>(state: T): T {
const component: Component<any, any> = Component.current!;
const __owl__ = component.__owl__;
const renderFn = __owl__.renderFn;
let lastRenderRevNumber;
__owl__.renderFn = function(comp, params) {
lastRenderRevNumber = __owl__.observer!.rev;
return renderFn(comp, params);
};
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
__owl__.observer.notifyCB = () => {
if (lastRenderRevNumber < __owl__.observer!.rev) {
component.render();
}
};
}
return __owl__.observer.observe(state);
}
+6 -2
View File
@@ -236,7 +236,9 @@ QWeb.addDirective({
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
}
@@ -305,7 +307,9 @@ QWeb.addDirective({
!node.children[0].hasAttribute("t-key");
if (shouldWarn) {
console.warn(
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
`Directive t-foreach should always be used with a t-key! (in template: '${
ctx.templateName
}')`
);
}