Gutenberg 11.6 增加了对 thunks 的支持。你可以将 thunk 理解为可以分发的函数:

// actions.js
export const myThunkAction = () => ( { select, dispatch } ) => {
    return "I'm a thunk! I can be dispatched, use selectors, and even dispatch other actions.";
};

为什么 thunk 很有用?

Thunks 扩展了 Redux action 的含义。在引入 thunk 之前,actions 是纯函数,只能返回和产出数据。像与 store 交互或从 action 请求 API 数据这样的常见用例需要使用单独的 control。你经常会看到如下代码:

export function* saveRecordAction( id ) {
    const record = yield controls.select( 'current-store', 'getRecord', id );
    yield { type: 'BEFORE_SAVE', id, record };
    const results = yield controls.fetch({ url: 'https://...', method: 'POST', data: record });
    yield { type: 'AFTER_SAVE', id, results };
    return results;
}

const controls = {
    select: // ...,
    fetch: // ...,
};

像 store 操作和 fetch 函数这样的副作用会在 action 之外实现。Thunks 提供了一种替代方法,允许你在内部使用副作用,如下所示:

export const saveRecordAction = ( id ) => async ({ select, dispatch }) => {
    const record = select( 'current-store', 'getRecord', id );
    dispatch({ type: 'BEFORE_SAVE', id, record });
    const response = await fetch({ url: 'https://...', method: 'POST', data: record });
    const results = await response.json();
    dispatch({ type: 'AFTER_SAVE', id, results });
    return results;
}

这消除了实现单独 controls 的需要。

Thunk 可以访问 store helper

让我们看看 Gutenberg core 中的一个示例。在引入 thunk 之前,@wordpress/interface 包中的 toggleFeature action 是这样实现的:

export function* toggleFeature( scope, featureName ) {
    const currentValue = yield controls.select(
        interfaceStoreName,
        'isFeatureActive',
        scope,
        featureName
    );

    yield controls.dispatch(
        interfaceStoreName,
        'setFeatureValue',
        scope,
        featureName,
        ! currentValue
    );
}

Controls 是 dispatch actions 和从 store select 数据的唯一方式。

有了 thunk,有一种更清晰的方法。这就是现在 toggleFeature 的实现方式:

export function toggleFeature( scope, featureName ) {
    return function ( { select, dispatch } ) {
        const currentValue = select.isFeatureActive( scope, featureName );
        dispatch.setFeatureValue( scope, featureName, ! currentValue );
    };
}

多亏了 select 和 dispatch 参数,thunks 可以直接使用 store,而无需生成器和 controls。

Thunk 可以是异步的

想象一个简单的 React 应用程序,允许你设置恒温器的温度。它只有一个输入和一个按钮。点击该按钮会分发一个 saveTemperatureToAPI action,其中包含来自输入的数值。

如果我们使用 controls 来保存温度,store 定义将如下所示:

const store = wp.data.createReduxStore( 'my-store', {
    actions: {
        saveTemperatureToAPI: function*( temperature ) {
            const result = yield { type: 'FETCH_JSON', url: 'https://...', method: 'POST', data: { temperature } };
            return result;
        }
    },
    controls: {
        async FETCH_JSON( action ) {
            const response = await window.fetch( action.url, {
                method: action.method,
                body: JSON.stringify( action.data ),
            } );
            return response.json();
        }
    },
    // reducers, selectors, ...
} );

虽然代码相当直接,但存在一层间接性。saveTemperatureToAPI action 不直接与 API 通信,而是必须通过 FETCH_JSON control。

让我们看看如何使用 thunk 消除这种间接性:

const store = wp.data.createReduxStore( 'my-store', {
    actions: {
        saveTemperatureToAPI: ( temperature ) => async () => {
            const response = await window.fetch( 'https://...', {
                method: 'POST',
                body: JSON.stringify( { temperature } ),
            } );
            return await response.json();
        }
    },
    // reducers, selectors, ...
} );

这很酷!更好的是,还支持 resolvers:

const store = wp.data.createReduxStore( 'my-store', {
    // ...
    selectors: {
        getTemperature: ( state ) => state.temperature
    },
    resolvers: {
        getTemperature: () => async ( { dispatch } ) => {
            const response = await window.fetch( 'https://...' );
            const result = await response.json();
            dispatch.receiveCurrentTemperature( result.temperature );
        }
    },
    // ...
} );

对 thunk 的支持默认包含在每个 data store 中,就像(现在是遗留的)对
生成器和 controls 的支持一样。

Thunk API

Thunk 接收一个单个对象参数,具有以下键:

select

一个包含 store 的选择器对象的对象,这些选择器已预先绑定到 state,这意味着你不需要提供 state,只需提供额外的参数。select 会触发相关的 resolvers(如果有),但不会等待它们完成。即使值为 null,它也会返回当前值。

如果选择器是公共 API 的一部分,则作为 select 对象上的方法可用:

const thunk = () => ( { select } ) => {
    // select 是 store 的选择器对象,已预先绑定到当前 state:
    const temperature = select.getTemperature();
}

由于并非所有选择器都暴露在 store 上,select 也充当支持将选择器作为参数传递的函数:

const thunk = () => ( { select } ) => {
    // select 支持私有选择器:
    const doubleTemperature = select( ( temperature ) => temperature * 2 );
}

resolveSelect

resolveSelect 与 select 相同,但它返回一个 promise,该 promise 通过相关的 resolver 提供的值解析。

const thunk = () => ( { resolveSelect } ) => {
    const temperature = await resolveSelect.getTemperature();
}

dispatch

一个包含 store actions 的对象

如果 action 是公共 API 的一部分,则作为 dispatch 对象上的方法可用:

const thunk = () => ( { dispatch } ) => {
    // dispatch 是 store actions 的对象:
    const temperature = await dispatch.retrieveTemperature();
}

由于并非所有 action 都暴露在 store 上,dispatch 也充当支持将 Redux action 作为参数传递的函数:

const thunk = () => async ( { dispatch } ) => {
    // dispatch 也是一个接受内联 action 的函数:
    dispatch({ type: 'SET_TEMPERATURE', temperature: result.value });

    // thunks 可以与 actions 互换使用
    dispatch( updateTemperature( 100 ) );

    // Thunks 也可以是异步的。如果是这样,dispatch 返回一个 promise
    await dispatch( ( ) => window.fetch( /* ... */ ) );
}

registry

Registry 通过其 dispatch、select 和 resolveSelect 方法提供对其他 store 的访问。
这些与上面描述的那些非常相似,只是略有不同。调用 registry.select( storeName ) 返回一个函数,该函数返回来自 storeName 的选择器对象。当你需要与其他 store 交互时这很有用。例如:

const thunk = () => ( { registry } ) => {
  const error = registry.select( 'core' ).getLastEntitySaveError( 'root', 'menu', menuId );
  /* ... */
}