[FIX] component: propagate correct info when reusing fibers

In some cases, a rendering initiated in some component is then remapped
into a larger rendering initiated by some parent.

If we have some components which implement shouldUpdate to return false,
then the following scenario can happen:

- some parent component is mounted (which triggers a rendering with
force: true => bypass the shouldUpdate)
- some sub component is updated and rerendered, AFTER the previous
rendering goes through it
- the sub component notices that there is an ongoing rendering, and
  remaps itself in the parent rendering

Before this commit, the new fiber in the subcomponent does not have
force flag set to true, so the new rendering for the subcomponent does
not go through its own children (if they have shouldUpdate=false)

Another more complex kind of scenaria can happen when a remapped
rendering happen with sub components with dynamic shouldUpdate. The
problem is the same at the end: the new rendering should ignore the
shouldUpdate, to make sure we have the last correct information.

With this commit, we make sure that the flag of the new fiber is set to
true.

closes #818
This commit is contained in:
Géry Debongnie
2021-01-06 15:20:55 +01:00
committed by aab-odoo
parent 3a10468f7b
commit d043d47754
3 changed files with 173 additions and 3 deletions
+16 -3
View File
@@ -10,13 +10,21 @@
- [Static Properties](#static-properties) - [Static Properties](#static-properties)
- [Methods](#methods) - [Methods](#methods)
- [Lifecycle](#lifecycle) - [Lifecycle](#lifecycle)
- [`constructor(parent, props)`](#constructorparent-props)
- [`willStart()`](#willstart)
- [`mounted()`](#mounted)
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
- [`willPatch()`](#willpatch)
- [`patched(snapshot)`](#patchedsnapshot)
- [`willUnmount()`](#willunmount)
- [`catchError(error)`](#catcherrorerror)
- [Root Component](#root-component) - [Root Component](#root-component)
- [Composition](#composition) - [Composition](#composition)
- [Form Input Bindings](#form-input-bindings) - [Form Input Bindings](#form-input-bindings)
- [References](#references) - [References](#references)
- [Dynamic sub components](#dynamic-sub-components) - [Dynamic sub components](#dynamic-sub-components)
- [Functional Components](#functional-components) - [Functional Components](#functional-components)
- [SVG components](#svg-components) - [SVG Components](#svg-components)
## Overview ## Overview
@@ -289,8 +297,13 @@ We explain here all the public methods of the `Component` class.
are updated. It returns a boolean, which indicates if the component should are updated. It returns a boolean, which indicates if the component should
ignore a props update. If it returns false, then `willUpdateProps` will not ignore a props update. If it returns false, then `willUpdateProps` will not
be called, and no rendering will occur. Its default implementation is to be called, and no rendering will occur. Its default implementation is to
always return true. This is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
can be useful if we are handling large number of components. can be useful if we are handling large number of components. Since this is an
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
some cases (for example, if a component is remounted, or if we want to force
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
provides the guarantee that the component will be rendered at some point in
the future (except if the component is destroyed or if some part of the UI crashes).
* **`destroy()`**. As its name suggests, this method will remove the component, * **`destroy()`**. As its name suggests, this method will remove the component,
and perform all necessary cleanup, such as unmounting the component, its children, and perform all necessary cleanup, such as unmounting the component, its children,
+1
View File
@@ -82,6 +82,7 @@ export class Fiber {
let oldFiber = __owl__.currentFiber; let oldFiber = __owl__.currentFiber;
if (oldFiber && !oldFiber.isCompleted) { if (oldFiber && !oldFiber.isCompleted) {
this.force = true;
if (oldFiber.root === oldFiber && !parent) { if (oldFiber.root === oldFiber && !parent) {
// both oldFiber and this fiber are root fibers // both oldFiber and this fiber are root fibers
this._reuseFiber(oldFiber); this._reuseFiber(oldFiber);
+156
View File
@@ -1405,4 +1405,160 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<div>2</div>"); expect(fixture.innerHTML).toBe("<div>2</div>");
expect(Widget.prototype.__render).toHaveBeenCalledTimes(2); expect(Widget.prototype.__render).toHaveBeenCalledTimes(2);
}); });
test("components with shouldUpdate=false", async () => {
const state = { p: 1, cc: 10 };
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return false;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return false;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return false;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
var div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div></div><div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
app.mount(div);
// wait for rendering from second mount to go through parent
await Promise.resolve();
await Promise.resolve();
state.cc++;
state.p++;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div><div> parent: 2<div> child <div> child child: 11</div></div></div></div></div>"
);
});
test("components with shouldUpdate=false, part 2", async () => {
const state = { p: 1, cc: 10 };
let shouldUpdate = true;
class ChildChild extends Component {
static template = xml`
<div>
child child: <t t-esc="state.cc"/>
</div>`;
state = state;
shouldUpdate() {
return shouldUpdate;
}
}
class Child extends Component {
static components = { ChildChild };
static template = xml`
<div>
child
<ChildChild/>
</div>`;
shouldUpdate() {
return shouldUpdate;
}
}
let parent: any;
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
parent: <t t-esc="state.p"/>
<Child/>
</div>`;
state = state;
constructor(a, b) {
super(a, b);
parent = this;
}
shouldUpdate() {
return shouldUpdate;
}
}
class App extends Component {
static components = { Parent };
static template = xml`
<div>
<Parent/>
</div>`;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div> parent: 1<div> child <div> child child: 10</div></div></div></div>"
);
state.cc++;
state.p++;
app.render();
// wait for rendering to go through child
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
shouldUpdate = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><div> parent: 2<div> child <div> child child: 11</div></div></div></div>"
);
});
}); });