# 快要被 createMemo 搞疯了 (March 1, 2025)

以前一直以为 SolidJS 的`createMemo`是和 Vue 的`computed`一样的东西，结果今天发现不是！

+ [createMemo - Basic reactivity - SolidDocs](https://docs.solidjs.com/reference/basic-reactivity/create-memo)
+ [computed - Reactivity API: Core - Vue.js](https://vuejs.org/api/reactivity-core.html#computed)

SolidJS 的 `createMemo`是 eagerly evaluating 的，也就是说，它是和`createEffect`一类，创建即调用的。它[文档](https://docs.solidjs.com/concepts/derived-values/memos)里说：

> They are similar to derived signals in that they are reactive values that automatically re-evaluate when their dependencies change.
>

明明是用来替代 [derived signals](https://docs.solidjs.com/concepts/derived-values/derived-signals) 的啊！这样看明明更像 effect 而不是 derived

而且一旦有 dependency 变动了，它是立即 evaluate 然后来根据 equal 与否来判断是否 invalidate 的，而不是按需来。按需来的叫`createLazyMemo`：[createLazyMemo - @solid-primitives/memo - Solid Primitives](https://primitives.solidjs.community/package/memo/#createlazymemo)

---

为什么看这个，是因为我最近在处理 hmr 库的一些奇奇怪怪的 bug。其中就在`create_memo`这边没转过来：

1. 如果它是 lazily evaluate 的，那为了让各种 effect 的值保持最新，它就必须得是 eagerly invalidate 的（这也是我之前的实现）这会导致它比起普通的 signal 就丢失了在 equal 时不 notify 的能力
2. 如果它是 eagerly evaluate 的（就像 SolidJS 这样），它确实就可以 lazily invalidate 了，因为它可以先比较是否 equal 再 notify 了，不会立马 invalidate，但这样会导致它可能会有一些不必须的 evaluate（比如可能它的 subscriber 正好已经不需要用到它了，即使这些 subscribers 重新执行了，也不会再 observe 这个 signal）

然后就觉得其实这东西是不是没有完美的解法？这是一个权衡：

1. Effect 这边不可能不重新执行就知道是否还需要某个 Signal
2. 一个 Derived 在被 trigger 时不可能不重新运行就知道它的结果会不会 equal

一个办法是，可以像`createMemo`那样，当被 trigger 后就立即 recompute，但是第一次 compute 推迟到第一次被调用时。这样相当于`autorun=False`的`create_effect`+ cache 而已。或者说这是我的底线，因为如果 API 是这样实现的，用户也可以在创建了这个 Memoized 之后立即 call 它，来达到和 SolidJS 的版本一样的效果，但是反过来不行。

所以其实可以暴露一个参数，类似

```python
optimization: "lazy-recompute" | "lazy-invalidate" | False
```

其中 lazy-recompute 代表之前的行为，lazy-invalidate 是上面说的这种，False 代表不检查

---

刚刚想了下，其实 SolidJS 的 createMemo 等效于：

```python
class _:
    var = State(0)
    derived = State(0)

    def __init__(self):
        @create_effect
        def _():
            self.derived = self.var * 2  # 假设就是乘二的关系
        # 但是好像需要保存一个 这个 effect 的引用，不然会被 gc 掉，但这里就不展示这些细节了
```

而我上面提到的中间形态等效于：

```python
class _:
    var = State(0)
    _derived = State(0)

    @cache  # 保证只运行一次，而且只在第一次需要时调用
    def ensure_effect(self):
        @create_effect
        def _():
            self._derived = self.var * 2
    
    @property
    def derived(self):
        self.ensure_effect()
        return self._derived
```

而现在是这样的 API：

```python
class _:
    var = State(0)

    @memoized_property
    def derived(self):
        return self.var * 2
```

未来允许给这个装饰器加参数，比如`memoized_property(optimization="lazy-invalidate")`获得上面第二种的效果。

---

其实刚刚忘说了，Svelte 5 的反应式原语好像略有不同，我略微探索了一下（顺带一提，顺便不小心发现了他们一个 bug：[`$derived` unexpectedly reevaluate · Issue #15414 · sveltejs/svelte](https://github.com/sveltejs/svelte/issues/15414)），观察到：

1. 如果某个`$derived`没有至少一个`$effect`用到它，它就不会被执行，这说明它是 lazy-evaluate 的
2. 虽然`$effect`第一次执行时，是从浅到深调用`$derived`们的，但是每次更新最底层的`$derived`先调用，再往外调用的，这说明至少最外层的`$effect`不是 eagerly-trigger 的

所以我怀疑它是一个 2-pass 的机制，当一个 Signal notify 时，它 invalidate 了某个`$derived`（相当于我这里的 memo），然后一层一层向上 invalidate，但是 invalidate 并不会立即删掉原来的 cache 的值，而只是把自己 mark 为 stale 状态（或者叫 dirty 吧），这样冒泡到最浅层，这就是“正向传播”

1. 到达最外层的时候，如果是 memo，就说明这个 memo 没被用上。（可以形象地理解为一个`$effect`就是在最外层“拉”一个反应性的变量的，如果没有人“拉”，那它就不会被求值。
2. 如果是个 effect，那这个 effect 并不会立即执行，而是会看它到底被谁 trigger 了。是全是 memo 还是也有 signal。只有被 signal 触发的才说明必须要 invalidate。否则就逐个排查每个 memo，看这些 memo 是否改变了值（相当于看每个 memo 作为一个 signal 的性质），如果改变了，那这个 effect 就得 reevaluate 了

这就是 effect 的“反向传播”。那如何知道某个 memo 是否改变了呢？

1. 如果 notify 它的 dependencies 中有 signal，那它就得 recompute，然后比较值是否改变了
2. 如果 notify 它的 dependencies 都是 memo，那就得先看这些 memo 有没有 change 了

这就是 memo 的反向传播

（写到这里我意识到，可能叫这些 memo 为 derived 更合适一些。今后我实现的时候会注意）



这么看来，effect 具有一个反向传播性质，这个确实我还没 implement，而且似乎 SolidJS 之类的也没这样的机制。我之前参考的更多是我见识到的 SolidJS 的一小部分，所以设计还很不成熟。

这么看，其实反应式编程有三种角色：

1. Signal，具有 track 跟踪调用者（自动绑定 subscribers）、notify（正向传播）的功能
2. Effect，具有 invalidate（被 notify 时调用）、check/determine（还没想好叫什么名字，总之就是invalidate后会调用，反向传播）的功能
3. Derived，具有以上二者的性质，Effect 是正向传播的终点，而 Signal 其实是反向传播的终点

这么看这和神经网络太像了！这不就是输入层、输出层和中间层嘛！！

---

原谅我写文章总是这么意识流。今后有机会肯定会写篇系统的文章跟大家讲讲的！现在如果有人对反应式编程感兴趣的话，我还是会推荐 Ta 去读读 Vue 的这篇文章：

[Reactivity in Depth - Vue.js](https://vuejs.org/guide/extras/reactivity-in-depth) 的前三小节：

+ What is Reactivity?
+ How Reactivity Works? （原标题是 How Reactivity Works in Vue，但其实讲的是 in JavaScript 而非局限于 Vue）
+ Runtime vs. Compile-time Reactivity

这篇文章讲的真的很中肯，提到了反应式编程的鼻祖，也讲到了新潮，而且说到了 why 和 why not，看得出来每个设计都是非常深思熟虑的结果。