React 编译器理论与Reactivity

React编译器实现了许多传统编译器转换,通常需要一些编译器理论背景才能理解。在本文中,我将尝试用例子更容易理解的方式解释编译器传递的一个称为"静态单赋值形式"(Static Single Assignment form,简称SSA)的过程。

如果你想知道React编译器是什么,我建议阅读我们最近的更新文章了解一些背景信息。

考虑这个简单的React组件:

function Component({ colours }) {
    let styles = { colours };
    return <Item styles={styles} />;
}

我们可以很容易地对其进行记忆化处理:

function Component(props) {
    const $ = useMemoCache(2);
    const { colours } = props;
    let t0;

    if ($[0] !== colours) {
        t0 = { colours };

        $[0] = colours;
        $[1] = t0;
    } else {
        t0 = $[1];
    }

    const styles = t0;
    return <Item styles={styles} />;
}

编译器可以追踪创建并传递为props的styles对象。

不要太担心useMemoCache钩子,它是编译器用于缓存值的内部API。将$视为一个数组。

React编译器也可以对JSX进行记忆化处理,但为了简洁起见,我在本文中省略了这部分内容。

现在,假设你想根据条件重构样式:

function Component({ colours, hover, hoverColours }) {
    let styles;
    if (!hover) {
        styles = { colours };
    } else {
        styles = { colours: hoverColours };
    }
    return <Item styles={styles} />;
}

对于编译器来说,记忆化styles对象变得更具挑战性,因为它不再是一个单一语句。它跨越了多个语句,并涉及到控制流 - stylesifelse块中都被创建。

编译器仍然可以跟踪跨两个块的样式创建,并像这样进行记忆化处理:

function Component(props) {
    const $ = useMemoCache(4);
    const { hover, colours, hoverColours } = props;
    let styles;

    if ($[0] !== hover || $[1] !== colours || $[2] !== hoverColours) {
        if (!hover) {
            styles = { colours };
        } else {
            styles = { colours: hoverColours };
        }

        $[0] = hover;
        $[1] = colours;
        $[2] = hoverColours;
        $[3] = styles;
    } else {
        styles = $[3];
    }

    return <Item styles={styles} />;
}

这能够工作,但不是理想的,因为如果hovercolourshoverColours中的任何一个发生变化,就会使记忆化值无效。这太“粗粒度”了。我们能不能做得更好呢?

跟踪值,而不是变量

一个核心的直觉是,我们将在if块中单独记忆化值,与else块中的值分开。它们是独立的“值”(独立的“对象”),只是被相同的变量标识符(styles)引用。

考虑稍微修改过的先前的例子,通过给它们分配不同的标识符来单独跟踪值:

let styles;
if (!hover) {
  t0 = { colours };              // <-- separate value
} else {
  t1 = { colours: hoverColours}; // <-- separate value
}
styles = choose(t0 or t1);

现在,很明显我们可以单独记忆化t0t1。你也意识到我们需要在t0t1之间进行选择并将其正确分配给styles,但现在我们先忽略这一点。

编译器可以在它们各自的块中记忆化这些值:

if (!hover) {
    if ($[0] !== colours) {
        t0 = {
            colours,
        };
        $[0] = colours;
        $[1] = t0;
    } else {
        t0 = $[1];
    }
} else {
    if ($[2] !== hoverColours) {
        t1 = {
            colours: hoverColours,
        };
        $[2] = hoverColours;
        $[3] = t1;
    } else {
        t1 = $[3];
    }
}
styles = choose(t0 or t1)

这比先前的例子更加“细粒度”。

复杂性在哪里?

但是,等等,我们只是在记忆化在其创建范围内的值,有什么难的?

嗯,让我们考虑另一个例子:

function Component({ colours, hover, hoverColours }) {
    let styles;
    if (!hover) {
        styles = { colours };
    } else {
        styles = { colours: hoverColours };
    }
    styles.height = "large"; // <-- modifying styles object
    return <Item styles={styles} />;
}

在上面的例子中,我们在if-else块之后通过添加一个名为height的新属性修改了styles对象。在这种情况下,不能安全地在if块和else块内部分别记忆化值了。

我们不能在记忆化之后修改一个值。这不是因为它在性能上不够优化,而是因为在重新渲染期间会导致不正确的行为。花一分钟思考一下这种行为在实践中可能如何显现。

我们需要一种方式来跟踪值在流动中的方式,而不仅仅是在创建它们的范围内简单地进行记忆化。

有人可能会争辩说,你不应该编写这样的代码。但是,本地突变在JavaScript中是非常自然的,而且有很多React代码是这样编写的,我们需要高效地进行编译。

跟踪流动

还记得我们之前忽略的“choose”函数吗?这让编译器在if-else块之间跟踪值的流动!

if (!hover) {
  t0 = { colours };
} else {
  t1 = { colours: hoverColours};
}
styles = choose(t0 or t1); // <-- tracks values after control flow
styles.height = 'large';

现在,代码(或者更准确地说,编译器的中间表示)告诉编译器styles要么是t0,要么是t1,并且修改styles等同于修改值t0t1

编译器现在可以推断styles只能在这个更粗粒度的水平上进行记忆化处理,就像这样:

if ($[0] !== hover || $[1] !== colours || $[2] !== hoverColours) {
    if (!hover) {
        styles = {
            colours,
        };
    } else {
        styles = {
            colours: hoverColours,
        };
    }

    styles.height = "large";
    $[0] = hover;
    $[1] = colours;
    $[2] = hoverColours;
    $[3] = styles;
} else {
    styles = $[3];
}

编译器理论

总结一下,我们讨论了使用临时标识符单独跟踪值以及使用“choose”函数在控制流中跟踪值的方法。

有趣的是,一个叫做静态单赋值形式(SSA)的经典编译器转换恰好做到了这一点!通过创建新的临时值跟踪新值和重新分配是SSA变换的核心部分。我们先前讨论的“choose”函数只是在SSA形式中定义的“phi”(Φ)函数。

React编译器使用的确切SSA变换来自出色的简单且高效的静态单赋值形式构建论文。

如果你对React编译器中的编译器理论有更多兴趣,可以看看其他标记的文章

2024-03-07

访问量 54

扫码关注公众号“前端微志”

第一时间获取新周刊

预览图片