Skip to content

fix(useRequest): 修复runAsync在节流状态下不生效的问题#2899

Open
Arktomson wants to merge 2 commits intoalibaba:masterfrom
Arktomson:fix/issue-2898
Open

fix(useRequest): 修复runAsync在节流状态下不生效的问题#2899
Arktomson wants to merge 2 commits intoalibaba:masterfrom
Arktomson:fix/issue-2898

Conversation

@Arktomson
Copy link
Copy Markdown
Contributor

@Arktomson Arktomson commented Jan 25, 2026

🤔 这个变动的性质是?

  • 新特性提交
  • 日常 bug 修复
  • 站点、文档改进
  • 演示代码改进
  • TypeScript 定义更新
  • 包体积优化
  • 性能优化
  • 功能增强
  • 国际化改进
  • 重构
  • 代码风格优化
  • 测试用例
  • 分支合并
  • 其他改动(是关于什么的改动?)

🔗 相关 Issue

Fixes #2898

💡 需求背景和解决方案

问题描述:

当使用 useRequest 配置节流选项(throttleLeading: true, throttleTrailing: false)时,runAsync 方法不遵守节流配置,会发送所有请求;而 run 方法能够正常工作。

问题根因:

问题由 useRequestImplement.ts 中使用 .bind() 方法导致。当调用 fetchInstance.runAsync.bind(fetchInstance) 时,会创建一个新函数,该函数永久引用绑定时刻的原始 runAsync 方法。之后,当节流插件替换 fetchInstance.runAsync 为节流版本时,已绑定的函数仍然指向原始的未节流方法。

解决方案:

runAsync 的绑定方式从:

runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance))

改为:

runAsync: useMemoizedFn((...args: TParams) => fetchInstance.runAsync(...args))

- Changed runAsync binding from .bind() to arrow function for dynamic lookup
- Added unit tests for runAsync with throttle configuration
- Fixes alibaba#2898
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Jan 25, 2026

npm i https://pkg.pr.new/ahooks@2899
npm i https://pkg.pr.new/@ahooksjs/use-url-state@2899

commit: 4e863be

@meet-student meet-student requested a review from crazylxr January 25, 2026 15:34
@whatwg6
Copy link
Copy Markdown

whatwg6 commented Jan 27, 2026

本地 patch 测试了下。节流虽然生效了,但是 2 个 promise 只有一个 resolve,另一个永远不会 resolve

@Arktomson
Copy link
Copy Markdown
Contributor Author

本地 patch 测试了下。节流虽然生效了,但是 2 个 promise 只有一个 resolve,另一个永远不会 resolve

又是另外一个问题了0.0,改的话看上去就是节流期间多次请求的话返回同一个promise

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes useRequest’s exported runAsync so it correctly respects throttle behavior by avoiding a .bind() that pins the pre-plugin runAsync implementation, and adds tests around throttled runAsync usage.

Changes:

  • Update useRequestImplement to expose runAsync via an arrow function so it always calls the latest fetchInstance.runAsync (including plugin-wrapped versions).
  • Modify the throttle plugin’s runAsync wrapper to reuse a promise within the throttle window.
  • Add unit tests covering runAsync with throttle settings, including the reported leading: true / trailing: false case.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/hooks/src/useRequest/src/useRequestImplement.ts Fixes runAsync export to dynamically call the (possibly plugin-wrapped) fetchInstance.runAsync.
packages/hooks/src/useRequest/src/plugins/useThrottlePlugin.ts Updates throttled runAsync promise behavior; currently has trailing-semantics issues.
packages/hooks/src/useRequest/tests/useThrottlePlugin.spec.ts Adds tests for throttled runAsync, but doesn’t cover throttleTrailing: true behaviors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 39 to +46
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
const now = Date.now();

// If there's a current promise and it was created within the throttle window,
// return it to share the result
if (currentPromise && now - promiseCreatedAt < throttleWait) {
return currentPromise;
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The currentPromise fast-path returns before calling throttledRef.current, which prevents lodash's throttle from registering subsequent calls. This breaks throttleTrailing: true (no trailing invocation) and throttleLeading: false (trailing uses the first call's args instead of the latest). Consider always invoking the throttled function on every call, while sharing a promise for the current throttle window, and ensure the executed callback uses the latest args for trailing behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +61
// Clear current promise after a delay to allow trailing calls
setTimeout(() => {
if (currentPromise && Date.now() - promiseCreatedAt >= throttleWait) {
currentPromise = null;
}
}, 0);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setTimeout(..., 0) blocks intended to clear currentPromise will almost never clear it because promiseCreatedAt was just set for this window, so Date.now() - promiseCreatedAt >= throttleWait will be false at +0ms. This logic is also duplicated in both then and catch. Either remove the clearing entirely (since the next window overwrites currentPromise), or clear it via a timer aligned to throttleWait / when the throttle window ends.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +90
test('useThrottlePlugin should respect throttleLeading and throttleTrailing options with runAsync', () => {
const callback = vi.fn();

act(() => {
hook = setUp(
() => {
callback();
return request({});
},
{
manual: true,
throttleWait: 3000,
throttleLeading: true,
throttleTrailing: false,
},
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage only exercises throttleTrailing: false for runAsync. Given the plugin exposes both throttleLeading and throttleTrailing, add a case for throttleTrailing: true (and ideally throttleLeading: false) asserting that the trailing invocation runs and uses the latest call's params; this would catch regressions in trailing behavior.

Copilot uses AI. Check for mistakes.
@crazylxr
Copy link
Copy Markdown
Collaborator

这次修复 useRequestImplement.tsrunAsync 的绑定方式,我觉得方向是对的;.bind(fetchInstance) 确实会把 runAsync 固定到插件改写前的实现,导致节流插件无法生效。

不过 useThrottlePlugin.ts 里新增的 currentPromise 复用逻辑会引入新的行为回归,建议这里再看一下。

问题在于:当节流窗口内再次调用 runAsync 时,现在会直接 return currentPromise,不会再进入 throttledRef.current(...)。这样 lodash throttle 收不到这次调用,trailing 触发就被绕过去了。

例如默认节流语义下(leading: true, trailing: true):

  1. runAsync(1) 立即执行
  2. 50ms 内再调用 runAsync(2)
  3. 预期应该在节流窗口结束后再触发一次 trailing 调用
  4. 但现在第二次调用直接复用旧 Promise,没有进入 throttle,最终只会执行第一次

所以这个改动虽然修复了 #2898 的场景,但会破坏 trailing: true(以及依赖“最后一次参数”的场景)。

我建议:

  • 保留 useRequestImplement.ts 里把 runAsync 改成箭头函数转发的修复
  • 不要在 throttle 窗口内直接短路掉对 throttledRef.current 的调用
  • 另外补几个 runAsync 用例覆盖:
    • leading: true, trailing: true
    • leading: false, trailing: true
    • trailing 场景下是否使用最后一次调用参数

这样能更稳地验证修复没有引入新的节流回归。

@Arktomson
Copy link
Copy Markdown
Contributor Author

Arktomson commented Mar 28, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug useRequest] runAsync does not respect throttle options while run works correctly

4 participants