mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] concurrency issue (more robust handling of children per render)
Before this commit, the list of all children was managed at the level of the root fiber, but this could cause issue when subfibers would be reused. With this commit, we use the childrenMap object that exists on each fiber instead.
This commit is contained in:
committed by
Sam Degueldre
parent
fd13277e1d
commit
859748aed9
+1
-1
@@ -67,7 +67,7 @@ export class App<
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||||
return new ComponentNode(Component, props, this);
|
return new ComponentNode(Component, props, this, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
||||||
|
|||||||
@@ -109,13 +109,11 @@ export function component<P extends object>(
|
|||||||
throw new Error(`Cannot find the definition of component "${name}"`);
|
throw new Error(`Cannot find the definition of component "${name}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node = new ComponentNode(C, props, ctx.app, ctx);
|
node = new ComponentNode(C, props, ctx.app, ctx, key);
|
||||||
ctx.children[key] = node;
|
ctx.children[key] = node;
|
||||||
node.initiateRender(new Fiber(node, parentFiber));
|
node.initiateRender(new Fiber(node, parentFiber));
|
||||||
}
|
}
|
||||||
parentFiber.childrenMap[key] = node;
|
parentFiber.childrenMap[key] = node;
|
||||||
|
|
||||||
parentFiber.root!.reachedChildren.add(node);
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +131,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
bdom: BDom | null = null;
|
bdom: BDom | null = null;
|
||||||
status: STATUS = STATUS.NEW;
|
status: STATUS = STATUS.NEW;
|
||||||
forceNextRender: boolean = false;
|
forceNextRender: boolean = false;
|
||||||
|
parentKey: string | null;
|
||||||
|
|
||||||
renderFn: Function;
|
renderFn: Function;
|
||||||
parent: ComponentNode | null;
|
parent: ComponentNode | null;
|
||||||
@@ -149,10 +148,17 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
|
|||||||
patched: LifecycleHook[] = [];
|
patched: LifecycleHook[] = [];
|
||||||
willDestroy: LifecycleHook[] = [];
|
willDestroy: LifecycleHook[] = [];
|
||||||
|
|
||||||
constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
|
constructor(
|
||||||
|
C: ComponentConstructor<P, E>,
|
||||||
|
props: P,
|
||||||
|
app: App,
|
||||||
|
parent: ComponentNode | null,
|
||||||
|
parentKey: string | null
|
||||||
|
) {
|
||||||
currentNode = this;
|
currentNode = this;
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.parent = parent || null;
|
this.parent = parent;
|
||||||
|
this.parentKey = parentKey;
|
||||||
this.level = parent ? parent.level + 1 : 0;
|
this.level = parent ? parent.level + 1 : 0;
|
||||||
applyDefaultProps(props, C);
|
applyDefaultProps(props, C);
|
||||||
const env = (parent && parent.childEnv) || app.env;
|
const env = (parent && parent.childEnv) || app.env;
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
current.children = [];
|
current.children = [];
|
||||||
current.childrenMap = {};
|
current.childrenMap = {};
|
||||||
current.bdom = null;
|
current.bdom = null;
|
||||||
if (current === root) {
|
|
||||||
root.reachedChildren = new WeakSet();
|
|
||||||
}
|
|
||||||
if (fibersInError.has(current)) {
|
if (fibersInError.has(current)) {
|
||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
fibersInError.delete(root);
|
fibersInError.delete(root);
|
||||||
@@ -108,7 +105,7 @@ export class Fiber {
|
|||||||
scheduler.delayedRenders.push(this);
|
scheduler.delayedRenders.push(this);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (!root.reachedChildren.has(prev)) {
|
if (!(prev.parentKey! in current.fiber.childrenMap)) {
|
||||||
// is dead. but we keep the render around just in case
|
// is dead. but we keep the render around just in case
|
||||||
scheduler.delayedRenders.push(this);
|
scheduler.delayedRenders.push(this);
|
||||||
return;
|
return;
|
||||||
@@ -150,8 +147,6 @@ export class RootFiber extends Fiber {
|
|||||||
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
|
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
|
||||||
locked: boolean = false;
|
locked: boolean = false;
|
||||||
|
|
||||||
reachedChildren: WeakSet<ComponentNode> = new WeakSet();
|
|
||||||
|
|
||||||
complete() {
|
complete() {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
this.locked = true;
|
this.locked = true;
|
||||||
|
|||||||
@@ -1164,6 +1164,69 @@ exports[`delayed rendering, but then initial rendering is cancelled by yet anoth
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`delayed rendering, destruction, stuff happens 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`A\`);
|
||||||
|
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`delayed rendering, destruction, stuff happens 2`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2,b3;
|
||||||
|
b2 = text(\`B\`);
|
||||||
|
if (ctx['state'].hasChild) {
|
||||||
|
b3 = component(\`C\`, {value: ctx['state'].someValue+ctx['props'].value}, key + \`__1\`, node, ctx);
|
||||||
|
}
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`delayed rendering, destruction, stuff happens 3`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
let block4 = createBlock(\`<p><block-text-0/></p>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`C\`);
|
||||||
|
const b3 = component(\`D\`, {}, key + \`__1\`, node, ctx);
|
||||||
|
let txt1 = ctx['props'].value;
|
||||||
|
const b4 = block4([txt1]);
|
||||||
|
return multi([b2, b3, b4]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`delayed rendering, destruction, stuff happens 4`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
let block3 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = text(\`D\`);
|
||||||
|
let hdlr1 = [ctx['increment'], ctx];
|
||||||
|
let txt1 = ctx['state'].val;
|
||||||
|
const b3 = block3([hdlr1, txt1]);
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`delayed rendering, reusing fiber and stuff 1`] = `
|
exports[`delayed rendering, reusing fiber and stuff 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3783,6 +3783,106 @@ test("destroyed component causes other soon to be destroyed component to rerende
|
|||||||
expect(fixture.innerHTML).toBe(" A 22");
|
expect(fixture.innerHTML).toBe(" A 22");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("delayed rendering, destruction, stuff happens", async () => {
|
||||||
|
const promC = makeDeferred();
|
||||||
|
let stateB: any = null;
|
||||||
|
|
||||||
|
class D extends Component {
|
||||||
|
static template = xml`D<button t-on-click="increment"><t t-esc="state.val"/></button>`;
|
||||||
|
state = useState({ val: 1 });
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
increment() {
|
||||||
|
this.state.val++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class C extends Component {
|
||||||
|
static template = xml`C<D/><p><t t-esc="props.value"/></p>`;
|
||||||
|
static components = { D };
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
onWillUpdateProps(() => promC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class B extends Component {
|
||||||
|
static template = xml`B<t t-if="state.hasChild"><C value="state.someValue + props.value"/></t>`;
|
||||||
|
static components = { C };
|
||||||
|
state = useState({ someValue: 3, hasChild: true });
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
stateB = this.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class A extends Component {
|
||||||
|
static template = xml`A<B value="state.value"/>`;
|
||||||
|
static components = { B };
|
||||||
|
state = useState({ value: 33 });
|
||||||
|
setup() {
|
||||||
|
useLogLifecycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = await mount(A, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("ABCD<button>1</button><p>36</p>");
|
||||||
|
expect([
|
||||||
|
"A:setup",
|
||||||
|
"A:willStart",
|
||||||
|
"A:willRender",
|
||||||
|
"B:setup",
|
||||||
|
"B:willStart",
|
||||||
|
"A:rendered",
|
||||||
|
"B:willRender",
|
||||||
|
"C:setup",
|
||||||
|
"C:willStart",
|
||||||
|
"B:rendered",
|
||||||
|
"C:willRender",
|
||||||
|
"D:setup",
|
||||||
|
"D:willStart",
|
||||||
|
"C:rendered",
|
||||||
|
"D:willRender",
|
||||||
|
"D:rendered",
|
||||||
|
"D:mounted",
|
||||||
|
"C:mounted",
|
||||||
|
"B:mounted",
|
||||||
|
"A:mounted",
|
||||||
|
]).toBeLogged();
|
||||||
|
|
||||||
|
// render in A, it updates B and C, but render is blocked in C
|
||||||
|
parent.state.value = 50;
|
||||||
|
await nextTick();
|
||||||
|
expect([
|
||||||
|
"A:willRender",
|
||||||
|
"B:willUpdateProps",
|
||||||
|
"A:rendered",
|
||||||
|
"B:willRender",
|
||||||
|
"C:willUpdateProps",
|
||||||
|
"B:rendered",
|
||||||
|
]).toBeLogged();
|
||||||
|
|
||||||
|
// update B => removes child C
|
||||||
|
stateB.hasChild = false;
|
||||||
|
// update D => render should be delayed, because AB is currently rendering
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect([
|
||||||
|
"B:willRender",
|
||||||
|
"B:rendered",
|
||||||
|
"A:willPatch",
|
||||||
|
"B:willPatch",
|
||||||
|
"C:willUnmount",
|
||||||
|
"D:willUnmount",
|
||||||
|
"D:willDestroy",
|
||||||
|
"C:willDestroy",
|
||||||
|
"B:patched",
|
||||||
|
"A:patched",
|
||||||
|
]).toBeLogged();
|
||||||
|
expect(fixture.innerHTML).toBe("AB");
|
||||||
|
});
|
||||||
|
|
||||||
// test.skip("components with shouldUpdate=false", async () => {
|
// test.skip("components with shouldUpdate=false", async () => {
|
||||||
// const state = { p: 1, cc: 10 };
|
// const state = { p: 1, cc: 10 };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user