[FIX] runtime, compiler: fix refs getting set or unset incorrectly

Previously, refs could get set to null incorrectly, or could stay set
when they shouldn't. This was caused by the fact that singleRefSetter
and multiRefSetters are created on every render, meaning that any render
that did not affect the status of the ref (mounted or not), would
overwrite the previous ref setter, including its closure, causing the
captured `_el` or `count` to be lost. This means that on a subsequent
render that did affect the status of the ref, the singleRefSetter would
consider that it did not set the ref, and so shouldn't unset it, causing
it to incorrectly stay keep the element, while in multiRefSetter, the
opposite problem occured: the count would be 0 on removal, causing it to
believe that it was the first call in the corresponding patch that
affected the ref, when in fact, the closure is already stale, because it
was created from the previous render, and the fresh multiRefSetter from
the latest render may already have been called by an element with that
ref that came earlier in the template.

This commit changes the ref setting strategy to work around the issue of
stale closures: we define a setRef method on ComponentNode that is
always called, this method ignores calls with `null`, meaning that the
refs object on the ComponentNode never reverts to a null value for any
key. Instead, the useRef hook will check whether the element that the
ref points to is still mounted, and return null if not.
This commit is contained in:
Samuel Degueldre
2023-03-06 13:24:39 +01:00
committed by Géry Debongnie
parent 7ac81ed5fe
commit 975ed32f0b
13 changed files with 177 additions and 171 deletions
+18 -33
View File
@@ -192,11 +192,9 @@ class CodeTarget {
code: string[] = [];
hasRoot = false;
hasCache = false;
hasRef: boolean = false;
// maps ref name to [id, expr]
refInfo: { [name: string]: [string, string] } = {};
shouldProtectScope: boolean = false;
on: EventHandlers | null;
hasRefWrapper: boolean = false;
constructor(name: string, on?: EventHandlers | null) {
this.name = name;
@@ -215,17 +213,13 @@ class CodeTarget {
generateCode(): string {
let result: string[] = [];
result.push(`function ${this.name}(ctx, node, key = "") {`);
if (this.hasRef) {
result.push(` const refs = this.__owl__.refs;`);
for (let name in this.refInfo) {
const [id, expr] = this.refInfo[name];
result.push(` const ${id} = ${expr};`);
}
}
if (this.shouldProtectScope) {
result.push(` ctx = Object.create(ctx);`);
result.push(` ctx[isBoundary] = 1`);
}
if (this.hasRefWrapper) {
result.push(` let refWrapper = makeRefWrapper(this.__owl__);`);
}
if (this.hasCache) {
result.push(` let cache = ctx.cache || {};`);
result.push(` let nextCache = ctx.cache = {};`);
@@ -704,30 +698,21 @@ export class CodeGenerator {
// t-ref
if (ast.ref) {
this.target.hasRef = true;
const isDynamic = INTERP_REGEXP.test(ast.ref);
if (isDynamic) {
this.helpers.add("singleRefSetter");
const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
const idx = block!.insertData(`singleRefSetter(refs, ${str})`, "ref");
attrs["block-ref"] = String(idx);
} else {
let name = ast.ref;
if (name in this.target.refInfo) {
// ref has already been defined
this.helpers.add("multiRefSetter");
const info = this.target.refInfo[name];
const index = block!.data.push(info[0]) - 1;
attrs["block-ref"] = String(index);
info[1] = `multiRefSetter(refs, \`${name}\`)`;
} else {
let id = generateId("ref");
this.helpers.add("singleRefSetter");
this.target.refInfo[name] = [id, `singleRefSetter(refs, \`${name}\`)`];
const index = block!.data.push(id) - 1;
attrs["block-ref"] = String(index);
}
if (this.dev) {
this.helpers.add("makeRefWrapper");
this.target.hasRefWrapper = true;
}
const isDynamic = INTERP_REGEXP.test(ast.ref);
let name = `\`${ast.ref}\``;
if (isDynamic) {
name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
}
let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;
if (this.dev) {
setRefStr = `refWrapper(${name}, ${setRefStr})`;
}
const idx = block!.insertData(setRefStr, "ref");
attrs["block-ref"] = String(idx);
}
const dom = xmlDoc.createElement(ast.tag);