properly handle concurrent rendering passes

This commit is contained in:
Géry Debongnie
2019-02-07 12:46:30 +01:00
parent f648f47197
commit 66da26f92f
3 changed files with 168 additions and 47 deletions
+39 -18
View File
@@ -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);
+81 -28
View File
@@ -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;
+48 -1
View File
@@ -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>"
);
});
});