svelte-mobx(或类似库)如何把两套响应式机制“桥接”起来

100 阅读2分钟

营销业务是前端要有专业判断的 最后一个阵地

javascript.plainenglish.io/why-svelte-…

简单来说,就是 “在 MobX 的响应式机制里,一旦监听到可观察数据发生变动,主动通知/调用 Svelte 的更新函数,从而让 Svelte 继续它的响应式更新。”

核心做法通常包括以下几个步骤(以常见模式为例):

  1. 创建一个能够代表 MobX 数据状态的 Svelte Store

    • 在 Svelte 生态中,一般会使用 writablereadable(来自 svelte/store)来创建一个 Store。
    • 而这个 Store 的值会和 MobX 的某个可观察对象/值挂钩。
  2. 利用 MobX 的 autorunreaction

    • 在 Svelte Store 初始化时,就用 autorun(或 reaction)去“监听” MobX 里的可观察数据。
    • 每当 MobX 数据更新,就调用 Svelte Store 的 set() 或内部的变动通知方法,把最新值同步给 Svelte Store。
  3. Svelte Store 值变动后,触发 Svelte 的响应式更新

    • 当 Svelte Store 的值变化后,Svelte 本身就会触发订阅该 Store 的组件或变量发生更新;
    • 这会让组件模板或 Svelte 的 $: 声明式代码重新执行、重新渲染 DOM。
  4. 可选的:在销毁/卸载组件时,销毁 MobX 的 reaction

    • 如果你是在一个组件里用到这个混合的 Store,那么往往需要在组件销毁或 Store 不再使用时,手动取消 MobX reaction/autorun 的订阅,以免产生内存泄漏。

底层可能的示例代码

给一个简化的示例,你可以看到这种“桥接”思路是如何写的。以下是一个伪示例,不一定跟 svelte-mobx 源码一模一样,但思路相近:

js
复制
// 1. 假设我们有一个 mobxStore,对象里有可观察值
import { observable } from 'mobx';
import { readable } from 'svelte/store';
import { autorun } from 'mobx';

const mobxStore = observable({
  count: 0,
  increment() {
    this.count++;
  }
});

// 2. 封装一个函数,把 mobxStore 转成 Svelte 的 Store
function mobxToSvelte(mobxObj, selectValueFn) {
  // selectValueFn 用于从 mobxObj 中读出要注入到 Svelte 的值
  // 比如: (store) => store.count

  return readable(selectValueFn(mobxObj), (set) => {
    // autorun: 当 mobxObj 里被观察的属性发生变更时,会执行回调
    const dispose = autorun(() => {
      const newValue = selectValueFn(mobxObj);
      set(newValue);
    });

    // 返回清理函数
    return () => dispose();
  });
}

// 3. 在 Svelte 组件里用这个 store
import { mobxToSvelte } from './bridge';

export const countStore = mobxToSvelte(mobxStore, (s) => s.count);

// Svelte 中使用
/*
<script>
  import { countStore } from './somewhere';
  $: countValue = $countStore; // 通过自动订阅 store 获取最新值
</script>

<template>
  <p>Count: {countValue}</p>
  <button on:click={() => mobxStore.increment()}>
    +1
  </button>
</template>
*/
  • 每当 mobxStore.count 更新,MobX 的 autorun 都会把最新的 mobxStore.count 传给 set()
  • set() 会让 countStore(Svelte Store)产生一个“值变了”的事件;
  • Svelte 响应式系统监听到这个 Store 值变动,就重新渲染与它相关的组件模板或代码。

为什么这样能“保留”两套响应式?

  • Svelte 一端:只认自己的 Store 或 $: 声明式语句。当 Store 的值变了,Svelte 会精确地更新 DOM。
  • MobX 一端:只需要跟踪那些“可观察数据”(observablecomputed)有没有变。
  • 桥接:利用 MobX 的 autorun/reaction,在数据变动时主动去调用 Svelte 的 Store 更新函数;反之,如果需要从 Svelte 发动作(比如点击按钮)去修改 MobX 数据,也可以直接调用 MobX store 方法(如 store.increment())。
  • 如此,就等于在 MobX 与 Svelte 之间加了一个很薄的“转换层”。

svelte-mobx 库的做法

svelte-mobx(或类似的 mobx-svelte-store)等库,实际上就是把上面手写的“桥接逻辑”包装成一个更简洁的 API,让你能够在 Svelte 中直接使用类似:

js
复制
import { observer } from 'svelte-mobx';
import { myMobxStore } from './myMobxStore';

// 直接在模板里用 $: auto-run reaction
// 或通过 observer 让组件感知 mobxStore 的变化

它们通常内部也是依赖 autorun / reactioncomputed 来观察 MobX 对象的变化,然后再触发 Svelte 的更新。

要点:Svelte 自己本身没有提供像 Vue 那样的“自动对任意对象做 Proxy 化”功能,也不会自动跟踪所有外部对象的变化。
一旦你把状态交给了 MobX,就需要额外的桥接代码,让“MobX → Svelte”这一段通讯顺畅。
svelte-mobx 做的事情,核心就是这套监听 + 更新的桥接机制。


这段代码做了什么?

ts
复制
import * as mobx from "mobx";
import { onDestroy } from "svelte";

type AutorunFn = (view: () => void) => void;

function connect(): { autorun: AutorunFn } {
    let disposer: mobx.IReactionDisposer;

    onDestroy(() => {
        disposer && disposer();
    });

    return {
        autorun: (view: () => void) => {
            disposer && disposer();
            disposer = mobx.autorun(view);
        },
    };
}

export { connect };
  1. 引入了 MobX 并从 svelte 导入了 onDestroy
  2. 定义了 connect() 函数,返回一个拥有 autorun(view) 方法的对象。
  3. 当你在组件里调用 connect() 时,它会在内部记录一次 mobx.autorun(...) 返回的 disposer(即取消订阅的函数)。
  4. onDestroy(() => disposer && disposer()):当 Svelte 组件销毁时,就自动调用 mobx.autorundisposer,从而停止跟踪,避免内存泄漏。
  5. 每次调用 autorun(view) 都会先释放之前的 Reaction,再创建新的 Reaction。

可以理解为:它只是一段让你“在 Svelte 组件里安全地使用 autorun”的封装。至于你要怎么拿到 MobX 的可观察数据、怎么让它和组件中的变量交互,还需要自己写额外的逻辑。


在 Svelte 中的使用示例

假设你有一个 mobx store:

ts
复制
// store.ts
import { observable } from 'mobx';

export const myStore = observable({
  count: 0,
  increment() {
    this.count++;
  }
});

然后在某个 Svelte 组件中使用你贴出来的 connect()

html
复制
<!-- MyComponent.svelte -->
<script lang="ts">
  import { connect } from './connect'; // 你那段代码所在文件
  import { myStore } from './store';

  // 调用 connect()
  const { autorun } = connect();

  // 使用 autorun 监听 mobx store 的变化
  autorun(() => {
    console.log('myStore.count 变化了:', myStore.count);
  });

  // 组件中的函数,触发 mobx store 的更新
  function handleClick() {
    myStore.increment();
  }
</script>

<div>
  <p>当前 count:{myStore.count}</p>
  <button on:click={handleClick}> +1 </button>
</div>

注意点

  1. 你会发现,上面并没有让 Svelte 的模板自动追踪 myStore.count 的变化。

    • 因为 Svelte 自身并不知道 myStore 是个 MobX 对象。
    • Svelte 只在编译期追踪本地的声明变量Svelte Store (writable, readable等)。
    • 你在模板里直接写 {myStore.count} 不会自动更新 UI(Svelte 不会监听到MobX的变化)。你可以在 autorun() 里手动把 count 赋给一个本地的 let 变量,然后让Svelte用这个本地变量来刷新UI。
  2. 如果你想真正地让UI自动刷新,你需要额外的桥接; 比如:

    ts
    复制
    // 伪代码: 创建一个svelte store,autorun同步MobX的值
    import { writable } from 'svelte/store';
    import { myStore } from './store';
    import { autorun } from 'mobx';
    
    export const svelteCount = writable(myStore.count);
    
    autorun(() => {
      svelteCount.set(myStore.count);
    });
    

    这样在组件里用 $svelteCount 才会自动更新 DOM。

  3. 你的 connect() 只解决了“把 Reaction 生命周期和 Svelte 组件解绑”的问题,让你可以在 Svelte 里愉快地调用 mobx.autorun() 而不用担心卸载组件时忘记清理 Reaction。


只有这么少的底层代码,为什么?

  • 因为它只专注做了最小的事情:在 Svelte 组件的销毁周期中,自动调用 mobxdisposer
  • 实际上,如果你看过像 mobx-svelte-storesvelte-mobx 之类的库,它们功能更多:会把 MobX observable 数据封装成 Svelte Store,或通过一系列高级API让你在 Svelte 模板里直接写 $: someVar = store.prop 也能自动更新。
  • 但上面这段 connect() 只是一个简单的示例/工具函数,并不包含完整的“自动桥接 MobX → Svelte”流程,就这么点代码也很正常。大多数使用 MobX + Svelte 的项目,往往会自己写一个或者用现成的封装,让这两套响应式机制“互通”。