Zustand 、Jotai和Valtio源码探析

一个核心的API:useSyncExternalStore

作用:安全地将React组件链接到外部状态管理库(如Redux、Zustand、浏览器storage),解决并发渲染下的撕裂问题

最核心的代码:

ts 复制代码
function useSyncExternalStore(subscribe, getSnapshot) {

  const [state, setState] = useState(getSnapshot());

  useEffect(() => {

    const handleStoreChange = () => {

      setState(getSnapshot());

    };

    // 1. 订阅状态变化(返回清理函数)

    const unsubscribe = subscribe(handleStoreChange);

    // 2. 返回清理函数(组件卸载时执行)

    return unsubscribe;

  }, [subscribe, getSnapshot]);

  return state;

}

演示Zustand的订阅过程

javascript 复制代码
// 1. 这是一个极其迷你的 Store
const store = {
  // 这是那个名单本子 (Set)
  listeners: new Set(),
  // ✨重点在这里:订阅函数✨
  subscribe: function(callback) {
    // 动作:把你传进来的函数(联系方式),加到本子上
    this.listeners.add(callback);
    console.log(`✅ 成功追加一个监听!现在名单里有 ${this.listeners.size} 个人。`);
    // 返回一个函数,用来取消订阅(以后再说)
    return () => this.listeners.delete(callback);
  },
  // 假装数据变了,通知大家
  setState: function() {
    console.log("📢 只有一件事:数据变了!开始挨个通知...");
    // 遍历 Set,执行每个函数
    this.listeners.forEach(run => run());
  }
};

// ==========================================
// 场景开始:两个"组件"来订阅了
// ==========================================

// 模拟组件 A(比如是页面顶部的 Header)

const componentA_Update = () => console.log("   -> 组件A收到通知:我要检查下用户名变没变");

// 模拟组件 B(比如是页面底部的 Footer)

const componentB_Update = () => console.log("   -> 组件B收到通知:我要检查下版权年份变没变");

// 动作 1:组件 A 出生了,请求订阅

store.subscribe(componentA_Update);

// 👉 结果:Set 内部现在是 { componentA_Update }


// 动作 2:组件 B 出生了,请求订阅

store.subscribe(componentB_Update);

// 👉 结果:Set 内部现在是 { componentA_Update, componentB_Update }

// ==========================================
// 动作 3:数据变了!
// ==========================================

store.setState();

演示Jotai的订阅过程

Jotai的核心区别在于"订阅是跟着Atom走的,而不是跟着Store走的"。在 Zustand 里,是你跑到大厅(Store)里喊一嗓子,所有人都会听到。 在 Jotai 里,是你分别跑到不同的房间(Atom)门口去留小纸条。

javascript 复制代码
// ==========================================
// 1. 模拟一个迷你的 Jotai Store (Provider)
// ==========================================

const jotaiStore = {
  // 这里的名单本子是【分门别类】的!
  // Key 是 atom 本身,Value 是这个 atom 专属的粉丝名单(Set)
  listeners: new Map(),
  // ✨重点在这里:订阅函数✨
  // 你必须告诉我:你要订阅【哪一个 Atom】?
  subscribe: function(atom, callback) {
    // 1. 如果这个 atom 还没人关注过,先给它建个新的空名单
    if (!this.listeners.has(atom)) {
      this.listeners.set(atom, new Set());
    }
    
    // 2. 拿到这个 atom 专属的名单
    const fans = this.listeners.get(atom);
    // 3. 把回调加上去
    fans.add(callback);
    console.log(`✅ 成功关注!Atom [${atom.key}] 现在有 ${fans.size} 个粉丝。`);
    return () => fans.delete(callback);

  },

  // 假装这一颗具体的 Atom 变了
  setAtom: function(atom, newValue) {
    console.log(`📢 只有一件事:Atom [${atom.key}] 的值变成了 ${newValue}!开始通知粉丝...`);
    // 1. 只找这个 Atom 的粉丝
    const fans = this.listeners.get(atom);
    if (fans) {
      // 2. 精准通知,闲杂人等根本不会被吵醒
      fans.forEach(run => run());
    } else {
      console.log("   (尴尬: 这个 atom 没有任何人订阅,无事发生)");
    }
  }
};

// ==========================================
// 场景开始:定义两个独立的 Atom
// ==========================================
const countAtom = { key: 'CountAtom', init: 0 }; // 房间 A
const textAtom  = { key: 'TextAtom',  init: 'hi' }; // 房间 B
// ==========================================
// 模拟组件
// ==========================================

// 模拟组件 A:只关心数字

// 对应代码: useAtom(countAtom)

const componentA_Update = () => console.log("   -> 组件A收到通知:我订阅的 Count 变了,我要重渲染!");

  


// 模拟组件 B:只关心文字

// 对应代码: useAtom(textAtom)

const componentB_Update = () => console.log("   -> 组件B收到通知:我订阅的 Text 变了,我要重渲染!");

  


// 动作 1:组件 A 订阅 countAtom

jotaiStore.subscribe(countAtom, componentA_Update);

  


// 动作 2:组件 B 订阅 textAtom

jotaiStore.subscribe(textAtom, componentB_Update);

  


// ==========================================

// 动作 3:修改 TextAtom (比如输入框打字)

// ==========================================

jotaiStore.setAtom(textAtom, 'hello world'); 

  


// 👉 结果:

// 只有组件 B 会打印日志。

// 组件 A 正在睡大觉,根本不知道发生了什么。这就是"原子化订阅"的威力。

演示Valtio的订阅过程

对于 Valtio,它的核心在于 "间谍 (Proxy)" 和 "快照 (Snapshot)"。它的订阅既不是去大厅喊(Zustand),也不是去房间留条(Jotai),而是 "给对象装个窃听器"。你以为你在随意修改对象 state.count++,其实你改的是一个装了窃听器的 Proxy。这个窃听器会自动通知 React:"嘿,版本号变了,快来拿新照片(Snapshot)"。

javascript 复制代码
// ==========================================

// 1. 模拟一个迷你的 Valtio Proxy

// ==========================================

  


// 这是我们的"窃听器中心"

// Key 是 proxy 对象本身,Value 是订阅者名单

const listenersMap = new WeakMap();

  


// 这是我们的"版本记录中心"

const versionMap = new WeakMap();

  


// ✨ 造一个带窃听器的对象

function proxy(initialObj) {

  // 初始版本号 0

  let version = 0;

  

  // 真正的核心:拦截器

  const p = new Proxy(initialObj, {

    

    // 拦截写入:你以为只有赋值,其实还触发了通知

    set(target, prop, value) {

      target[prop] = value;

      

      // 1. 升级版本号 (Version Increment)

      version++;

      versionMap.set(p, version);

      

      console.log(`📢 监测到写入:${prop} = ${value} (当前版本: v${version})`);

      

      // 2. 只有在此刻,才通知订阅者

      notify(p);

      return true;

    }

  });

  


  // 初始化记录

  listenersMap.set(p, new Set());

  versionMap.set(p, version);

  

  return p;

}

  


// 辅助函数:通知

function notify(p) {

  const fans = listenersMap.get(p);

  fans.forEach(cb => cb());

}

  


// ==========================================

// 场景开始:创建一个可变状态

// ==========================================

const state = proxy({ count: 0, text: 'hello' });

  


// ==========================================

// 模拟组件 (使用 useSnapshot)

// ==========================================

  


// 模拟组件 A

const componentA_Update = () => {

    // 每次组件渲染,都会检查版本号

    const currentVer = versionMap.get(state);

    

    // 如果版本变了,React 就会拿到一个新的 snapshot 从而更新

    console.log(`   -> 组件A收到通知:版本变成 v${currentVer} 了,我要去拉取新快照!`);

};

  


// 动作 1:组件订阅

// 在 Valtio 里,这一步通常发生在 useSnapshot 内部

const fans = listenersMap.get(state);

fans.add(componentA_Update);

  


// ==========================================

// 动作 2:直接修改属性 (Mutable!)

// ==========================================

console.log("--- 准备修改 count ---");

state.count++; 

// 👉 结果:控制台打印 "监测到写入..." -> "组件A收到通知..."

  


console.log("--- 准备修改 text ---");

state.text = 'world';

// 👉 结果:同样触发通知。注意:这里是对象级别的通知。

// (真实的 Valtio 还有更高级的属性级优化,但原理就是这个 Loop)

  

核心对比

  • Zustand: store.subscribe(cb)

• 比喻:大喇叭广播。

• 机制:所有变更都会触发 cb,必须由 CB 内部自己决定是不是真的要更新 (Selector)。

• 适用:粗粒度、低频、全局状态。

  • Jotai: store.subscribe(atom, cb)

• 比喻:房间门口留条。

• 机制:只有 指定 Atom 变更才会触发 cb,不需要 Selector,天然精准。

• 适用:细粒度、高频、复杂依赖图(如节点编辑器)。

  • Valtio: subscribe(proxy, cb)

• 比喻:装了窃听器。

• 机制:写的时候自动触发通知,读的时候检查版本号 (Version Check)。哪怕你改的是深层嵌套属性 state.a.b.c = 1,也会通过递归 Proxy 冒泡上来触发更新。

• 适用:极高频交互、深层嵌套数据、游戏/3D开发(喜欢 Mutable 写法的场景)。

相关推荐
LawrenceLan17 小时前
Flutter 零基础入门(八):Dart 类(Class)与对象(Object)
前端·flutter
小oo呆17 小时前
【学习心得】Python的Pydantic(简介)
前端·javascript·python
funnycoffee12317 小时前
F5 Big IP如何设置web和SSH登录的白名单
前端·tcp/ip·ssh
JarvanMo17 小时前
国产 App,求你放过我的 iPhone 电量吧!
前端
先飞的笨鸟17 小时前
2026 年 Expo + React Native 项目接入微信分享完整指南
前端·ios·app
angelQ17 小时前
Vercel部署:前后端分离项目的整体部署流程及问题排查
前端·javascript
AI前端老薛17 小时前
CSS实现动画的几种方式
前端·css
晨米酱17 小时前
轻量级 Git Hooks 管理工具 Husky
前端·代码规范
Jing_Rainbow17 小时前
【 前端三剑客-35 /Lesson58(2025-12-08)】JavaScript 原型继承与对象创建机制详解🧬
前端·javascript·面试