核心内部
本指南对于那些希望理解 Jotai 核心实现的人非常有益。它并不是核心实现的完整示例,而是一个简化版本。它受到了 Daishi Kato(@dai_shi)的一系列推文的启发。
第一版
让我们从一个简单的例子开始。原子就是一个将返回配置对象的函数。我们使用 WeakMap 来映射原子及其状态。 WeakMap 不会在内存中保留其键,所以如果一个原子被垃圾收集,它的状态也会被垃圾收集。这有助于避免内存泄漏。
import { useState, useEffect } from 'react'// atom 函数返回一个包含初始值的配置对象export const atom = (initialValue) => ({ init: initialValue })// 我们需要跟踪原子的状态。// 我们使用 weakmap 来避免内存泄漏const atomStateMap = new WeakMap()const getAtomState = (atom) => {let atomState = atomStateMap.get(atom)if (!atomState) {atomState = { value: atom.init, listeners: new Set() }atomStateMap.set(atom, atomState)}return atomState}// useAtom 钩子返回当前值的元组// 和一个用于更新原子值的函数export const useAtom = (atom) => {const atomState = getAtomState(atom)const [value, setValue] = useState(atomState.value)useEffect(() => {const callback = () => setValue(atomState.value)// 同一个原子可以在多个组件中使用,所以我们需要// 一直监听原子的状态变化,直到组件被卸载。atomState.listeners.add(callback)callback()return () => atomState.listeners.delete(callback)}, [atomState])const setAtom = (nextValue) => {atomState.value = nextValue// 让所有订阅的组件知道原子的状态已经改变atomState.listeners.forEach((l) => l())}return [value, setAtom]}
这是一个使用我们简化的原子实现的例子。计数器示例
参考推文:揭秘 jotai 的内部
第二版
等一下!我们可以做得更好。在 Jotai 中,我们可以创建派生原子。派生原子是依赖于其他原子的原子。
const priceAtom = atom(10)const readOnlyAtom = atom((get) => get(priceAtom) * 2)const writeOnlyAtom = atom(null, // 传递 `null` 作为第一个参数是一种约定(get, set, args) => {set(priceAtom, get(priceAtom) - args)},)const readWriteAtom = atom((get) => get(priceAtom) * 2,(get, set, newPrice) => {set(priceAtom, newPrice / 2)// 你可以同时设置尽可能多的原子},)
为了跟踪所有的依赖项,我们需要在原子的状态中添加一个属性。假设原子 X 依赖于原子 Y, 所以当我们更新原子 Y 时,我们也更新原子 X。这被称为依赖跟踪。
const atomState = {value: atom.init,listeners: new Set(),dependents: new Set(),}
我们现在需要创建用于读取和写入原子的函数,这些函数可以处理更新依赖原子的状态。
import { useState, useEffect } from 'react'export const atom = (read, write) => {if (typeof read === 'function') {return { read, write }}const config = {init: read,// read 函数中的 get 是用来读取原子值的。// 它是响应式的,读取依赖项会被跟踪。read: (get) => get(config),// write 函数中的 get 也是用来读取原子值的,但它不会被跟踪。// write 函数中的 set 是用来写入原子值的,// 它将调用目标原子的 write 函数。write:write ||((get, set, arg) => {if (typeof arg === 'function') {set(config, arg(get(config)))} else {set(config, arg)}}),}return config}// 与上面相同,但状态有一个额外的属性:dependentsconst atomStateMap = new WeakMap()const getAtomState = (atom) => {let atomState = atomStateMap.get(atom)if (!atomState) {atomState = {value: atom.init,listeners: new Set(),dependents: new Set(),}atomStateMap.set(atom, atomState)}return atomState}// 如果原子是基本类型,我们返回它的值。// 如果原子是派生的,我们读取父原子的值// 并将当前原子添加到父原子的依赖集合中(递归)。const readAtom = (atom) => {const atomState = getAtomState(atom)const get = (a) => {if (a === atom) {return atomState.value}const aState = getAtomState(a)aState.dependents.add(atom) // XXX 只添加return readAtom(a) // XXX 没有缓存}const value = atom.read(get)atomState.value = valuereturn value}// 如果 atomState 被修改,我们需要通知所有的依赖原子(递归)// 现在为所有依赖于此原子的组件运行回调const notify = (atom) => {const atomState = getAtomState(atom)atomState.dependents.forEach((d) => {if (d !== atom) notify(d)})atomState.listeners.forEach((l) => l())}// writeAtom 使用必要的参数调用 atom.write 并触发 notify 函数const writeAtom = (atom, value) => {const atomState = getAtomState(atom)// 'a' 是 atomStateMap 中的某个原子const get = (a) => {const aState = getAtomState(a)return aState.value}// 如果 'a' 与 atom 相同,更新值,通知该原子并返回// 否则为 'a' 调用 writeAtom(递归)const set = (a, v) => {if (a === atom) {atomState.value = vnotify(atom)return}writeAtom(a, v)}atom.write(get, set, value)}export const useAtom = (atom) => {const [value, setValue] = useState()useEffect(() => {const callback = () => setValue(readAtom(atom))const atomState = getAtomState(atom)atomState.listeners.add(callback)callback()return () => atomState.listeners.delete(callback)}, [atom])const setAtom = (nextValue) => {writeAtom(atom, nextValue)}return [value, setAtom]}
这是一个使用我们的派生原子实现的例子。派生计数器示例
参考推文:支持派生原子