mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
bb64e87634
Before this commit, an unwanted behaviour happened when using components with shouldUpdate implemented, and store/state. The actual problem is the following: the sub component is using a store, and shouldupdate. Whenever the component is rendered, it checks if there is an incoming rendering from the context. If that is the case, it skips the rendering, because we actually only want to be rendered by the store rendering (otherwise, we may run into issue with inconsistent data (more recent data from the context, older data in the component). However, if shouldUpdate is implemented, then the rendering coming from the context simply does not arrive. To fix this, we tried to just force these renderings to go through all children, regardless of their shouldUpdate status. This actually works, but then shouldUpdate is ignored, which is an issue in Odoo discuss. Then, after discussing this situation, we noticed that the context/store system is actually unsafe: the protection given by the check mentioned above is in fact fundamentally insufficient: there are other perfectly valid situations where a rendering can be triggered on the component, which will bypass the check (for example, an explicit call to this.render() or a rendering initiated by some parent component) and cause a crash if the component is not properly defensively written. Therefore, it is currently mandatory for all components using context/store to be aware of that, and to protect themselves against such situations. So, in that regard, the check is not really a protection, it just helps hiding an unsafe situation anyway and we decided to remove it. Note that this is a potentially breaking change: components using a store and some local state will now be rendered twice in some cases... closes #799
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
import { Component } from "./component/component";
|
|
import { scheduler } from "./component/scheduler";
|
|
import { EventBus } from "./core/event_bus";
|
|
import { Observer } from "./core/observer";
|
|
|
|
/**
|
|
* 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,
|
|
* but when we have to deal with some mostly global information, this can be
|
|
* annoying, since each component will need to pass the information to each
|
|
* children, even though some or most of them will not use the information.
|
|
*
|
|
* With a `Context` object, each component can subscribe (with the `useContext`
|
|
* hook) to its state, and will be updated whenever the context state is updated.
|
|
*/
|
|
|
|
function partitionBy<T>(arr: T[], fn: (t: T) => boolean) {
|
|
let lastGroup: T[] | false = false;
|
|
let lastValue;
|
|
return arr.reduce((acc: T[][], cur) => {
|
|
let curVal = fn(cur);
|
|
if (lastGroup) {
|
|
if (curVal === lastValue) {
|
|
lastGroup.push(cur);
|
|
} else {
|
|
lastGroup = false;
|
|
}
|
|
}
|
|
if (!lastGroup) {
|
|
lastGroup = [cur];
|
|
acc.push(lastGroup);
|
|
}
|
|
lastValue = curVal;
|
|
return acc;
|
|
}, []);
|
|
}
|
|
|
|
export class Context extends EventBus {
|
|
state: any;
|
|
observer: Observer;
|
|
rev: number = 1;
|
|
// mapping from component id to last observed context id
|
|
mapping: { [componentId: number]: number } = {};
|
|
|
|
constructor(state: Object = {}) {
|
|
super();
|
|
this.observer = new Observer();
|
|
this.observer.notifyCB = () => {
|
|
// notify components in the next microtask tick to ensure that subscribers
|
|
// are notified only once for all changes that occur in the same micro tick
|
|
let rev = this.rev;
|
|
return Promise.resolve().then(() => {
|
|
if (rev === this.rev) {
|
|
this.__notifyComponents();
|
|
}
|
|
});
|
|
};
|
|
this.state = this.observer.observe(state);
|
|
this.subscriptions.update = [];
|
|
}
|
|
|
|
/**
|
|
* Instead of using trigger to emit an update event, we actually implement
|
|
* our own function to do that. The reason is that we need to be smarter than
|
|
* a simple trigger function: we need to wait for parent components to be
|
|
* done before doing children components. More precisely, if an update
|
|
* as an effect of destroying a children, we do not want to call any code
|
|
* from the child, and certainly not render it.
|
|
*
|
|
* This method implements a simple grouping algorithm by depth. If we have
|
|
* connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
|
|
* them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
|
|
* be updated sequentially, but each components in a given group will be done in
|
|
* parallel.
|
|
*
|
|
* This is a very simple algorithm, but it avoids checking if a given
|
|
* component is a child of another.
|
|
*/
|
|
async __notifyComponents() {
|
|
const rev = ++this.rev;
|
|
const subscriptions = this.subscriptions.update;
|
|
const groups = partitionBy(subscriptions, (s) => (s.owner ? s.owner.__owl__.depth : -1));
|
|
for (let group of groups) {
|
|
const proms = group.map((sub) => sub.callback.call(sub.owner, rev));
|
|
// at this point, each component in the current group has registered a
|
|
// top level fiber in the scheduler. It could happen that rendering these
|
|
// components is done (if they have no children). This is why we manually
|
|
// flush the scheduler. This will force the scheduler to check
|
|
// immediately if they are done, which will cause their rendering
|
|
// promise to resolve earlier, which means that there is a chance of
|
|
// processing the next group in the same frame.
|
|
scheduler.flush();
|
|
await Promise.all(proms);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The`useContext` hook is the normal way for a component to register themselve
|
|
* to context state changes. The `useContext` method returns the context state
|
|
*/
|
|
export function useContext(ctx: Context): any {
|
|
const component: Component = Component.current!;
|
|
return useContextWithCB(ctx, component, component.render.bind(component));
|
|
}
|
|
|
|
export function useContextWithCB(ctx: Context, component: Component, method): any {
|
|
const __owl__ = component.__owl__;
|
|
const id = __owl__.id;
|
|
const mapping = ctx.mapping;
|
|
if (id in mapping) {
|
|
return ctx.state;
|
|
}
|
|
if (!__owl__.observer) {
|
|
__owl__.observer = new Observer();
|
|
__owl__.observer.notifyCB = component.render.bind(component);
|
|
}
|
|
|
|
mapping[id] = 0;
|
|
const renderFn = __owl__.renderFn;
|
|
__owl__.renderFn = function (comp, params) {
|
|
mapping[id] = ctx.rev;
|
|
return renderFn(comp, params);
|
|
};
|
|
ctx.on("update", component, async (contextRev) => {
|
|
if (mapping[id] < contextRev) {
|
|
mapping[id] = contextRev;
|
|
await method();
|
|
}
|
|
});
|
|
const __destroy = component.__destroy;
|
|
component.__destroy = (parent) => {
|
|
ctx.off("update", component);
|
|
delete mapping[id];
|
|
__destroy.call(component, parent);
|
|
};
|
|
return ctx.state;
|
|
}
|