resolverがどのように動作しているのかソースコードを覗いてみます。
export default function createReduxStore( key, options )
毎度おなじみcreateReduxStore()
の内部を見ます。
引数のoptions.resolvers
が使用されてる箇所です。
if ( options.resolvers ) {
const result = mapResolvers(
options.resolvers,
selectors,
store,
resolversCache
);
resolvers = result.resolvers;
selectors = result.selectors;
}
resolvers
とselectors
はcreateReduxStore()
関数の戻り値のリテラルオブジェクトに追加されて返されます。
リゾルバを引数で設定していればmapResolvers()
が実行されます。
ではmapResolvers()
です。
function mapResolvers( resolvers, selectors, store, resolversCache ) {
const mappedResolvers = mapValues( resolvers, ( resolver ) => {
} );
const mapSelector = ( selector, selectorName ) => {
};
return {
resolvers: mappedResolvers,
selectors: mapValues( selectors, mapSelector ),
};
}
戻り値を見ます。
return {
resolvers: mappedResolvers,
selectors: mapValues( selectors, mapSelector ),
};
どちらもmapValues()
の戻り値ですが、これは第一引数の配列の要素をひとつづつ第二引数のコールバックに渡して変換して返す関数です。
resolvers
に設定されるコードを見るとfullfill
プロパティに関数を持つリテラルオブジェクトに変換されています。
{
...resolver,
fullfill: resolver
}
selectors
はセレクタをmapSelector
で変換してます。
このmapSelector
を展開してみます。
const mapSelector = ( selector, selectorName ) => {
const resolver = resolvers[ selectorName ];
if ( ! resolver ) {
selector.hasResolver = false;
return selector;
}
const selectorResolver = ( ...args ) => {
// ... 省略
};
selectorResolver.hasResolver = true;
return selectorResolver;
};
最初のコードを見ると、
const resolver = resolvers[ selectorName ];
セレクタの名前と同じ名前のリゾルバを取得してます。
無ければそのままセレクタを返しますが、あればそれはselectorResolver
に置き換えてます。
戻り値にはhasResolver
が生されていて、対応するリゾルバが存在するかどうかを知ることが出来ます。
ではselectorResolver
を展開します。
const selectorResolver = ( ...args ) => {
async function fulfillSelector() {
// ... 省略
}
fulfillSelector( ...args );
return selector( ...args );
};
セレクタを実行する前にfullfillSelector()
を実行してます。
これがセレクタと同じ名前のリゾルバがある場合、そのリゾルバが実行される
コードなのでしょう。
最後にfullfillSelector()
を展開します。
async function fulfillSelector() {
const state = store.getState();
if (
resolversCache.isRunning( selectorName, args ) ||
( typeof resolver.isFulfilled === 'function' &&
resolver.isFulfilled( state, ...args ) )
) {
return;
}
const { metadata } = store.__unstableOriginalGetState();
if (
metadataSelectors.hasStartedResolution(
metadata,
selectorName,
args
)
) {
return;
}
resolversCache.markAsRunning( selectorName, args );
setTimeout( async () => {
resolversCache.clear( selectorName, args );
store.dispatch(
metadataActions.startResolution( selectorName, args )
);
try {
await fulfillResolver(
store,
mappedResolvers,
selectorName,
...args
);
store.dispatch(
metadataActions.finishResolution(
selectorName,
args
)
);
} catch ( error ) {
store.dispatch(
metadataActions.failResolution(
selectorName,
args,
error
)
);
}
} );
}
fullfillSelector()
は非同期関数であることに注意してください。
重要な部分はsetTimeout()
内で実行されてます。
ただし、以下のようにすでに同じ名前と引数の組み合わせがあると実行をやめます。
if (metadataSelectors.hasStartedResolution(metadata, selectorName, args)
{
return;
}
これが「同じセレクタ名と引数」は二度目は実行されない理由ですね。
詳しくはmetadataSelectors
やmetadataActions
の中身を読んでください(めっちゃ面倒です)
セレクタやアクションを関数として大雑把に表現(実際に掛かれているコードでは無いので注意)すると次のようになってます。
function hasStartedRedolution(metadata, selectorName, args)
{
return !! metadata[selectorName].get(args)
}
function startResolutions(selectorName, args)
{
metadata[selectorName].set(args, {status: 'resolving'})
}
function finishResolution(selectorName, args)
{
metadata[selectorName].set(args, {status: 'finished'})
}
function failResolution(selectorName, args, error)
{
metadata[selectorName].set(args, {status: 'error', error })
}
そして一番重要な部分がここ。
await fulfillResolver(
store,
mappedResolvers,
selectorName,
...args
);
fullfillResolver()
関数は以下のように定義してあります。
async function fulfillResolver( store, resolvers, selectorName, ...args ) {
const resolver = resolvers[ selectorName ];
if ( ! resolver ) {
return;
}
const action = resolver.fulfill( ...args );
if ( action ) {
await store.dispatch( action );
}
}
セレクタ名と一致するリゾルバがあればそれを実行しています。
何となくセレクタとリゾルバの関係が分かったような気が。
セレクタと同じ名前のリゾルバがあれば、セレクタ実行前にそれを非同期で実行、
ただし同じ名前と引数のペアであれば再度実行されない
JavaScriptで書かれたコードを読むの辛いなー。