mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component: refactor rendering pipeline
This commit introduces a brand new rendering system based on a fiber class and a scheduler. closes #330
This commit is contained in:
committed by
Géry Debongnie
parent
2bed1cfbd1
commit
9c5cad15c1
+105
-144
@@ -1,6 +1,8 @@
|
||||
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 "./props_validation";
|
||||
|
||||
@@ -11,7 +13,6 @@ import "./props_validation";
|
||||
* contains:
|
||||
*
|
||||
* - the Env interface (generic type for the environment)
|
||||
* - the Fiber interface (owl metadata attached to a rendering)
|
||||
* - the Internal interface (the owl specific metadata attached to a component)
|
||||
* - the Component class
|
||||
*/
|
||||
@@ -34,27 +35,6 @@ export interface Env {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fibers are small abstractions designed to contain all the internal state
|
||||
* associated to a "rendering work unit", relative to a specific component.
|
||||
*
|
||||
* A rendering will cause the creation of a fiber for each impacted components.
|
||||
*/
|
||||
export interface Fiber<Props> {
|
||||
force: boolean;
|
||||
rootFiber: Fiber<any> | null;
|
||||
isCancelled: boolean;
|
||||
scope: any;
|
||||
vars: any;
|
||||
patchQueue: Fiber<any>[];
|
||||
component: Component<any, any>;
|
||||
vnode: VNode | null;
|
||||
props: Props;
|
||||
promise: Promise<VNode> | null;
|
||||
// handlers?: any;
|
||||
// mountedHandlers?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly an internal detail of implementation. The Meta interface is
|
||||
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||
@@ -65,6 +45,7 @@ interface Internal<T extends Env, Props> {
|
||||
// relationships
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
pvnode: VNode | null;
|
||||
isMounted: boolean;
|
||||
isDestroyed: boolean;
|
||||
|
||||
@@ -77,7 +58,7 @@ interface Internal<T extends Env, Props> {
|
||||
// the component instance back whenever the template is rerendered.
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
currentFiber: Fiber<Props> | null;
|
||||
currentFiber: Fiber | null;
|
||||
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer: Observer | null;
|
||||
@@ -183,6 +164,7 @@ export class Component<T extends Env, Props extends {}> {
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
vnode: null,
|
||||
pvnode: null,
|
||||
isMounted: false,
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
@@ -296,27 +278,42 @@ export class Component<T extends Env, Props extends {}> {
|
||||
async mount(target: HTMLElement, renderBeforeRemount: boolean = false): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isMounted) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
const fiber = this.__createFiber(false, undefined, undefined, undefined);
|
||||
const fiber = new Fiber(null, this, this.props, undefined, undefined, false);
|
||||
if (!__owl__.vnode) {
|
||||
fiber.promise = this.__prepareAndRender(fiber);
|
||||
const vnode = await fiber.promise;
|
||||
if (__owl__.isDestroyed) {
|
||||
// component was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this.__patch(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) {
|
||||
fiber.patchQueue.push(fiber);
|
||||
fiber.promise = this.__render(fiber);
|
||||
await fiber.promise;
|
||||
this.__applyPatchQueue(fiber);
|
||||
}
|
||||
target.appendChild(this.el!);
|
||||
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
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 {
|
||||
target.appendChild(this.el!);
|
||||
if (document.body.contains(target)) {
|
||||
this.__callMounted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,19 +339,22 @@ export class Component<T extends Env, Props extends {}> {
|
||||
*/
|
||||
async render(force: boolean = false): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
if (!__owl__.isMounted) {
|
||||
if (
|
||||
(!__owl__.isMounted && !__owl__.currentFiber) ||
|
||||
(__owl__.currentFiber && !__owl__.currentFiber.isRendered)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const fiber = this.__createFiber(force, undefined, undefined, undefined);
|
||||
fiber.patchQueue.push(fiber);
|
||||
fiber.promise = this.__render(fiber);
|
||||
await fiber.promise;
|
||||
|
||||
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
|
||||
// we only update the vnode and the actual DOM if no other rendering
|
||||
// occurred between now and when the render method was initially called.
|
||||
this.__applyPatchQueue(fiber);
|
||||
}
|
||||
const fiber = new Fiber(null, this, this.props, undefined, undefined, force);
|
||||
this.__render(fiber);
|
||||
return new Promise(resolve => {
|
||||
scheduler.addFiber(fiber.root, () => {
|
||||
if (__owl__.isMounted && fiber === fiber.root) {
|
||||
fiber.__applyPatchQueue();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,27 +407,6 @@ export class Component<T extends Env, Props extends {}> {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is a helper to create a fiber element.
|
||||
*/
|
||||
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
|
||||
const fiber: Fiber<Props> = {
|
||||
force,
|
||||
scope,
|
||||
vars,
|
||||
rootFiber: null,
|
||||
isCancelled: false,
|
||||
component: this,
|
||||
vnode: null,
|
||||
patchQueue: parent ? parent.patchQueue : [],
|
||||
props: this.props,
|
||||
promise: null
|
||||
};
|
||||
fiber.rootFiber = parent ? parent.rootFiber : fiber;
|
||||
this.__owl__.currentFiber = fiber;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to perform a full destroy, from the point of view of an Owl
|
||||
* component. It does not remove the el (this is done only once on the top
|
||||
@@ -478,7 +457,7 @@ export class Component<T extends Env, Props extends {}> {
|
||||
__owl__.mountedCB();
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
console.error(e); // TODO : add a test
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,25 +483,35 @@ export class Component<T extends Env, Props extends {}> {
|
||||
*/
|
||||
async __updateProps(
|
||||
nextProps: Props,
|
||||
parentFiber: Fiber<any>,
|
||||
scope?: any,
|
||||
vars?: any
|
||||
parentFiber: Fiber,
|
||||
scope: any,
|
||||
vars: any,
|
||||
previousSibling?: Fiber | null
|
||||
): Promise<void> {
|
||||
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const __owl__ = this.__owl__;
|
||||
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
|
||||
if (!parentFiber.child) {
|
||||
parentFiber.child = fiber;
|
||||
} else {
|
||||
previousSibling!.sibling = fiber;
|
||||
}
|
||||
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
nextProps = this.__applyDefaultProps(nextProps, defaultProps);
|
||||
}
|
||||
await Promise.all([
|
||||
this.willUpdateProps(nextProps),
|
||||
this.__owl__.willUpdatePropsCB && this.__owl__.willUpdatePropsCB(nextProps)
|
||||
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps)
|
||||
]);
|
||||
if (fiber.isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.props = nextProps;
|
||||
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||
fiber.patchQueue.push(fiber);
|
||||
|
||||
await this.__render(fiber);
|
||||
this.__render(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +523,7 @@ export class Component<T extends Env, Props extends {}> {
|
||||
const __owl__ = this.__owl__;
|
||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||
__owl__.vnode = patch(target, vnode);
|
||||
__owl__.currentFiber = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -541,10 +531,15 @@ export class Component<T extends Env, Props extends {}> {
|
||||
* subcomponent is created. It gets its scope and vars, if any, from the
|
||||
* parent template.
|
||||
*/
|
||||
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
|
||||
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
|
||||
fiber.promise = this.__prepareAndRender(fiber);
|
||||
return fiber.promise;
|
||||
__prepare(parentFiber: Fiber, scope: any, vars: any, previousSibling?: Fiber | null) {
|
||||
const fiber = new Fiber(parentFiber, this, this.props, scope, vars, parentFiber.force);
|
||||
fiber.shouldPatch = false;
|
||||
if (!parentFiber.child) {
|
||||
parentFiber.child = fiber;
|
||||
} else {
|
||||
previousSibling!.sibling = fiber;
|
||||
}
|
||||
return this.__prepareAndRender(fiber);
|
||||
}
|
||||
|
||||
__getTemplate(qweb: QWeb): string {
|
||||
@@ -570,68 +565,66 @@ export class Component<T extends Env, Props extends {}> {
|
||||
}
|
||||
return p._template;
|
||||
}
|
||||
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
|
||||
async __prepareAndRender(fiber: Fiber) {
|
||||
try {
|
||||
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
|
||||
} catch (e) {
|
||||
errorHandler(e, this);
|
||||
return Promise.resolve(h("div"));
|
||||
errorHandler(e, fiber);
|
||||
fiber.vnode = h("div"); // -> we render this div at the end
|
||||
return Promise.resolve();
|
||||
}
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
if (this.__owl__.isDestroyed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!fiber.isCancelled) {
|
||||
this.__render(fiber);
|
||||
}
|
||||
return this.__render(fiber);
|
||||
}
|
||||
|
||||
__render(fiber: Fiber<Props>): Promise<VNode> {
|
||||
__render(fiber: Fiber) {
|
||||
const __owl__ = this.__owl__;
|
||||
const promises: Promise<void>[] = [];
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode;
|
||||
try {
|
||||
vnode = __owl__.render!(this, {
|
||||
promises,
|
||||
handlers: __owl__.boundHandlers,
|
||||
fiber: fiber
|
||||
});
|
||||
} catch (e) {
|
||||
vnode = __owl__.vnode || h("div");
|
||||
errorHandler(e, this);
|
||||
errorHandler(e, fiber);
|
||||
}
|
||||
fiber.vnode = vnode;
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
// tricky part is that a child component can be rerendered on its own, which
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent component. With this, we make sure that the parent component will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = __owl__.id;
|
||||
|
||||
// we applly here the class information described on the component by the
|
||||
// we apply here the class information described on the component by the
|
||||
// template (so, something like <MyComponent class="..."/>) to the actual
|
||||
// root vnode
|
||||
if (__owl__.classObj) {
|
||||
vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => vnode);
|
||||
fiber.root.counter--;
|
||||
fiber.isRendered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-component directive
|
||||
*/
|
||||
__mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
__mount(fiber: Fiber, elm: HTMLElement): VNode {
|
||||
if (fiber !== this.__owl__.currentFiber) {
|
||||
fiber = this.__owl__.currentFiber!; // TODO: check if we can remove fiber arg
|
||||
}
|
||||
const vnode = fiber.vnode!;
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.classObj) {
|
||||
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, __owl__.classObj);
|
||||
}
|
||||
__owl__.vnode = patch(elm, vnode);
|
||||
__owl__.currentFiber = null;
|
||||
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
|
||||
this.__callMounted();
|
||||
}
|
||||
@@ -664,47 +657,15 @@ export class Component<T extends Env, Props extends {}> {
|
||||
}
|
||||
return <Props>props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given patch queue from a 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
|
||||
*/
|
||||
__applyPatchQueue(fiber: Fiber<Props>) {
|
||||
const patchQueue = fiber.patchQueue;
|
||||
let component: Component<any, any> = this;
|
||||
try {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
component = patchQueue[i].component;
|
||||
if (component.__owl__.willPatchCB) {
|
||||
component.__owl__.willPatchCB();
|
||||
}
|
||||
component.willPatch();
|
||||
}
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.__patch(fiber.vnode);
|
||||
}
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
component = patchQueue[i].component;
|
||||
component.patched();
|
||||
if (component.__owl__.patchedCB) {
|
||||
component.__owl__.patchedCB();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler(e, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Error handling
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
Fiber.prototype.handleError = function(error) {
|
||||
errorHandler(error, this);
|
||||
};
|
||||
/**
|
||||
* This is the global error handler for errors occurring in Owl main lifecycle
|
||||
* methods. Caught errors are triggered on the QWeb instance, and are
|
||||
@@ -713,8 +674,9 @@ export class Component<T extends Env, Props extends {}> {
|
||||
* If there are no such component, we destroy everything. This is better than
|
||||
* being in a corrupted state.
|
||||
*/
|
||||
function errorHandler(error: Error, component: Component<any, any>) {
|
||||
export function errorHandler(error: Error, fiber: Fiber) {
|
||||
let canCatch = false;
|
||||
let component = fiber.component;
|
||||
let qweb = component.env.qweb;
|
||||
let root = component;
|
||||
while (component && !(canCatch = component.catchError !== Component.prototype.catchError)) {
|
||||
@@ -722,7 +684,6 @@ function errorHandler(error: Error, component: Component<any, any>) {
|
||||
component = component.__owl__.parent!;
|
||||
}
|
||||
console.error(error);
|
||||
// we trigger error on QWeb so it can be logged/handled
|
||||
qweb.trigger("error", error);
|
||||
|
||||
if (canCatch) {
|
||||
|
||||
+31
-24
@@ -232,7 +232,6 @@ QWeb.addDirective({
|
||||
let propStr = Object.keys(props)
|
||||
.map(k => k + ":" + props[k])
|
||||
.join(",");
|
||||
let dummyID = ctx.generateID();
|
||||
let defID = ctx.generateID();
|
||||
let componentID = ctx.generateID();
|
||||
let keyID = key && ctx.generateID();
|
||||
@@ -350,18 +349,13 @@ QWeb.addDirective({
|
||||
ctx.addLine(
|
||||
`let w${componentID} = ${templateId} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateId}]] : false;`
|
||||
);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
|
||||
}
|
||||
let shouldProxy = false;
|
||||
let shouldProxy = !ctx.parentNode;
|
||||
if (keepAlive) {
|
||||
ctx.addLine(
|
||||
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
|
||||
);
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(null);`);
|
||||
} else {
|
||||
if (shouldProxy) {
|
||||
let id = ctx.generateID();
|
||||
ctx.rootContext.rootNode = id;
|
||||
shouldProxy = true;
|
||||
@@ -378,15 +372,9 @@ QWeb.addDirective({
|
||||
ctx.addIf(
|
||||
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
|
||||
);
|
||||
ctx.addIf(
|
||||
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
|
||||
);
|
||||
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`w${componentID}.destroy();`);
|
||||
ctx.addLine(`w${componentID} = false;`);
|
||||
ctx.closeIf();
|
||||
ctx.closeIf();
|
||||
|
||||
ctx.addIf(`!w${componentID}`);
|
||||
// new component
|
||||
@@ -450,15 +438,26 @@ QWeb.addDirective({
|
||||
} else {
|
||||
scopeVars = "undefined, undefined";
|
||||
}
|
||||
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
|
||||
ctx.addLine(`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 = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
|
||||
let registerCode = "";
|
||||
if (shouldProxy) {
|
||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(vnode=>{if (w${componentID}.__owl__.isDestroyed){return}${createHook}let pvnode=h(vnode.sel, {key: ${templateId}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});${registerCode}w${componentID}.__owl__.pvnode = pvnode;});`
|
||||
`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}}}});`
|
||||
);
|
||||
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
|
||||
ctx.addLine(
|
||||
`def${defID}.then(function () {if (w${componentID}.__owl__.isDestroyed) {return;} const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
|
||||
);
|
||||
if (registerCode) {
|
||||
ctx.addLine(registerCode);
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||
}
|
||||
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
|
||||
|
||||
ctx.addElse();
|
||||
// need to update component
|
||||
@@ -475,24 +474,32 @@ QWeb.addDirective({
|
||||
}
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
|
||||
", " + 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.addLine(
|
||||
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
|
||||
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
|
||||
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}${registerCode}});`
|
||||
);
|
||||
ctx.closeIf();
|
||||
|
||||
if (classObj) {
|
||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||
}
|
||||
|
||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||
ctx.addLine(`sibling = w${componentID}.__owl__.currentFiber;`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { VNode } from "../vdom/index";
|
||||
import { Component } from "./component";
|
||||
/**
|
||||
* Fibers are small abstractions designed to contain all the internal state
|
||||
* associated to a "rendering work unit", relative to a specific component.
|
||||
*
|
||||
* A rendering will cause the creation of a fiber for each impacted components.
|
||||
*/
|
||||
export class Fiber {
|
||||
force: boolean;
|
||||
isCancelled: boolean = false;
|
||||
shouldPatch: boolean = true;
|
||||
isRendered: boolean = false;
|
||||
|
||||
scope: any;
|
||||
vars: any;
|
||||
props: any;
|
||||
|
||||
component: Component<any, any>;
|
||||
vnode: VNode | null = null;
|
||||
|
||||
root: Fiber;
|
||||
child: Fiber | null = null;
|
||||
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;
|
||||
this.vars = vars;
|
||||
this.props = props;
|
||||
this.component = component;
|
||||
|
||||
this.root = parent ? parent.root : this;
|
||||
this.parent = parent;
|
||||
|
||||
let oldFiber = component.__owl__.currentFiber;
|
||||
if (oldFiber && !oldFiber.isCancelled) {
|
||||
this.__remapFiber(oldFiber);
|
||||
}
|
||||
|
||||
this.root.counter++;
|
||||
|
||||
component.__owl__.currentFiber = this;
|
||||
}
|
||||
|
||||
__remapFiber(oldFiber: Fiber) {
|
||||
oldFiber.cancel();
|
||||
if (oldFiber === oldFiber.root) {
|
||||
oldFiber.root.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.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given patch queue from a 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
|
||||
*/
|
||||
__applyPatchQueue() {
|
||||
const patchQueue: Fiber[] = [];
|
||||
const doWork: (Fiber) => Fiber | null = function(f) {
|
||||
if (f.shouldPatch) {
|
||||
patchQueue.push(f);
|
||||
}
|
||||
return f.child;
|
||||
};
|
||||
this.__walk(doWork);
|
||||
let component: Component<any, any> = this.component;
|
||||
this.shouldPatch = false;
|
||||
const patchLen = patchQueue.length;
|
||||
try {
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
component = patchQueue[i].component;
|
||||
if (component.__owl__.willPatchCB) {
|
||||
component.__owl__.willPatchCB();
|
||||
}
|
||||
component.willPatch();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.__patch(fiber.vnode);
|
||||
}
|
||||
} catch (e) {
|
||||
this.handleError(e);
|
||||
}
|
||||
try {
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
component = patchQueue[i].component;
|
||||
component.patched();
|
||||
if (component.__owl__.patchedCB) {
|
||||
component.__owl__.patchedCB();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.shouldPatch = true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.__walk(f => {
|
||||
if (!f.isRendered) {
|
||||
f.root.counter--;
|
||||
}
|
||||
f.isCancelled = true;
|
||||
return f.child;
|
||||
});
|
||||
}
|
||||
|
||||
handleError(e: Error) {}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Fiber } from "./fiber";
|
||||
|
||||
// scheduler
|
||||
interface Task {
|
||||
fiber: Fiber;
|
||||
callback: () => void;
|
||||
}
|
||||
|
||||
export const scheduler = {
|
||||
tasks: [] as Task[],
|
||||
isRunning: false,
|
||||
|
||||
addFiber(fiber, callback) {
|
||||
this.tasks.push({ fiber, callback });
|
||||
if (this.isRunning) {
|
||||
return;
|
||||
}
|
||||
this.scheduleTasks();
|
||||
},
|
||||
flush() {
|
||||
let tasks = this.tasks;
|
||||
this.tasks = [];
|
||||
tasks = tasks.filter(task => {
|
||||
if (task.fiber.isCancelled) {
|
||||
return false;
|
||||
}
|
||||
if (task.fiber.counter === 0) {
|
||||
task.callback();
|
||||
return false;
|
||||
}
|
||||
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)
|
||||
};
|
||||
+5
-3
@@ -1,8 +1,8 @@
|
||||
import { Component } from "./component/component";
|
||||
import { Observer } from "./core/observer";
|
||||
import { scheduler } from "./component/scheduler";
|
||||
import { EventBus } from "./core/event_bus";
|
||||
import { Observer } from "./core/observer";
|
||||
import { onWillUnmount } from "./hooks";
|
||||
|
||||
/**
|
||||
* The `Context` object provides a way to share data between an arbitrary number
|
||||
* of component. Usually, data is passed from a parent to its children component,
|
||||
@@ -53,7 +53,9 @@ export class Context extends EventBus {
|
||||
const sub = subs[i];
|
||||
const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
|
||||
if (shouldCallback) {
|
||||
await sub.callback.call(sub.owner, id);
|
||||
const render = sub.callback.call(sub.owner, id);
|
||||
scheduler.flush();
|
||||
await render;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class CompilationContext {
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineRefs: boolean = false;
|
||||
shouldDefineResult: boolean = true;
|
||||
shouldDefineSibling: boolean = true;
|
||||
shouldProtectContext: boolean = false;
|
||||
shouldTrackScope: boolean = false;
|
||||
loopNumber: number = 0;
|
||||
@@ -63,6 +64,9 @@ export class CompilationContext {
|
||||
if (this.shouldDefineResult) {
|
||||
this.code.unshift(" let result;");
|
||||
}
|
||||
if (this.shouldDefineSibling) {
|
||||
this.code.unshift(" let sibling = null;");
|
||||
}
|
||||
if (this.shouldDefineRefs) {
|
||||
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||
}
|
||||
|
||||
@@ -207,9 +207,7 @@ QWeb.addDirective({
|
||||
`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: owner}));`
|
||||
);
|
||||
if (!ctx.parentNode) {
|
||||
ctx.addLine(
|
||||
`Promise.all(extra.promises).then(() => utils.defineProxy(result, ${parentNode}[0]))`
|
||||
);
|
||||
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
|
||||
}
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
|
||||
+1
-1
@@ -645,7 +645,7 @@ export class QWeb extends EventBus {
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
const v = ctx.getValue(value);
|
||||
let formattedValue = typeof v === 'string' ? ctx.formatExpression(v) : v.id;
|
||||
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
|
||||
|
||||
if (attName === "class") {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
|
||||
+5
-3
@@ -557,13 +557,15 @@ type ArrayOrElement<T> = T | T[];
|
||||
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
||||
|
||||
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||
if (sel === "dummy") {
|
||||
// we do not need to add the namespace on dummy elements, they come from a
|
||||
// subcomponent, which will handle the namespace itself
|
||||
return;
|
||||
}
|
||||
data.ns = "http://www.w3.org/2000/svg";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
const child = children[i];
|
||||
if (child === null) {
|
||||
continue;
|
||||
}
|
||||
let childData = child.data;
|
||||
if (childData !== undefined) {
|
||||
addNS(childData, (child as VNode).children as VNodes, child.sel);
|
||||
|
||||
Reference in New Issue
Block a user