Lodash 源码阅读-shortOut
概述
shortOut 是一个内部工具函数,用于优化高频调用的函数。它通过跟踪函数的调用频率和时间间隔,在函数被频繁调用时直接返回第一个参数,从而避免不必要的计算。
前置学习
依赖函数
- nativeNow:获取当前时间戳的函数
技术知识
- 函数节流:限制函数调用频率的技术
- 性能优化:减少不必要的计算
- 闭包:JavaScript 中的闭包概念
- 函数调用:apply 方法的使用
源码实现
function shortOut(func) {
var count = 0,
lastCalled = 0;
return function () {
var stamp = nativeNow(),
remaining = HOT_SPAN - (stamp - lastCalled);
lastCalled = stamp;
if (remaining > 0) {
if (++count >= HOT_COUNT) {
return arguments[0];
}
} else {
count = 0;
}
return func.apply(undefined, arguments);
};
}
实现思路
shortOut 函数的主要目的是优化高频调用的函数。它通过以下步骤实现:
- 跟踪函数的调用次数和时间间隔
- 如果函数在短时间内被频繁调用(达到 HOT_COUNT 次),则直接返回第一个参数
- 如果调用间隔超过 HOT_SPAN,则重置计数器
- 否则正常调用原函数
这种实现方式可以避免在函数被频繁调用时进行不必要的计算,提高性能。
源码解析
让我们逐行分析 shortOut 函数的实现:
function shortOut(func) {
var count = 0,
lastCalled = 0;
这里定义了两个闭包变量:
count:用于跟踪在特定时间窗口内函数被调用的次数lastCalled:记录上次函数被调用的时间戳
这两个变量会在函数的多次调用之间保持状态,这是闭包的典型应用。
return function () {
shortOut 函数返回一个新函数,这是高阶函数的特性。返回的这个函数会替代原始函数被调用。
var stamp = nativeNow(),
nativeNow 获取当前时间戳,通常是 Date.now() 的引用,用于计算函数调用的时间间隔。
remaining = HOT_SPAN - (stamp - lastCalled);
HOT_SPAN 是一个常量,表示"热调用"时间窗口的大小(单位:毫秒)。
stamp - lastCalled计算距离上次调用经过了多长时间remaining表示还剩多少时间窗口,如果为正,说明当前调用距离上次调用的时间间隔小于HOT_SPAN
lastCalled = stamp;
更新 lastCalled 为当前时间戳,为下次调用做准备。注意这一步是在任何判断逻辑之前执行的,确保时间间隔的计算总是基于最新的调用时间。
if (remaining > 0) {
如果 remaining > 0,说明当前调用与上次调用的时间间隔小于 HOT_SPAN,即函数在短时间内被再次调用。
if (++count >= HOT_COUNT) {
++count 增加调用计数,然后检查是否达到了 HOT_COUNT 阈值。
HOT_COUNT 是另一个常量,表示在 HOT_SPAN 时间窗口内函数调用次数的阈值。
return arguments[0];
如果函数在短时间内被调用的次数达到或超过 HOT_COUNT,则直接返回第一个参数,不再执行原始函数体。
这是性能优化的核心:当函数被频繁调用时,假设调用者只关心第一个参数,我们可以跳过原函数的执行。
为什么返回 arguments[0] 而不是其他值?这是因为在很多场景下,函数的第一个参数常常是关键数据,而函数本身可能是对这个数据的某种转换。在高频调用时,直接返回输入数据是一个合理的折衷。
}
} else {
count = 0;
如果当前调用距离上次调用的时间间隔超过了 HOT_SPAN(即 remaining <= 0),则重置计数器。
这表示"热调用"状态已经冷却,我们重新开始跟踪调用频率。
}
return func.apply(undefined, arguments);
};
}
如果不满足频繁调用的条件,则正常调用原始函数,并透传所有参数和返回值。
使用 func.apply(undefined, arguments) 而不是直接调用 func(...arguments) 是为了兼容旧版本的 JavaScript。
关键常量解释
虽然源码片段中没有显示 HOT_COUNT 和 HOT_SPAN 的定义,但在 Lodash 中:
HOT_COUNT:通常设为 800,表示在热时间窗口内函数被调用的阈值次数HOT_SPAN:通常设为 16 毫秒,约等于 60fps 下一帧的时间,表示热时间窗口的大小
这两个值的选择反映了 Lodash 对性能优化的经验判断:如果一个函数在 16ms 内被调用超过 800 次,那么它可能是在一个高频循环或事件中,此时进行性能优化是有意义的。
shortOut 与防抖、节流的对比
shortOut、debounce(防抖) 和 throttle(节流) 都是用于优化高频函数调用的技术,但它们的实现机制和使用场景有显著差异。
基本概念对比
-
shortOut (短路优化)
- 目的:优化极高频率调用的函数性能
- 原理:检测到极高频调用时,跳过函数主体执行,直接返回第一个参数
- 特点:计数型,在达到阈值后改变行为
-
debounce (防抖)
- 目的:将连续触发的事件合并成一次执行
- 原理:等待指定时间没有新的调用才执行函数
- 特点:延迟型,只关心最后一次调用
-
throttle (节流)
- 目的:限制函数在一定时间内执行的频率
- 原理:保证函数在指定时间内最多执行一次
- 特点:频率型,均匀地执行函数
行为特点对比
假设我们在短时间内频繁调用这三种不同方式包装的函数:
- shortOut:前 HOT_COUNT 次(默认 800 次)会正常执行,之后的调用直接返回第一个参数,不执行函数体
- debounce:只有在最后一次调用后等待指定时间(如 300ms)没有新的调用,才会执行一次
- throttle:会按照固定频率(如每 300ms)执行一次,忽略中间的调用
代码实现对比
shortOut 实现(简化版):
function shortOut(func) {
var count = 0,
lastCalled = 0;
return function () {
var now = Date.now();
if (now - lastCalled < 16) {
// HOT_SPAN
if (++count >= 800) {
// HOT_COUNT
return arguments[0];
}
} else {
count = 0;
}
lastCalled = now;
return func.apply(this, arguments);
};
}
debounce 实现(简化版):
function debounce(func, wait) {
var timeout;
return function () {
var context = this,
args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args);
}, wait);
};
}
throttle 实现(简化版):
function throttle(func, wait) {
var lastCall = 0;
return function () {
var now = Date.now();
if (now - lastCall >= wait) {
lastCall = now;
return func.apply(this, arguments);
}
};
}
适用场景对比
-
shortOut 适用于:
- 极高频率调用的函数,如渲染循环中的某些计算
- 可以容忍在高频调用时简化处理逻辑的场景
- 函数的第一个参数包含了关键信息,直接返回第一个参数是可接受的
-
debounce 适用于:
- 用户输入事件,如搜索框输入
- 窗口大小调整事件
- 需要等待用户操作完成后再执行的场景
-
throttle 适用于:
- 滚动事件处理
- 鼠标移动事件
- 需要定期执行但又不希望过于频繁的场景
性能特点对比
- shortOut:针对极端情况(如函数在 16ms 内被调用 800+ 次)的优化,通过简化处理逻辑提升性能
- debounce:通过减少函数执行次数提升性能,但会有一定的延迟
- throttle:通过控制函数执行频率提升性能,保证一定的响应性
函数行为变化对比
- shortOut:改变函数行为(返回第一个参数而非执行函数)
- debounce:延迟函数执行,但不改变函数行为
- throttle:限制函数执行频率,但不改变函数行为
这三种技术各有优缺点,在实际应用中应根据具体需求选择合适的方案。shortOut 是一种更为激进的优化手段,适用于非常特殊的高频调用场景;而 debounce 和 throttle 则是更为通用的函数调用频率控制技术。
应用场景
shortOut 是一个专门设计用于处理极端高频调用场景的优化工具,它在 Lodash 内部使用较多,而在普通应用开发中较少直接使用。以下是几个适合 shortOut 的实际应用场景:
1. Lodash 内部优化
Lodash 内部主要用 shortOut 来优化高阶函数的性能,特别是处理函数转换的场景:
// Lodash 内部示例 - 优化 baseSetToString 函数
var setToString = shortOut(baseSetToString);
// 当 setToString 在短时间内被频繁调用时(如函数组合过程中)
// 会跳过复杂的 baseSetToString 逻辑,提高性能
这在函数式编程和链式调用中尤其有用,因为这些场景可能涉及大量的函数转换和包装操作。
2. 紧密循环中的昂贵计算
在游戏开发或数据可视化等领域,有时会遇到非常紧密的渲染循环:
// 极高帧率游戏引擎中的物理计算
const calculatePhysics = shortOut(function (gameState) {
// 复杂的物理计算...
return gameState.withUpdatedPhysics();
});
// 游戏主循环
function gameLoop() {
const currentState = getGameState();
// 当帧率极高时,物理计算会自动降频
const nextState = calculatePhysics(currentState);
updateGame(nextState);
requestAnimationFrame(gameLoop);
}
在这种场景下,如果游戏运行在高性能设备上达到极高帧率,shortOut 可以自动降低某些计算的频率,而不影响视觉体验。
3. 实验性能监控工具
可以利用 shortOut 的特性来检测性能问题:
// 创建一个性能监控包装器
function createPerformanceMonitor(func, name) {
const wrapped = shortOut(function (...args) {
console.log(`Function ${name} executed normally`);
return func(...args);
});
return function (...args) {
const result = wrapped(...args);
// 如果 result === args[0],说明触发了短路条件
if (result === args[0] && args.length > 0) {
console.warn(`高频调用检测: ${name} 在短时间内被调用超过阈值`);
}
return result;
};
}
// 使用监控器
const monitoredFunction = createPerformanceMonitor(
expensiveCalculation,
"expensiveCalculation"
);
这种方式可以帮助开发者找出可能存在的性能瓶颈。
注意事项
使用 shortOut 时需要注意:
- 它改变了函数的行为:不再执行原函数逻辑,而是直接返回第一个参数
- 阈值设置较高:默认 16ms 内 800 次,只适用于真正的极端高频调用
- 第一个参数很重要:确保在短路时返回第一个参数是合理的
因此,shortOut 主要适用于性能关键型应用中的内部优化,而不是常规的用户交互处理。对于一般的高频事件处理,debounce 和 throttle 通常是更合适的选择。
总结
通过学习 shortOut 函数,我们可以看到以下设计原则:
-
性能优化:通过减少不必要的计算提高性能。
-
智能节流:根据调用频率动态调整行为。
-
资源保护:防止函数被过度调用导致资源耗尽。
-
平滑降级:在频繁调用时提供合理的默认行为。
-
代码复用:将通用的性能优化逻辑抽象出来。