mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[REF] component: large cleanup of concurrency branch
We remove here old comments, add some tests and documentation, and in general, make sure the state of the code is in a good shape part of #330
This commit is contained in:
+29
-37
@@ -1,10 +1,10 @@
|
||||
import { Observer } from "../core/observer";
|
||||
import { CompiledTemplate, QWeb } from "../qweb/index";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import { Fiber } from "./fiber";
|
||||
import { scheduler } from "./scheduler";
|
||||
import "./directive";
|
||||
import { Fiber } from "./fiber";
|
||||
import "./props_validation";
|
||||
import { Scheduler } from "./scheduler";
|
||||
|
||||
/**
|
||||
* Owl Component System
|
||||
@@ -20,6 +20,8 @@ import "./props_validation";
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
const raf = window.requestAnimationFrame.bind(window);
|
||||
export const scheduler = new Scheduler(raf);
|
||||
|
||||
/**
|
||||
* An Env (environment) is an object that will be (mostly) shared between all
|
||||
@@ -62,7 +64,7 @@ interface Internal<T extends Env, Props> {
|
||||
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer: Observer | null;
|
||||
render: CompiledTemplate;
|
||||
renderFn: CompiledTemplate;
|
||||
mountedCB: Function | null;
|
||||
willUnmountCB: Function | null;
|
||||
willPatchCB: Function | null;
|
||||
@@ -179,7 +181,7 @@ export class Component<T extends Env, Props extends {}> {
|
||||
willStartCB: null,
|
||||
willUpdatePropsCB: null,
|
||||
observer: null,
|
||||
render: qweb.render.bind(qweb, this.__getTemplate(qweb)),
|
||||
renderFn: qweb.render.bind(qweb, this.__getTemplate(qweb)),
|
||||
classObj: null,
|
||||
refs: null
|
||||
};
|
||||
@@ -280,41 +282,31 @@ export class Component<T extends Env, Props extends {}> {
|
||||
if (__owl__.isMounted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
|
||||
if (!__owl__.vnode) {
|
||||
this.__prepareAndRender(fiber);
|
||||
return new Promise(resolve => {
|
||||
scheduler.addFiber(fiber, () => {
|
||||
if (!__owl__.isDestroyed) {
|
||||
this.__patch(fiber.vnode);
|
||||
target.appendChild(this.el!);
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else if (renderBeforeRemount) {
|
||||
this.__render(fiber);
|
||||
return new Promise(resolve => {
|
||||
scheduler.addFiber(fiber, () => {
|
||||
if (!__owl__.isDestroyed) {
|
||||
this.__patch(fiber.vnode);
|
||||
target.appendChild(this.el!);
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (__owl__.vnode && !renderBeforeRemount) {
|
||||
target.appendChild(this.el!);
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
|
||||
if (!__owl__.vnode) {
|
||||
this.__prepareAndRender(fiber);
|
||||
} else {
|
||||
this.__render(fiber);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
scheduler.addFiber(fiber, () => {
|
||||
if (!__owl__.isDestroyed) {
|
||||
this.__patch(fiber.vnode);
|
||||
target.appendChild(this.el!);
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +342,7 @@ export class Component<T extends Env, Props extends {}> {
|
||||
return new Promise(resolve => {
|
||||
scheduler.addFiber(fiber.root, () => {
|
||||
if (__owl__.isMounted && fiber === fiber.root) {
|
||||
fiber.__applyPatchQueue();
|
||||
fiber.patchComponents();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
@@ -588,7 +580,7 @@ export class Component<T extends Env, Props extends {}> {
|
||||
}
|
||||
let vnode;
|
||||
try {
|
||||
vnode = __owl__.render!(this, {
|
||||
vnode = __owl__.renderFn!(this, {
|
||||
handlers: __owl__.boundHandlers,
|
||||
fiber: fiber
|
||||
});
|
||||
@@ -674,7 +666,7 @@ Fiber.prototype.handleError = function(error) {
|
||||
* If there are no such component, we destroy everything. This is better than
|
||||
* being in a corrupted state.
|
||||
*/
|
||||
export function errorHandler(error: Error, fiber: Fiber) {
|
||||
function errorHandler(error: Error, fiber: Fiber) {
|
||||
let canCatch = false;
|
||||
let component = fiber.component;
|
||||
let qweb = component.env.qweb;
|
||||
|
||||
+65
-60
@@ -240,7 +240,6 @@ QWeb.addDirective({
|
||||
// want to evaluate it only once)
|
||||
ctx.addLine(`let key${keyID} = 'key' + ${key};`);
|
||||
}
|
||||
ctx.addLine(`let def${defID};`);
|
||||
|
||||
let locationExpr = `\`__${ctx.generateID()}__`;
|
||||
for (let i = 0; i < ctx.loopNumber - 1; i++) {
|
||||
@@ -376,7 +375,69 @@ QWeb.addDirective({
|
||||
ctx.addLine(`w${componentID} = false;`);
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addIf(`!w${componentID}`);
|
||||
let registerCode = "";
|
||||
if (shouldProxy) {
|
||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||
}
|
||||
|
||||
// SLOTS
|
||||
const varDefs: string[] = [];
|
||||
const hasSlots = node.childNodes.length;
|
||||
if (hasSlots) {
|
||||
ctx.rootContext.shouldTrackScope = true;
|
||||
for (let v of Object.values(ctx.variables)) {
|
||||
if (v["id"]) {
|
||||
varDefs.push(v["id"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let scopeVars;
|
||||
if (hasSlots) {
|
||||
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
|
||||
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
|
||||
scopeVars = `${scope}, ${vars}`;
|
||||
} else {
|
||||
scopeVars = "undefined, undefined";
|
||||
}
|
||||
|
||||
ctx.addIf(`w${componentID}`);
|
||||
|
||||
// need to update component
|
||||
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
|
||||
if (keepAlive) {
|
||||
// if we have t-keepalive="1", the component could be unmounted, but then
|
||||
// we __updateProps is called. This is ok, but we do not want to call
|
||||
// the willPatch/patched hooks of the component in this case, so we
|
||||
// disable the patch queue
|
||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||
}
|
||||
let styleCode = "";
|
||||
if (tattStyle) {
|
||||
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
|
||||
", " + scopeVars}, sibling)${styleCode};`
|
||||
);
|
||||
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
|
||||
let keepAliveCode = "";
|
||||
if (keepAlive) {
|
||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
|
||||
ctx.addLine(keepAliveCode);
|
||||
}
|
||||
if (registerCode) {
|
||||
ctx.addLine(registerCode);
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||
}
|
||||
|
||||
ctx.addElse();
|
||||
|
||||
// new component
|
||||
let dynamicFallback = "";
|
||||
if (!value.match(INTERP_REGEXP)) {
|
||||
@@ -395,17 +456,7 @@ QWeb.addDirective({
|
||||
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
|
||||
ctx.addLine(`parent.__owl__.cmap[${templateId}] = w${componentID}.__owl__.id;`);
|
||||
|
||||
// SLOTS
|
||||
const varDefs: string[] = [];
|
||||
const hasSlots = node.childNodes.length;
|
||||
if (hasSlots) {
|
||||
ctx.rootContext.shouldTrackScope = true;
|
||||
for (let v of Object.values(ctx.variables)) {
|
||||
if (v["id"]) {
|
||||
varDefs.push(v["id"]);
|
||||
}
|
||||
}
|
||||
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
const slotNodes = clone.querySelectorAll("[t-set]");
|
||||
const slotId = QWeb.nextSlotId++;
|
||||
@@ -430,20 +481,8 @@ QWeb.addDirective({
|
||||
}
|
||||
}
|
||||
|
||||
let scopeVars;
|
||||
if (hasSlots) {
|
||||
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
|
||||
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
|
||||
scopeVars = `${scope}, ${vars}`;
|
||||
} else {
|
||||
scopeVars = "undefined, undefined";
|
||||
}
|
||||
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
|
||||
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars}, sibling);`);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
let registerCode = "";
|
||||
if (shouldProxy) {
|
||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`let pvnode = h('dummy', {key: ${templateId}, hook: {insert(vn) { let nvn=w${componentID}.__mount(fiber, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});`
|
||||
);
|
||||
@@ -459,47 +498,13 @@ QWeb.addDirective({
|
||||
}
|
||||
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
|
||||
|
||||
ctx.addElse();
|
||||
// need to update component
|
||||
let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber";
|
||||
if (keepAlive) {
|
||||
// if we have t-keepalive="1", the component could be unmounted, but then
|
||||
// we __updateProps is called. This is ok, but we do not want to call
|
||||
// the willPatch/patched hooks of the component in this case, so we
|
||||
// disable the patch queue
|
||||
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
|
||||
}
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
|
||||
", " + scopeVars}, sibling);`
|
||||
);
|
||||
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
|
||||
let keepAliveCode = "";
|
||||
if (keepAlive) {
|
||||
keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
|
||||
ctx.addLine(keepAliveCode);
|
||||
}
|
||||
if (registerCode) {
|
||||
ctx.addLine(registerCode);
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||
}
|
||||
if (tattStyle) {
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`
|
||||
);
|
||||
}
|
||||
ctx.closeIf();
|
||||
|
||||
if (classObj) {
|
||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||
}
|
||||
|
||||
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber;`);
|
||||
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber || sibling;`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+45
-9
@@ -1,17 +1,46 @@
|
||||
import { VNode } from "../vdom/index";
|
||||
import { Component } from "./component";
|
||||
|
||||
/**
|
||||
* Owl Fiber Class
|
||||
*
|
||||
* Fibers are small abstractions designed to contain all the internal state
|
||||
* associated to a "rendering work unit", relative to a specific component.
|
||||
* 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 {
|
||||
// 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;
|
||||
|
||||
// isCancelled means that the rendering corresponding to this fiber and its
|
||||
// children is cancelled. No extra work should be done.
|
||||
isCancelled: 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;
|
||||
|
||||
scope: any;
|
||||
vars: any;
|
||||
props: any;
|
||||
@@ -24,8 +53,6 @@ export class Fiber {
|
||||
sibling: Fiber | null = null;
|
||||
parent: Fiber | null = null;
|
||||
|
||||
counter: number = 0;
|
||||
|
||||
constructor(parent: Fiber | null, component: Component<any, any>, props, scope, vars, force) {
|
||||
this.force = force;
|
||||
this.scope = scope;
|
||||
@@ -38,7 +65,7 @@ export class Fiber {
|
||||
|
||||
let oldFiber = component.__owl__.currentFiber;
|
||||
if (oldFiber && !oldFiber.isCancelled) {
|
||||
this.__remapFiber(oldFiber);
|
||||
this._remapFiber(oldFiber);
|
||||
}
|
||||
|
||||
this.root.counter++;
|
||||
@@ -46,7 +73,13 @@ export class Fiber {
|
||||
component.__owl__.currentFiber = this;
|
||||
}
|
||||
|
||||
__remapFiber(oldFiber: Fiber) {
|
||||
/**
|
||||
* 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();
|
||||
if (oldFiber === oldFiber.root) {
|
||||
oldFiber.root.counter++;
|
||||
@@ -75,7 +108,7 @@ export class Fiber {
|
||||
* 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) {
|
||||
_walk(doWork: (f: Fiber) => Fiber | null) {
|
||||
let root = this;
|
||||
let current: Fiber = this;
|
||||
while (true) {
|
||||
@@ -103,7 +136,7 @@ export class Fiber {
|
||||
* 2) Call '__patch' on the component of each patch
|
||||
* 3) Call 'patched' on the component of each patch, in reverse order
|
||||
*/
|
||||
__applyPatchQueue() {
|
||||
patchComponents() {
|
||||
const patchQueue: Fiber[] = [];
|
||||
const doWork: (Fiber) => Fiber | null = function(f) {
|
||||
if (f.shouldPatch) {
|
||||
@@ -111,7 +144,7 @@ export class Fiber {
|
||||
}
|
||||
return f.child;
|
||||
};
|
||||
this.__walk(doWork);
|
||||
this._walk(doWork);
|
||||
let component: Component<any, any> = this.component;
|
||||
this.shouldPatch = false;
|
||||
const patchLen = patchQueue.length;
|
||||
@@ -149,8 +182,11 @@ export class Fiber {
|
||||
this.shouldPatch = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a fiber and all its children.
|
||||
*/
|
||||
cancel() {
|
||||
this.__walk(f => {
|
||||
this._walk(f => {
|
||||
if (!f.isRendered) {
|
||||
f.root.counter--;
|
||||
}
|
||||
|
||||
+36
-19
@@ -1,14 +1,29 @@
|
||||
import { Fiber } from "./fiber";
|
||||
|
||||
// scheduler
|
||||
/**
|
||||
* Owl Scheduler Class
|
||||
*
|
||||
* The scheduler is the part of Owl that will effectively apply a rendering
|
||||
* whenever a fiber is ready.
|
||||
*
|
||||
* Briefly, it can be used to register root fibers. Whenever there is an
|
||||
* active root fiber, it will poll continuously each animation frame (so, about
|
||||
* once every 16ms) and whenever a root fiber is ready, it will apply it.
|
||||
*/
|
||||
|
||||
interface Task {
|
||||
fiber: Fiber;
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
export const scheduler = {
|
||||
tasks: [] as Task[],
|
||||
isRunning: false,
|
||||
export class Scheduler {
|
||||
tasks: Task[] = [];
|
||||
isRunning: boolean = false;
|
||||
requestAnimationFrame: typeof window.requestAnimationFrame;
|
||||
|
||||
constructor(requestAnimationFrame) {
|
||||
this.requestAnimationFrame = requestAnimationFrame;
|
||||
}
|
||||
|
||||
addFiber(fiber, callback) {
|
||||
this.tasks.push({ fiber, callback });
|
||||
@@ -16,7 +31,12 @@ export const scheduler = {
|
||||
return;
|
||||
}
|
||||
this.scheduleTasks();
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all current tasks. This only applies to the fibers that are ready.
|
||||
* Other tasks are left unchanged.
|
||||
*/
|
||||
flush() {
|
||||
let tasks = this.tasks;
|
||||
this.tasks = [];
|
||||
@@ -31,20 +51,17 @@ export const scheduler = {
|
||||
return true;
|
||||
});
|
||||
this.tasks = tasks.concat(this.tasks);
|
||||
},
|
||||
processTasks() {
|
||||
this.flush();
|
||||
if (this.tasks.length > 0) {
|
||||
this.scheduleTasks();
|
||||
} else {
|
||||
this.isRunning = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
scheduleTasks() {
|
||||
this.isRunning = true;
|
||||
this.requestAnimationFrame(() => this.processTasks());
|
||||
},
|
||||
|
||||
requestAnimationFrame: requestAnimationFrame.bind(window)
|
||||
};
|
||||
this.requestAnimationFrame(() => {
|
||||
this.flush();
|
||||
if (this.tasks.length > 0) {
|
||||
this.scheduleTasks();
|
||||
} else {
|
||||
this.isRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user