Recoil 還在實驗階段,不能在生產環境使用。目前文章分析的版本是 0.0.13css
Hooks 組件的狀態管理。目前不能在類組件裏面使用。使用 useHooks 讀寫狀態和訂閱組件。node
支持 ts。react
向後兼容 React,支持 React 並行模式。git
並行模式實際上不是把頁面渲染和響應用戶交互放在不一樣的線程去並行執行,而是把渲染任務分紅多個時間片,在用戶輸入的時候能夠暫停渲染,達到近似並行的效果。github
Recoil 的狀態和衍生狀態都既能夠是同步的也能夠是異步的,可結合 <suspense>
處理異步狀態,也能夠用 recoil 暴露的 loadable。redux
React
<suspense>
用於代碼分片和異步獲取數據apiconst Clock = React.lazy(() => { console.log("start importing Clock"); return import("./Clock"); }); <Suspense fallback={<Loading />}> { showClock ? <Clock/> : null} </Suspense> 複製代碼
分散(原子化)的狀態管理,打平狀態樹,實現精確的組件更新通知。這樣能夠避免一處更新,全量渲染。能夠對比 Redux 的單一 store ,在 select state 的時候就須要自頂向下逐層讀取,一個 state 改變,所有訂閱 store 的組件都會收到更新通知。promise
Recoil state tree 與組件樹正交,正交線就是使用 Recoil state 的組件爲點連成的線,減少了 state tree 與組件樹的接觸面,也就說 state 只會影響訂閱它的組件。並且,正交線上的不一樣的組件屬於不一樣 state 的子節點,state 之間不會相互影響其餘 state 訂閱的組件。緩存
Recoil state tree 與組件樹正交 [1] :安全
Recoil demo:"shope cart"
實現購物車功能,獲取商品列表、添加商品到購物車、提交訂單。這裏面涉及到在商品列表裏面添加到購物車時,購物車組件和商品列表組件要共享購物車狀態,訂單組件要和購物車組件共享訂單狀態,還有請求商品列表和提交訂單請求涉及到的異步狀態的處理。下面是這個 demo 的預覽圖:
先來實現簡單的獲取商品列表、提交到購物車、添加訂單的功能。提交訂單的功能在後面單獨文字解釋原理。
Src/Store/atom.ts
import {atom} from 'recoil'
import { getProductList } from '../service'
import { ProductItem, CartState, OrderItem, OrderState } from '../const'
// 商品 state
export const productAtom = atom<Array<ProductItem>>({
key: 'productState',
default: (async () => {
const res: any = await getProductList()
return res.data.products
})() // 返回 promise
})
// 購物車 state
export const cartAtom = atom<CartState>({
key: 'cartState',
default: []
})
// 訂單 state
export const orderAtom = atom<OrderState>({
key: 'orderState',
default: []
})
複製代碼
Src/store.selector.ts
import { orderAtom } from './atoms'
import { selector} from 'recoil'
// 計算訂單總價,屬於訂單狀態的衍生狀態
export const myOrderTotalCost = selector<number>({
key: 'myOrderTotalPrice',
get: ({ get }) => {
const order = get(orderAtom)
return order.reduce((total, orderItem) => total + orderItem.price * orderItem.quantity, 0)
}
})
複製代碼
Src/store/hooks.ts
import { useEffect, useState } from 'react'
import {
useRecoilState,
useSetRecoilState,
useResetRecoilState,
Loadable,
useRecoilValue,
useRecoilStateLoadable,
RecoilState,
SetterOrUpdater
} from 'recoil'
import { cartAtom, orderAtom, orderWillSubmitAtom, orderIDHadSubmitAtom } from './atoms'
import {submitOrderRes} from './selector'
import {produce} from 'immer'
import { ProductItem, CartItem, CartState, OrderItem, OrderState, LoadableStateHandler } from '../const'
// 添加商品到 cart
export function useAddProductToCart() {
const [cart, setCart] = useRecoilState<CartState>(cartAtom)
const addToCart = (item: ProductItem) => {
const idx = cart.findIndex(cartItem => item.id === cartItem.id)
if (idx === -1) {
const newItem = {...item, quantity: 1}
setCart([...cart, newItem])
} else {
setCart(produce(draftCart => {
const itemInCart = draftCart[idx]
itemInCart.quantity ++
}))
}
}
return [addToCart]
}
// 減小 cart 裏的商品的數量
export function useDecreaseProductIncart() {
const setCart = useSetRecoilState<CartState>(cartAtom)
const decreaseItemInCart = (item: CartItem) => {
setCart(produce(draftCart => {
const {id} = item
draftCart.forEach((item: CartItem, idx: number, _draftCart: CartState) => {
if(item.id === id) {
if (item.quantity > 1) item.quantity --
else if (item.quantity === 1) {
draftCart.splice(idx, 1)
}
}
})
}))
}
return [decreaseItemInCart]
}
// 刪除 cart 裏的商品
export function useRemoveProductIncart() {
const setCart = useSetRecoilState<CartState>(cartAtom)
const rmItemIncart = (item: CartItem|ProductItem) => {
setCart(produce(draftCart => {
draftCart = draftCart.filter((_item: CartItem) => _item.id !== item.id)
return draftCart
}))
}
return [rmItemIncart]
}
// 將 cart 裏的商品加到訂單
export function useAddProductToOrder() {
const setOrder = useSetRecoilState<OrderState>(orderAtom)
const [rmItemIncart] = useRemoveProductIncart()
const addToOrder = (item: CartItem) => {
setOrder(produce(draftOrder => {
draftOrder = [...draftOrder, {...item, orderID: Math.random()}]
return draftOrder
}))
// 從購物車刪除掉
rmItemIncart(item)
}
return [addToOrder]
}
複製代碼
這裏只給出商品列表組件,限於篇幅,購物車和訂單組件訂閱 Recoil state 的方法與商品列表組件相似
src/pages/product.tsx
import React from 'react'
import {useRecoilValueLoadable, useRecoilValue} from 'recoil'
import {productAtom, cartAtom} from '../store/atoms'
import {useAddProductToCart, useRemoveProductIncart} from '../store/hooks'
import { ProductItem } from '../const'
import '../style/products.css'
function ProductListLoadable(): JSX.Element {
// productsLoadable 會被緩存,即商品請求結果會被緩存,
// 重複使用 useRecoilValueLoadable(productAtom) 不會發起重複請求
const productsLoadable = useRecoilValueLoadable(productAtom)
const cart = useRecoilValue(cartAtom)
const [addToCart] = useAddProductToCart()
const [rmItemInCart] = useRemoveProductIncart()
// 能夠用 React 的 Suspense 代替下面的 switch 來處理異步狀態
switch (productsLoadable.state) {
case 'hasValue':
const products: Array<ProductItem> = productsLoadable.contents
return <div> { products .map((product:ProductItem) => <div key={product.id} className="product-item"> <div><img src={product.img} style={{width: '60px'}} alt=''/></div> <div>{product.name}</div> <div>{product.price} 元</div> { cart.findIndex( itemCart => itemCart.id === product.id ) === -1 ? <div className='add-to-cart' onClick={() => addToCart(product)}> 加入購物車 </div> : <div className='rm-in-cart' onClick={() => rmItemInCart(product)}> 從購物車刪除 </div> } </div> ) } </div>
case 'hasError':
return <div>請求出錯</div>
case 'loading':
default:
return <div style={{textAlign: 'center'}}>正在加載中......</div>
}
}
export default function ProductList() {
return <div> <h3> <i className="fas fa-store-alt"></i> 商品列表 </h3> <ProductListLoadable/> </div>
}
複製代碼
代碼裏面省略了提交訂單的部分,這裏單獨文字講述利用 Recoil selector 實現這部分功能的原理:由於 selector 是一個純函數,並且 selector 會訂閱其餘 atom 或者 selector ,當訂閱的數據發生變化,selector 會自動執行 get,返回結果,因此能夠定義一個待提交的訂單的 atom 叫作 orderWillSubmitAtom
,當向這個 atom 添加訂單,那麼依賴這個atom 的 selector 會自動獲取這個訂單並執行訂單提交請求,最後咱們在定義一個 useHooks,用於處理這個 selector 的返回值。因此最後提交訂單的這個功能,只須要把提交訂單到 orderWillSubmitAtom
的 api 暴露給 UI 組件便可,達到 UI 與邏輯分離。
下面來結合源碼分析核心 api
<RecoilRoot/>
用法:
<RecoilRoot>
<App/>
</RecoilRoot>
複製代碼
Recoil 狀態的上下文,能夠有多個 <RecoilRoot/>
共存,若是幾個<RecoilRoot/>
嵌套,那麼裏面 <RecoilRoot/>
的 Recoil state 會覆蓋外面的同名的 Recoil state 。Recoil state 的名字由 key 肯定,也就是atom
和 selector
的 key 值。在一個 <RecoilRoot/>
中每一個 atom
和 selector
的 key 值應該是惟一的。
<RecoilRoot/>
使用 React Context 初始化 Recoil state 上下文:
// 中間省略部分代碼
const AppContext = React.createContext<StoreRef>({current: defaultStore});
const useStoreRef = (): StoreRef => useContext(AppContext);
function RecoilRoot({ initializeState_DEPRECATED, initializeState, store_INTERNAL: storeProp, // For use with React "context bridging" children, }: Props): ReactElement {
// 中間省略部分代碼
const store: Store = storeProp ?? {
getState: () => storeState.current,
replaceState,
getGraph,
subscribeToTransactions,
addTransactionMetadata,
};
const storeRef = useRef(store);
storeState = useRef(
initializeState_DEPRECATED != null
? initialStoreState_DEPRECATED(store, initializeState_DEPRECATED)
: initializeState != null
? initialStoreState(initializeState)
: makeEmptyStoreState(),
);
// ...
// Cleanup when the <RecoilRoot> is unmounted
useEffect(
() => () => {
for (const atomKey of storeRef.current.getState().knownAtoms) {
cleanUpNode(storeRef.current, atomKey);
}
},
[],
);
return (
<AppContext.Provider value={storeRef}> <MutableSourceContext.Provider value={mutableSource}> <Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} /> {children} </MutableSourceContext.Provider> </AppContext.Provider>
);
}
module.exports = {
useStoreRef,
// 。。。
RecoilRoot,
// 。。。
};
複製代碼
<RecoilRoot/>
包裹的子組件能調用 useStoreRef
獲取到放在 context value 裏的 store,源碼裏有兩層 Context.provider,主要看第一層 <AppContext.Provider value={storeRef}>
。
第二層
<MutableSourceContext.Provider value={mutableSource}>
是爲了在將來兼容 React 併發模式而新近增長的,它在跨 React 根組件共享 Recoil 狀態時,從 Recoil 狀態上下文中分離出可變源以單獨保存(Separate context for mutable source #519
),可變源就是指除開 React state、Recoil state 這些不可變狀態以外的數據來源,好比 window.location 。這個和 React 新 APIuseMutableSource()
有關,useMutableSource()
是爲了在 React 的併發模式下可以安全和高效的訂閱可變源而新加的,詳情移步 useMutableSource RFC。
<AppContext.Provider value={storeRef}>
裏面 storeRef
就是 Recoil state 上下文,它由React.useRef()
生成,也就是把 Recoil state 放在 ref.current
,因此子組件獲取 store 的方式就是使用 useStoreRef().current
當 <RecoilRoot/>
卸載時清除 Recoil state 避免內存泄漏:
// Cleanup when the <RecoilRoot> is unmounted
useEffect(
() => () => {
for (const atomKey of storeRef.current.getState().knownAtoms) {
cleanUpNode(storeRef.current, atomKey);
}
},
[],
);
複製代碼
還有一個組件 <Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} />
用於在每次更新 Recoil state 的時候通知組件更新,具體的原理下面會講到。
atom
、selector
用法:
const productAtom = atom({
key: 'productState',
default: []
})
複製代碼
const productCount = selector({
key: 'productCount',
get: ({get}) => {
const products = get(productAtom)
return products.reduce((count, productItem) => count + productItem.count, 0)
},
set?:({set, reset, get}, newValue) => {
set(productAtom, newValue)
}
})
複製代碼
atom
即 Recoil state。atom
的初始值在 default 參數設置,值能夠是 肯定值、Promise、其餘 atom、 selector。selector
是純函數,用於計算 Recoil state 的派生數據並作緩存。selector
是 Recoil derived state (派生狀態),它不只僅能夠從 atom
衍生數據,也能夠從其餘 selector
衍生數據。 selector
的屬性 get 的回調函數 get 的參數*(參數爲 atom或者selector)*會成爲 selector
的依賴 ,當依賴改變的時候,selector
就會從新計算獲得新的衍生數據並更新緩存。若是依賴不變就用緩存值。selector 的返回值由 get 方法返回,能夠返回計算出的肯定的值,也能夠是 Promise、其餘 selector、atom, 若是 atom
和selector
的值是 Promise ,那麼表示狀態值是異步獲取的,Recoil 提供了專門的 api 用於獲取異步狀態,並支持和 React suspense 結合使用顯示異步狀態。
atom
和 selector
都有一個 key 值用於在 Recoil 上下文中惟一標識。key 值在同一個 <RecoilRoot>
上下文裏必須是惟一的。
判斷依賴是否改變:因爲依賴根本上就是 atom,因此當設置 atom 值的時候就知道依賴改變了,此時就會觸發 selector 從新計算 get。下面的 myGet
就是 selector
的 get:
function myGet(store: Store, state: TreeState): [DependencyMap, Loadable<T>] {
initSelector(store);
return [
new Map(),
detectCircularDependencies(() =>
getSelectorValAndUpdatedDeps(store, state),
),
];
}
複製代碼
getSelectorValAndUpdatedDeps
執行後返回裝載了衍生數據計算結果的 loadable,若是有緩存就返回緩存,沒有緩存就從新計算:
function getSelectorValAndUpdatedDeps( store: Store, state: TreeState, ): Loadable<T> {
const cachedVal = getValFromCacheAndUpdatedDownstreamDeps(store, state);
// 若是有緩存
if (cachedVal != null) {
setExecutionInfo(cachedVal);
return cachedVal;
}
// 省略了一些代碼
// 沒有緩存
return getValFromRunningNewExecutionAndUpdatedDeps(store, state);
}
複製代碼
進到上面最後一行的函數 getValFromRunningNewExecutionAndUpdatedDeps(store, state)
,它用於計算 selector 的 屬性 get 的回調函數 get ,計算出新值,緩存新值,同時更新依賴:
function getValFromRunningNewExecutionAndUpdatedDeps( store: Store, state: TreeState, ): Loadable<T> {
const newExecutionId = getNewExecutionId();
// 計算 selector 的 get ,返回裝載了新的計算結果的 loadable
const [loadable, newDepValues] = evaluateSelectorGetter(
store,
state,
newExecutionId,
);
setExecutionInfo(loadable, newDepValues, newExecutionId, state);
maybeSetCacheWithLoadable(
state,
depValuesToDepRoute(newDepValues),
loadable,
);
notifyStoreWhenAsyncSettles(store, loadable, newExecutionId);
return loadable;
}
複製代碼
暴露出來的用於獲取和設置 Recoil state 的 Hooks:
useRecoilValue
、useRecoilValueLoadable
useSetRecoilState
、useResetRecoilState
useRecoilState
、useRecoilStateLoadable
、useRecoilCallback
其中 useRecoilState
= useRecoilValue
+ useSetRecoilState
,後兩個就是獲取 Recoil state 的只讀狀態值和設置狀態值。
useResetRecoilState
用於重置 Recoil state 爲初始狀態,也就是 default 值。
useRecoilStateLoadable
用於獲取異步狀態和設置狀態,useRecoilValueLoadable
只讀異步狀態
useRecoilCallback
:上面的幾個 api 都會讓組件去訂閱 state,當 state 改變組件就會更新,可是使用 useRecoilCallback
能夠不用更新組件。 useRecoilCallback
容許組件不訂閱 Recoil state 的狀況下獲取和設置 Recoil state,也就說組件能夠獲取 state 可是不用更新,反過來組件沒必要更新自身才能獲取新 state,將讀取 state 延遲到咱們須要的時候而不是在組件 monted 的時候。
用法:
const [products, setProducts] = useRecoilState(productAtom)
複製代碼
咱們以 useRecoliState
舉例,看一下 Recoil 中設置 Recoil state 值走的流程:
// Recoil_Hooks.ts
function useRecoilState<T>( recoilState: RecoilState<T>, ): [T, SetterOrUpdater<T>] {
if (__DEV__) {
// $FlowFixMe[escaped-generic]
validateRecoilValue(recoilState, 'useRecoilState');
}
return [useRecoilValue(recoilState), useSetRecoilState(recoilState)];
}
複製代碼
useRecoiState
返回 useRecoilValue
, useSetRecoilState
。分別看一下後面兩個。
useSetRecoilState
設置 Recoil state 首先用於設置狀態的 useSetRecoilState
,看它是怎麼把新狀態更新到 store,下面是這個過程的調用鏈 :
useSetRecoilState
調用 setRecoilValue
,調用 <RecoilRoot>
的 useStoreRef()
獲取 Recoil state 上下文,即 store,能夠看到實際上 useSetRecoilState
返回的 setter 除了能夠傳入新的狀態值,也能夠傳入一個回調函數,後者返回新的狀態值:function useSetRecoilState<T>(recoilState: RecoilState<T>): SetterOrUpdater<T> {
if (__DEV__) {
// $FlowFixMe[escaped-generic]
validateRecoilValue(recoilState, 'useSetRecoilState');
}
const storeRef = useStoreRef();
return useCallback( // 返回 setter
(newValueOrUpdater: (T => T | DefaultValue) | T | DefaultValue) => {
setRecoilValue(storeRef.current, recoilState, newValueOrUpdater);
},
[storeRef, recoilState],
);
}
複製代碼
setRecoilValue
調 queueOrPerformStateUpdate
:function setRecoilValue<T>( store: Store, recoilValue: AbstractRecoilValue<T>, valueOrUpdater: T | DefaultValue | (T => T | DefaultValue), ): void {
queueOrPerformStateUpdate(
store,
{
type: 'set',
recoilValue,
valueOrUpdater,
}, // action
recoilValue.key,
'set Recoil value',
);
}
複製代碼
queueOrPerformStateUpdate
調 applyActionsToStore
,這裏會把更新排隊或者當即執行更新,排隊是爲了批處理多個更新,因爲批處理 API 暫時被標記爲 unstable,因此這裏不分析,只看當即更新:function queueOrPerformStateUpdate( store: Store, action: Action<mixed>, key: NodeKey, message: string, ): void {
if (batchStack.length) {// 排隊批處理
const actionsByStore = batchStack[batchStack.length - 1];
let actions = actionsByStore.get(store);
if (!actions) {
actionsByStore.set(store, (actions = []));
}
actions.push(action);
} else { // 當即更新
Tracing.trace(message, key, () => applyActionsToStore(store, [action]));
// 執行 applyActionsToStore
}
}
複製代碼
applyActionsToStore
調 applyAction
:function applyActionsToStore(store, actions) {
store.replaceState(state => {
const newState = copyTreeState(state);
for (const action of actions) {
applyAction(store, newState, action);
}
invalidateDownstreams(store, newState);
return newState;
});
}
// 。。。。。
function copyTreeState(state) {
return {
...state,
atomValues: new Map(state.atomValues),
nonvalidatedAtoms: new Map(state.nonvalidatedAtoms),
dirtyAtoms: new Set(state.dirtyAtoms),
};
}
複製代碼
// store.replaceState
const replaceState = replacer => {
const storeState = storeRef.current.getState();
startNextTreeIfNeeded(storeState);//
// Use replacer to get the next state:
const nextTree = nullthrows(storeState.nextTree);
// nextTree: The TreeState that is written to when during the course of a transaction
// (generally equal to a React batch) when atom values are updated.
let replaced;
try {
stateReplacerIsBeingExecuted = true;
// replaced 爲新的 state tree
replaced = replacer(nextTree);
} finally {
stateReplacerIsBeingExecuted = false;
}
// 若是新、舊 state tree 淺比較相等,就不更新組件
// 實際上,若是不是批處理更新,這裏 replaced 、nextTree 引用不可能相等
if (replaced === nextTree) {
return;
}
if (__DEV__) {
if (typeof window !== 'undefined') {
window.$recoilDebugStates.push(replaced); // TODO this shouldn't happen here because it's not batched
}
}
// 若是新、舊 state tree 淺比較不相等,就更新組件,更新會被 React 推入調度
// Save changes to nextTree and schedule a React update:
storeState.nextTree = replaced; // 更新 state tree
// 通知訂閱了 state 的組件更新
nullthrows(notifyBatcherOfChange.current)(); // notifyBatcherOfChange.current() 就是 setState({})
};
複製代碼
applyAction
調 setNodeValue
,後者返回 depMap 和 writes,從下面的分析中知道 depMap 就是一個空的 new Map()
,writes 也是 Map,可是不是空 Map,而是設置了一個元素,元素的 key 是 atom key,value 是 一個 Loadable,Loadable 中裝載了 atom 的狀態值 :function applyAction(store: Store, state: TreeState, action: Action<mixed>) {
if (action.type === 'set') {
const {recoilValue, valueOrUpdater} = action;
// 讀取 state value,若是是一個函數就會執行函數返回 value
const newValue = valueFromValueOrUpdater(
store,
state,
recoilValue,
valueOrUpdater,
);
// 返回 nodes ,nodes 是一個 Map ,用來存儲 Recoil state,每個 state 又是一個 Map
const [depMap, writes] = setNodeValue(
store,
state,
recoilValue.key,
newValue,
);
saveDependencyMapToStore(depMap, store, state.version);
// setNodeValue
for (const [key, loadable] of writes.entries()) {
writeLoadableToTreeState(state, key, loadable);
}
}
else if (action.type === 'setLoadable'){.....}
// 下面省略若干 else if
}
複製代碼
這裏 writeLoadableToTreeState
把狀態值以 [key, Loadable]
寫入 state.atomValues
:
function writeLoadableToTreeState( state: TreeState, key: NodeKey, loadable: Loadable<mixed>, ): void {
if (
loadable.state === 'hasValue' &&
loadable.contents instanceof DefaultValue
) {
state.atomValues.delete(key);
} else {
state.atomValues.set(key, loadable);
}
state.dirtyAtoms.add(key);
state.nonvalidatedAtoms.delete(key);
}
複製代碼
後面讀取狀態時就能夠像這樣讀:const loadable = state.atomValues.get(key)
setNodeValue
調 node.set
:function setNodeValue<T>( store: Store, state: TreeState, key: NodeKey, newValue: T | DefaultValue, ): [DependencyMap, AtomValues] {
const node = getNode(key);
if (node.set == null) {
throw new ReadOnlyRecoilValueError(
`Attempt to set read-only RecoilValue: ${key}`,
);
}
return node.set(store, state, newValue);
}
複製代碼
node.set
中 node 是 nodes 的元素(node = nodes.get(key)
), nodes 是一個 Map 結構const nodes: Map<string, Node<any>> = new Map();
const recoilValues: Map<string, RecoilValue<any>> = new Map();
複製代碼
nodes 的每一個 node 是這樣的對象結構:
{
key: NodeKey,
// Returns the current value without evaluating or modifying state
peek: (Store, TreeState) => ?Loadable<T>,
// Returns the discovered deps and the loadable value of the node
get: (Store, TreeState) => [DependencyMap, Loadable<T>],
set: ( // node.set store: Store, state: TreeState, newValue: T | DefaultValue, ) => [DependencyMap, AtomValues],
// Clean up the node when it is removed from a <RecoilRoot>
cleanUp: Store => void,
// Informs the node to invalidate any caches as needed in case either it is
// set or it has an upstream dependency that was set. (Called at batch end.)
invalidate?: TreeState => void,
shouldRestoreFromSnapshots: boolean,
dangerouslyAllowMutability?: boolean,
persistence_UNSTABLE?: PersistenceInfo,
}
複製代碼
那麼 node 是何時初始化的呢? 在初始化一個 Recoil state(調用 atom 或 selector) 的時候會調用一個 registerNode
在 nodes 中註冊一個 node:
function registerNode<T>(node: Node<T>): RecoilValue<T> {
// 。。。
nodes.set(node.key, node);
// 。。。
recoilValues.set(node.key, recoilValue);
// 。。。
}
複製代碼
以在初始化 atom 爲例,初始化一個 atom 時會調用 registerNode
註冊一個 node,此時 node.set
是這樣的,它沒有直接把新狀態值 newValue 寫入到 node,而是先用 loadableWithValue
封裝了一層:
function mySet( store: Store, state: TreeState, newValue: T | DefaultValue, ): [DependencyMap, AtomValues] {
initAtom(store, state, 'set');
// Bail out if we're being set to the existing value, or if we're being
// reset but have no stored value (validated or unvalidated) to reset from:
if (state.atomValues.has(key)) {
const existing = nullthrows(state.atomValues.get(key));
if (existing.state === 'hasValue' && newValue === existing.contents) {
return [new Map(), new Map()];
}
} else if (
!state.nonvalidatedAtoms.has(key) &&
newValue instanceof DefaultValue
) {
return [new Map(), new Map()];
}
if (__DEV__) {
if (options.dangerouslyAllowMutability !== true) {
deepFreezeValue(newValue);
}
}
// can be released now if it was previously in use
cachedAnswerForUnvalidatedValue = undefined;
return [new Map(), new Map().set(key, loadableWithValue(newValue))];
}
複製代碼
咱們順便看看 node.get
,這個在後面讀取狀態值的時候會用到,它會直接返回裝載着狀態值的 Loadable,state.atomValues.get(key))
讀取到的就是 loadable,由於寫入狀態的時候就是 [key, Loadable]
這樣的形式寫入 node Map 的。
function myGet(store: Store, state: TreeState): [DependencyMap, Loadable<T>] {
initAtom(store, state, 'get');
if (state.atomValues.has(key)) {
// Atom value is stored in state:
return [new Map(), nullthrows(state.atomValues.get(key))];
}
// 。。。。
}
複製代碼
咱們能夠看看 loadableWithValue(newValue)
這個方法,loadableWithValue(newValue)
返回一個裝載肯定的狀態值的 loadable,除了 loadableWithValue(newValue)
,還有 loadableWithError(error)
、loadableWithPromise
,這些就是讀取異步狀態時調用的,用來封裝 Loadable :
function loadableWithValue<T>(value: T): Loadable<T> {
// Build objects this way since Flow doesn't support disjoint unions for class properties
return Object.freeze({
state: 'hasValue',
contents: value,
...loadableAccessors,
});
}
function loadableWithError<T>(error: Error): Loadable<T> {
return Object.freeze({
state: 'hasError',
contents: error,
...loadableAccessors,
});
}
function loadableWithPromise<T>(promise: LoadablePromise<T>): Loadable<T> {
return Object.freeze({
state: 'loading',
contents: promise,
...loadableAccessors,
});
}
複製代碼
useRecoilValue
讀取狀態useRecoilValue
調 useRecoilValueLoadable
, 後者把狀態封裝到一個 Loadable 並返回,再調用 handleLoadable
讀取 Loadable 裝載的 狀態值 :function useRecoilValue<T>(recoilValue: RecoilValue<T>): T {
if (__DEV__) {
// $FlowFixMe[escaped-generic]
validateRecoilValue(recoilValue, 'useRecoilValue');
}
const loadable = useRecoilValueLoadable(recoilValue);
// $FlowFixMe[escaped-generic]
return handleLoadable(loadable, recoilValue, storeRef);
}
複製代碼
useRecoilValueLoadable
內部調用 useRecoilValueLoadable_LEGACY
:function useRecoilValueLoadable<T>( recoilValue: RecoilValue<T>, ): Loadable<T> {
if (mutableSourceExists()) {
// eslint-disable-next-line fb-www/react-hooks
return useRecoilValueLoadable_MUTABLESOURCE(recoilValue);
} else {
// eslint-disable-next-line fb-www/react-hooks
return useRecoilValueLoadable_LEGACY(recoilValue);
}
}
複製代碼
useRecoilValueLoadable_LEGACY
調用 subscribeToRecoilValue
訂閱state:
function useRecoilValueLoadable_LEGACY<T>( recoilValue: RecoilValue<T>, ): Loadable<T> {
if (__DEV__) {
// $FlowFixMe[escaped-generic]
validateRecoilValue(recoilValue, 'useRecoilValueLoadable');
}
const storeRef = useStoreRef();
const [_, forceUpdate] = useState([]);
const componentName = useComponentName();
useEffect(() => {
const store = storeRef.current;
const sub = subscribeToRecoilValue(
store,
recoilValue,
_state => {
Tracing.trace('RecoilValue subscription fired', recoilValue.key, () => {
forceUpdate([]);
});
},
componentName,
);
Tracing.trace('initial update on subscribing', recoilValue.key, () => {
/** * Since we're subscribing in an effect we need to update to the latest * value of the atom since it may have changed since we rendered. We can * go ahead and do that now, unless we're in the middle of a batch -- * in which case we should do it at the end of the batch, due to the * following edge case: Suppose an atom is updated in another useEffect * of this same component. Then the following sequence of events occur: * 1. Atom is updated and subs fired (but we may not be subscribed * yet depending on order of effects, so we miss this) Updated value * is now in nextTree, but not currentTree. * 2. This effect happens. We subscribe and update. * 3. From the update we re-render and read currentTree, with old value. * 4. Batcher's effect sets currentTree to nextTree. * In this sequence we miss the update. To avoid that, add the update * to queuedComponentCallback if a batch is in progress. */
const state = store.getState();
if (state.nextTree) {
store.getState().queuedComponentCallbacks_DEPRECATED.push(
Tracing.wrap(() => {
forceUpdate([]);
}),
);
} else {
forceUpdate([]);
}
});
return () => sub.release(store);
}, [recoilValue, storeRef]);
return getRecoilValueAsLoadable(storeRef.current, recoilValue);
}
複製代碼
getRecoilValueAsLoadable
又調用 getNodeLoadable
,後者最終返回 Loadable:unction getRecoilValueAsLoadable<T>(
store: Store,
{key}: AbstractRecoilValue<T>,
treeState: TreeState = store.getState().currentTree,
): Loadable<T> {
// Reading from an older tree can cause bugs because the dependencies that we
// discover during the read are lost.
const storeState = store.getState();
// ...
const [dependencyMap, loadable] = getNodeLoadable(store, treeState, key);
// ...
return loadable;
}
複製代碼
getNodeLoadable
內部直接用 node.get
返回 loadable, :function getNodeLoadable<T>( store: Store, state: TreeState, key: NodeKey, ): [DependencyMap, Loadable<T>] {
return getNode(key).get(store, state); // node.get, 即 state.atomValues.get(key),由於 get 就是 myGet
}
複製代碼
咱們能夠返回看看上面 node.get
的定義,內部就是用 state.atomValues.get(key)
返回 loadable。
看完了 Recoil 內部設置和讀取 state 的流程,再來看 Recoil 怎麼在 state 改變時通知組件更新,Recoil 定義了一個 Batcher
組件,只要 Recoil state 改變,就調用一次 setState({})
,它沒有往 React 中設置任何狀態,做用只是觸發 React 把這次更新放入到隊列中等待 React commit 階段批量更新。setState({})
雖然是空的,可是 React 內部會精確的找到調用 setState({})的組件,只更新這個組件,而不會影響到其餘組件。
function Batcher(props: {setNotifyBatcherOfChange: (() => void) => void}) {
const storeRef = useStoreRef();
const [_, setState] = useState([]);
props.setNotifyBatcherOfChange(() => setState({}));
useEffect(() => {
// enqueueExecution runs this function immediately; it is only used to
// manipulate the order of useEffects during tests, since React seems to
// call useEffect in an unpredictable order sometimes.
Queue.enqueueExecution('Batcher', () => {
const storeState = storeRef.current.getState();
const {nextTree} = storeState;
// Ignore commits that are not because of Recoil transactions -- namely,
// because something above RecoilRoot re-rendered:
if (nextTree === null) {
return;
}
// nextTree is now committed -- note that copying and reset occurs when
// a transaction begins, in startNextTreeIfNeeded:
storeState.previousTree = storeState.currentTree;
storeState.currentTree = nextTree;
storeState.nextTree = null;
// sendEndOfBatchNotifications 取出 storeState.queuedComponentCallbacks_DEPRECATED 隊列裏的 force
sendEndOfBatchNotifications(storeRef.current);
const discardedVersion = nullthrows(storeState.previousTree).version;
storeState.graphsByVersion.delete(discardedVersion);
storeState.previousTree = null;
});
});
return null;
}
複製代碼
function sendEndOfBatchNotifications(store: Store) {
const storeState = store.getState();
// 中間省略部分代碼
// Special behavior ONLY invoked by useInterface.
// FIXME delete queuedComponentCallbacks_DEPRECATED when deleting useInterface.
storeState.queuedComponentCallbacks_DEPRECATED.forEach(cb => cb(treeState));
storeState.queuedComponentCallbacks_DEPRECATED.splice(
0,
storeState.queuedComponentCallbacks_DEPRECATED.length,
);
}
複製代碼
storeState.queuedComponentCallbacks_DEPRECATED
中把 useRecoilValue
中推入的 forceUpdate 取出來執行,因而 useRecoilValue
便可以在更新的時候讀取新的 state。
Batcher
被放在 <RecoilRoot>
下面成爲其 children,返回文章最上面 <RecoilRoot>
的源碼能夠看到是這樣寫的:
<AppContext.Provider value={storeRef}>
<MutableSourceContext.Provider value={mutableSource}> <Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} /> {children} </MutableSourceContext.Provider>
</AppContext.Provider>
複製代碼
Batcher
的 props setNotifyBatcherOfChange
,其傳入的參數 x
即 () => setState({})
:
const notifyBatcherOfChange = useRef<null | (mixed => void)>(null);
function setNotifyBatcherOfChange(x: mixed => void) {
notifyBatcherOfChange.current = x;
}
複製代碼
Recoil state 改變時調用 notifyBatcherOfChange.current
即 () => setState({})
:
onst replaceState = replacer => {
// ....
nullthrows(notifyBatcherOfChange.current)();
};
複製代碼
這個replaceState
就是在設置 Recoil state 的流程中被調用的,能夠返回到上面設置 Recoil state 中調用 applyActionsToStore
這一步。因此每次調用 useSetRecoilState
設置 Recoil state 都會調用 React 的 setState
觸發更新,交給 React 來批量更新。
以上大體的流程,描述了 recoil 的狀態設置、讀取、訂閱的過程,能夠用如下這個圖歸納:
個人另外一篇文章利用這個過程實現了一個小型的 recoil :簡單實現 Recoil 的狀態訂閱共享
這裏對 Recoil 的用法和幾個穩定的 API 作了簡單的分析,對於 Recoil 怎麼批量更新和支持 React 並行模式等等沒有涉及。
Recoil 官網:recoiljs.org/
[1] 圖片來自: Recoil: A New State Management Library Moving Beyond Redux and the Context API