mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Propagate reactive scope dependencies transitively
During PruneNonReactiveDependencies, we sometimes need to promote a value from non-reactive to reactive if it ended up being grouped in the same reactive scope as some other reactive value. This generally happens due to interleaving mutations. In this case all downstream usage of the promoted value need to also be considered reactive. Fully propagating the reactivity requires re-running InferReactivePlaces, to account for things like control reactivity. We can't yet reuse that pass here though, because we haven't unified the pipeline on HIR yet. For now, we propagate the reactivity through local variables and downstream reactive scopes. See test fixtures for some examples that now correctly propagate reactivity and some that need the full reactivity inference to run correctly. The latter cases are handled in the next PR.
This commit is contained in:
+85
-15
@@ -5,7 +5,14 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { IdentifierId, ReactiveFunction, ReactiveScopeBlock } from "../HIR";
|
||||
import {
|
||||
IdentifierId,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
isSetStateType,
|
||||
} from "../HIR";
|
||||
import { eachPatternOperand } from "../HIR/visitors";
|
||||
import { collectReactiveIdentifiers } from "./CollectReactiveIdentifiers";
|
||||
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
|
||||
|
||||
@@ -23,27 +30,90 @@ export function pruneNonReactiveDependencies(fn: ReactiveFunction): void {
|
||||
type ReactiveIdentifiers = Set<IdentifierId>;
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
|
||||
override visitScope(
|
||||
scope: ReactiveScopeBlock,
|
||||
override visitInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: ReactiveIdentifiers
|
||||
): void {
|
||||
this.traverseScope(scope, state);
|
||||
for (const dep of scope.scope.dependencies) {
|
||||
const isReactive = state.has(dep.identifier.id);
|
||||
if (!isReactive) {
|
||||
scope.scope.dependencies.delete(dep);
|
||||
this.traverseInstruction(instruction, state);
|
||||
|
||||
const { lvalue, value } = instruction;
|
||||
switch (value.kind) {
|
||||
case "LoadLocal": {
|
||||
if (lvalue !== null && state.has(value.place.identifier.id)) {
|
||||
state.add(lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "StoreLocal": {
|
||||
if (state.has(value.value.identifier.id)) {
|
||||
state.add(value.lvalue.place.identifier.id);
|
||||
if (lvalue !== null) {
|
||||
state.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Destructure": {
|
||||
if (state.has(value.value.identifier.id)) {
|
||||
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
|
||||
if (isSetStateType(lvalue.identifier)) {
|
||||
continue;
|
||||
}
|
||||
state.add(lvalue.identifier.id);
|
||||
}
|
||||
if (lvalue !== null) {
|
||||
state.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
if (
|
||||
lvalue !== null &&
|
||||
state.has(value.object.identifier.id) &&
|
||||
!isSetStateType(lvalue.identifier)
|
||||
) {
|
||||
state.add(lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ComputedLoad": {
|
||||
if (
|
||||
lvalue !== null &&
|
||||
(state.has(value.object.identifier.id) ||
|
||||
state.has(value.property.identifier.id))
|
||||
) {
|
||||
state.add(lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scope.scope.dependencies.size === 0) {
|
||||
// If a scope has no dependencies, then its declarations are all non-reactive
|
||||
for (const [, declaration] of scope.scope.declarations) {
|
||||
state.delete(declaration.identifier.id);
|
||||
}
|
||||
|
||||
override visitScope(
|
||||
scopeBlock: ReactiveScopeBlock,
|
||||
state: ReactiveIdentifiers
|
||||
): void {
|
||||
this.traverseScope(scopeBlock, state);
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const isReactive = state.has(dep.identifier.id);
|
||||
if (!isReactive) {
|
||||
scopeBlock.scope.dependencies.delete(dep);
|
||||
}
|
||||
} else {
|
||||
// otherwise, all the scope's declarations are reactive
|
||||
for (const [, declaration] of scope.scope.declarations) {
|
||||
}
|
||||
if (scopeBlock.scope.dependencies.size !== 0) {
|
||||
/**
|
||||
* If any of a scope's dependencies are reactive, then all of its
|
||||
* outputs will re-evaluate whenever those dependencies change.
|
||||
* Mark all of the outputs as reactive to reflect the fact that
|
||||
* they may change in practice based on a reactive input.
|
||||
*/
|
||||
for (const [, declaration] of scopeBlock.scope.declarations) {
|
||||
state.add(declaration.identifier.id);
|
||||
}
|
||||
for (const reassignment of scopeBlock.scope.reassignments) {
|
||||
state.add(reassignment.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
// a and b are independent but their mutations are interleaved, so
|
||||
// they get grouped in a reactive scope. this means that a becomes
|
||||
// reactive since it will effectively re-evaluate based on a reactive
|
||||
// input
|
||||
const a = [];
|
||||
const b = [];
|
||||
b.push(props.cond);
|
||||
a.push(null);
|
||||
|
||||
// Downstream consumer of a, which initially seems non-reactive except
|
||||
// that a becomes reactive, per above
|
||||
const c = [a];
|
||||
|
||||
let x;
|
||||
if (c[0]) {
|
||||
x = 1;
|
||||
} else {
|
||||
x = 2;
|
||||
}
|
||||
// The values assigned to `x` are non-reactive, but the value of `x`
|
||||
// depends on the "control" value `c[0]` which becomes reactive via
|
||||
// being interleaved with `b`.
|
||||
// Therefore x should be treated as reactive too.
|
||||
return [x];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ cond: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react";
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(1);
|
||||
|
||||
const a = [];
|
||||
const b = [];
|
||||
b.push(props.cond);
|
||||
a.push(null);
|
||||
|
||||
const c = [a];
|
||||
|
||||
let x;
|
||||
if (c[0]) {
|
||||
x = 1;
|
||||
} else {
|
||||
x = 2;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [x];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ cond: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [1]
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
function Component(props) {
|
||||
// a and b are independent but their mutations are interleaved, so
|
||||
// they get grouped in a reactive scope. this means that a becomes
|
||||
// reactive since it will effectively re-evaluate based on a reactive
|
||||
// input
|
||||
const a = [];
|
||||
const b = [];
|
||||
b.push(props.cond);
|
||||
a.push(null);
|
||||
|
||||
// Downstream consumer of a, which initially seems non-reactive except
|
||||
// that a becomes reactive, per above
|
||||
const c = [a];
|
||||
|
||||
let x;
|
||||
if (c[0]) {
|
||||
x = 1;
|
||||
} else {
|
||||
x = 2;
|
||||
}
|
||||
// The values assigned to `x` are non-reactive, but the value of `x`
|
||||
// depends on the "control" value `c[0]` which becomes reactive via
|
||||
// being interleaved with `b`.
|
||||
// Therefore x should be treated as reactive too.
|
||||
return [x];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ cond: true }],
|
||||
};
|
||||
+6
-5
@@ -35,7 +35,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { unstable_useMemoCache as useMemoCache } from "react";
|
||||
function Component(props) {
|
||||
const $ = useMemoCache(6);
|
||||
const $ = useMemoCache(7);
|
||||
let a;
|
||||
if ($[0] !== props.b) {
|
||||
a = {};
|
||||
@@ -57,12 +57,13 @@ function Component(props) {
|
||||
}
|
||||
const c = t0;
|
||||
let t1;
|
||||
if ($[4] !== a) {
|
||||
if ($[4] !== c || $[5] !== a) {
|
||||
t1 = [c, a];
|
||||
$[4] = a;
|
||||
$[5] = t1;
|
||||
$[4] = c;
|
||||
$[5] = a;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
t1 = $[6];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user