mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
properly handle concurrent rendering passes
This commit is contained in:
@@ -28,6 +28,10 @@ interface Meta<T extends WEnv> {
|
||||
// children mapping: from templateID to widgetID
|
||||
// should it be a map number => Widget?
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
renderId: number;
|
||||
renderProps: any;
|
||||
renderPromise: Promise<VNode> | null;
|
||||
}
|
||||
|
||||
const patch = init([sdListeners, sdAttrs]);
|
||||
@@ -92,7 +96,10 @@ export class Component<
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
children: {},
|
||||
cmap: {}
|
||||
cmap: {},
|
||||
renderId: 1,
|
||||
renderPromise: null,
|
||||
renderProps: props
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,28 +210,44 @@ export class Component<
|
||||
if (this.__widget__.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
const vnode = await this._render();
|
||||
this._patch(vnode);
|
||||
const renderVDom = this._render();
|
||||
const renderId = this.__widget__.renderId;
|
||||
const vnode = await renderVDom;
|
||||
if (renderId === this.__widget__.renderId) {
|
||||
// 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._patch(vnode);
|
||||
}
|
||||
}
|
||||
|
||||
private _patch(vnode) {
|
||||
this.__widget__.renderPromise = null;
|
||||
this.__widget__.vnode = patch(
|
||||
this.__widget__.vnode || document.createElement(vnode.sel!),
|
||||
vnode
|
||||
);
|
||||
}
|
||||
private async _start(): Promise<VNode> {
|
||||
await this.willStart();
|
||||
if (!this.__widget__.isDestroyed) {
|
||||
this.__widget__.renderProps = this.props;
|
||||
this.__widget__.renderPromise = this.willStart().then(() => {
|
||||
if (this.__widget__.isDestroyed) {
|
||||
return Promise.resolve(this.env.qweb.render("default"));
|
||||
}
|
||||
this.__widget__.isStarted = true;
|
||||
}
|
||||
if (this.inlineTemplate) {
|
||||
this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
|
||||
}
|
||||
return this._render();
|
||||
if (this.inlineTemplate) {
|
||||
this.env.qweb.addTemplate(
|
||||
this.inlineTemplate,
|
||||
this.inlineTemplate,
|
||||
true
|
||||
);
|
||||
}
|
||||
return this._render();
|
||||
});
|
||||
return this.__widget__.renderPromise;
|
||||
}
|
||||
|
||||
async _render(): Promise<VNode> {
|
||||
this.__widget__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
const template = this.inlineTemplate || this.template;
|
||||
let vnode = this.env.qweb.render(template, this, { promises });
|
||||
@@ -235,7 +258,9 @@ export class Component<
|
||||
// parent widget. With this, we make sure that the parent widget will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = this.__widget__.id;
|
||||
return Promise.all(promises).then(() => vnode);
|
||||
this.__widget__.renderProps = this.props;
|
||||
this.__widget__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return this.__widget__.renderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,12 +299,8 @@ export class Component<
|
||||
}
|
||||
}
|
||||
|
||||
export class PureComponent<T extends WEnv, Props, State> extends Component<
|
||||
T,
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
export class PureComponent<T extends WEnv, P, S> extends Component<T, P, S> {
|
||||
shouldUpdate(nextProps: P): boolean {
|
||||
for (let k in nextProps) {
|
||||
if (nextProps[k] !== this.props[k]) {
|
||||
return true;
|
||||
@@ -287,7 +308,7 @@ export class PureComponent<T extends WEnv, Props, State> extends Component<
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async updateState(nextState: Partial<State>) {
|
||||
async updateState(nextState: Partial<S>) {
|
||||
for (let k in nextState) {
|
||||
if (nextState[k] !== this.state[k]) {
|
||||
return super.updateState(nextState);
|
||||
|
||||
@@ -799,43 +799,96 @@ const widgetDirective: Directive = {
|
||||
ctx.addLine(
|
||||
`let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;`
|
||||
);
|
||||
ctx.addLine(`if (w${widgetID}) {`);
|
||||
ctx.addLine(`let props${widgetID} = ${props};`);
|
||||
ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
|
||||
|
||||
// check if we can reuse current rendering promise
|
||||
ctx.addLine(`if (w${widgetID} && w${widgetID}.__widget__.renderPromise) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`if (w${widgetID}.__widget__.isStarted) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(
|
||||
`def${defID} = w${widgetID}.updateProps(${props}).then(()=>{let vnode=h(w${widgetID}.__widget__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;c${
|
||||
`if (props${widgetID} === w${widgetID}.__widget__.renderProps) {`
|
||||
);
|
||||
ctx.indent();
|
||||
ctx.addLine(`def${defID} = w${widgetID}.__widget__.renderPromise;`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`} else {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID});`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`} else {`); // not started
|
||||
ctx.indent();
|
||||
ctx.addLine(
|
||||
`if (props${widgetID} === w${widgetID}.__widget__.renderProps) {`
|
||||
);
|
||||
ctx.indent();
|
||||
ctx.addLine(`def${defID} = w${widgetID}.__widget__.renderPromise;`);
|
||||
ctx.addLine(`isNew${widgetID} = true`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`} else {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`w${widgetID}.destroy();`);
|
||||
ctx.addLine(`w${widgetID} = false`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
|
||||
ctx.addLine(`if (!def${defID}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`if (w${widgetID}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`def${defID} = w${widgetID}.updateProps(props${widgetID});`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`} else {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(
|
||||
`w${widgetID} = new context.widgets['${value}'](owner, props${widgetID});`
|
||||
);
|
||||
ctx.addLine(
|
||||
`context.__widget__.cmap[${templateID}] = w${widgetID}.__widget__.id;`
|
||||
);
|
||||
for (let [event, method] of events) {
|
||||
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
|
||||
}
|
||||
let ref = node.getAttribute("t-ref");
|
||||
if (ref) {
|
||||
ctx.addLine(`context.refs['${ref}'] = w${widgetID};`);
|
||||
}
|
||||
|
||||
ctx.addLine(`def${defID} = w${widgetID}._start();`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
|
||||
ctx.addLine(`if (isNew${widgetID}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()}}});`
|
||||
);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`} else {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(
|
||||
`def${defID} = def${defID}.then(()=>{let vnode=h(w${widgetID}.__widget__.vnode.sel, {key: ${templateID}});vnode.elm=w${widgetID}.el;c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=vnode;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w${widgetID}.el,a.elm);a.elm=w${widgetID}.el;w${widgetID}.__mount();},remove(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()}}});`
|
||||
);
|
||||
ctx.dedent();
|
||||
ctx.addLine("} else {");
|
||||
ctx.indent();
|
||||
ctx.addLine(`}`);
|
||||
|
||||
ctx.addLine(
|
||||
`let _${widgetID} = new context.widgets['${value}'](owner, ${props});`
|
||||
);
|
||||
ctx.addLine(
|
||||
`context.__widget__.cmap[${templateID}] = _${widgetID}.__widget__.id;`
|
||||
);
|
||||
for (let [event, method] of events) {
|
||||
ctx.addLine(`_${widgetID}.on('${event}', owner, owner['${method}'])`);
|
||||
}
|
||||
|
||||
ctx.addLine(
|
||||
`def${defID} = _${widgetID}._start().then(() => _${widgetID}._render()).then(vnode=>{let pvnode=h(vnode.sel, {key: ${templateID}});c${
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=_${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){_${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()}}});`
|
||||
);
|
||||
|
||||
let ref = node.getAttribute("t-ref");
|
||||
if (ref) {
|
||||
ctx.addLine(`context.refs['${ref}'] = _${widgetID};`);
|
||||
}
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
ctx.addLine(`extra.promises.push(def${defID});`);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -769,6 +769,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
}
|
||||
const widget = new Test(env);
|
||||
await widget.mount(fixture);
|
||||
// console.log(children(widget)[0].__widget__)
|
||||
expect(fixture.innerHTML).toBe("<div>txttxt<div></div></div>");
|
||||
});
|
||||
|
||||
@@ -777,7 +778,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
// interplay between widgets and vnodes, a sub widget vnode was patched
|
||||
// twice.
|
||||
class Parent extends Widget {
|
||||
inlineTemplate = `<div><t t-widget="child" t-props="state"/></div>`;
|
||||
inlineTemplate = `<div><t t-widget="child" t-props="{flag:state.flag}"/></div>`;
|
||||
widgets = { child: Child };
|
||||
state = { flag: false };
|
||||
}
|
||||
@@ -793,3 +794,49 @@ describe("random stuff/miscellaneous", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><span>abcdef</span></div>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("async rendering", () => {
|
||||
let resolveA, resolveB;
|
||||
|
||||
class ChildA extends Widget {
|
||||
inlineTemplate = "<span>a</span>";
|
||||
willStart(): Promise<void> {
|
||||
return new Promise(r => (resolveA = r));
|
||||
}
|
||||
}
|
||||
|
||||
class ChildB extends Widget {
|
||||
inlineTemplate = "<span>b</span>";
|
||||
willStart(): Promise<void> {
|
||||
return new Promise(r => (resolveB = r));
|
||||
}
|
||||
}
|
||||
|
||||
test("creating two async widgets, scenario 1", async () => {
|
||||
class Parent extends Widget {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-if="state.flagA"><t t-widget="ChildA"/></t>
|
||||
<t t-if="state.flagB"><t t-widget="ChildB"/></t>
|
||||
</div>`;
|
||||
widgets = { ChildA, ChildB };
|
||||
state = { flagA: false, flagB: false };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("<div></div>");
|
||||
parent.updateState({ flagA: true });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("<div></div>");
|
||||
parent.updateState({ flagB: true });
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("<div></div>");
|
||||
resolveB();
|
||||
expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe("<div></div>");
|
||||
resolveA();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML.replace(/\r?\n|\r|\s+/g, "")).toBe(
|
||||
"<div><span>a</span><span>b</span></div>"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user