计时两兄弟:debounce 与 throttleTwo timing helpers: debounce and throttle
先分清「等你停手」和「匀速放行」,再各写一个。First tell the two apart — wait until the calls stop, versus let one through at a steady rate — then write each one.
这一页有什么On this page5
- 一句话说清 debounce 和 throttle 的语义差别,并各举一个正确的使用场景Say in one sentence how debounce and throttle differ, and give one correct use case for each
- 手写 trailing debounce,带 cancelWrite a trailing debounce by hand, with a cancel method
- 手写 leading + trailing 的 throttleWrite a throttle by hand that fires on both the first and the last call
- 说清为什么两者都必须用闭包存状态Explain why both of them have to keep their state in a closure
美国面试 phone screen 的头号手写题。考的不只是写出来 —— 面试官会先问「这俩有什么区别、各用在哪」,答错场景直接扣分:搜索框用 throttle、滚动埋点用 debounce 都是反着的。This is the number one write-it-yourself question in a phone screen. Writing the code is not the whole test — the interviewer asks first how the two differ and where each one belongs, and naming the wrong scenario costs you points right away: a search box with throttle, or scroll tracking with debounce, are both the wrong way round.
debounce:把一串调用压成最后一次debounce: squeeze a burst of calls down to the last one
Write a debounce; when do you reach for it
一句话:debounce 的语义是「等你停手」—— 连续调用只在最后一次之后 delay 毫秒执行一次。 典型场景:搜索框输入(停止输入才发请求)、窗口 resize 结束后重排。
实现只有三个关键决定:
- 状态放闭包里。timer 必须在返回的函数外面 —— 放在里面每次调用都是新的,永远清不掉上一次。
- 先
clearTimeout再setTimeout。这一清一设就是「重新计时」—— debounce 的全部灵魂。 - 参数跟着 timer 走。最后一次调用的 args 被闭包捕获,之前的全部作废。
会追问:「加一个 leading 选项怎么改?」—— 进来时 timer 为空且 leading 为真,先立刻执行一次, 再设一个只负责「解锁」的 timer。追问的追问:「cancel 和 flush 有什么区别」—— cancel 丢弃挂着的调用,flush 立刻执行它。
In one line: debounce means “wait until you stop” — a burst of calls runs once, delay ms after the last one. Typical uses: search-as-you-type (fire the request when typing pauses), re-layout after window resize settles.
The implementation comes down to three decisions:
- State lives in the closure. The timer must sit outside the returned function — inside, every call would get a fresh one and nothing could ever be cleared.
clearTimeoutfirst, thensetTimeout. That clear-and-reset pair is the whole soul of debounce: the clock restarts on every call.- Arguments ride along with the timer. The closure captures the last call’s args; every earlier set is discarded.
Follow-up: “Add a leading option” — if the timer is empty and leading is on, fire immediately, then set a timer whose only job is to unlock. And the follow-up’s follow-up: “cancel vs flush” — cancel drops the pending call, flush runs it right now.
throttle:不管多密,每个窗口最多一次throttle: however dense the calls, at most one per window
Write a throttle with leading and trailing calls
一句话:throttle 的语义是「匀速放行」—— 调用再密,每 interval 毫秒最多执行一次。典型场景:滚动位置上报、 拖拽跟随、按住按钮连点。
标准版是 leading + trailing:窗口开头立刻执行一次 (用户第一下操作马上有反馈),窗口里被压掉的调用, 在窗口结束时用最后一次的参数补执行一枪 (不丢最终状态)。所以要存两样东西:上次执行的时间戳lastTime,和窗口内最后一次的参数 lastArgs。
和 debounce 的分界线一句话说死:debounce 在连续事件流里可能永远不执行(只要不停手);throttle 保证按节奏执行。 滚动进度条用 debounce 会直到停下才动 —— 那就是选错了。
会追问:「用 setTimeout 一个变量能不能写?」—— 能写 leading-only 或 trailing-only 的简版;两头都要就得 时间戳 + timer 双状态。「CSS 里有类似的东西吗」—— 没有直接等价物,但 scroll 事件配IntersectionObserver 常常能把节流的需求整个消掉。
In one line: throttle means “let it through at a steady rate” — no matter how dense the calls, it runs at most once per interval. Typical uses: reporting scroll position, drag tracking, rapid button presses.
The standard version is leading + trailing: fire once at the start of the window (the user’s first action gets instant feedback), and when the window closes, fire once more with the last suppressed call’s arguments (so the final state is not lost). That means two pieces of state: the timestamp of the last run, lastTime, and the window’s last arguments, lastArgs.
The dividing line, said once and hard: in a continuous event stream debounce may never run (as long as the stream never pauses); throttle guarantees a steady beat. A scroll progress bar built on debounce only moves when scrolling stops — that is the wrong pick.
Follow-up: “Can you write it with one setTimeout variable?” — yes for leading-only or trailing-only; both edges need the timestamp-plus-timer pair. “Anything similar in CSS?” — no direct equivalent, but scroll plus IntersectionObserver often removes the need to throttle at all.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
cancel() 能取消挂着的那次。Turn this half-finished version, which calls through immediately every time, into a real debounce: a burst of calls runs only once, delay ms after the last one, and cancel() drops the pending run.- 调用 debounced() 不许立刻执行 fnCalling debounced() must not run fn right away
- 一串连续调用只在停手 delay 毫秒后执行一次,参数用最后那次的A burst of calls runs once, delay ms after the last call, with the arguments of that last call
- 两串隔开的调用各自触发一次Two bursts separated by a pause each fire once
- cancel() 取消还没发生的那次调用cancel() drops the call that has not happened yet
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.
- 第一次调用立刻执行(leading)The first call runs immediately (leading)
- 窗口内的后续调用不执行Later calls inside the same window do not run
- 窗口结束时用窗口内最后一次的参数补执行(trailing)When the window closes, run once more with the arguments of the last call in it (trailing)
- 窗口过了之后再调用,又立刻执行A call after the window has passed runs immediately again
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.
初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
timer声明在返回函数里面,每次调用都拿到一个全新的null,clearTimeout 永远清不到上一次的 —— 结果是每次调用都各自触发一次,和没写一样。闭包状态必须声明在「造函数的那一层」。This is the most common way the debounce question fails. timer is declared inside the returned function, so every call gets a brand new null and clearTimeout never reaches the previous timer. Every call then fires on its own, exactly as if you had written no debounce at all. Closure state has to be declared in the outer function, the one that builds the returned function.换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- debounce = 等你停手:清旧 timer + 设新 timer 就是「重新计时」。debounce = wait until the calls stop: clearing the old timer and setting a new one is what restarts the clock.
- throttle = 匀速放行:时间戳管 leading,timer + lastArgs 管 trailing。throttle = let one through at a steady rate: a timestamp handles the first call, a timer plus lastArgs handles the last one.
- 连续事件流里 debounce 可能永远不执行,throttle 保证节奏 —— 场景选错直接扣分。In a continuous stream of events debounce may never run at all, while throttle keeps a fixed rate — picking the wrong one costs you points.
- 状态放闭包(造函数那一层),放进返回函数里就全废了。Keep the state in the closure, in the outer function; move it inside the returned function and nothing works.
- 追问点:leading 选项、cancel vs flush、单变量简版的取舍。Follow-ups: a leading option, cancel versus flush, and the trade-off of the short one-variable version.