WordPressのブロック開発メモ その10ー1 リゾルバの解読

https://github.com/WordPress/gutenberg/blob/4fa2a874fe9c61a4609ea273425d1302ecc18bd0/packages/data/src/redux-store/index.js

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;
}

resolversselectorscreateReduxStore()関数の戻り値のリテラルオブジェクトに追加されて返されます。
リゾルバを引数で設定していれば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;
}

これが「同じセレクタ名と引数」は二度目は実行されない理由ですね。
詳しくはmetadataSelectorsmetadataActionsの中身を読んでください(めっちゃ面倒です)
セレクタやアクションを関数として大雑把に表現(実際に掛かれているコードでは無いので注意)すると次のようになってます。

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で書かれたコードを読むの辛いなー。

BlockEditor certificate css DataGrid Docker Gutenberg Hyper-V iframe MUI openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク