[js] underscore 常用方法源码解读
debounce
immediate: true
开启 leading-edge,可以执行时立即执行。
如果点击过快,除了某个时段的首次点击 ,其余查询都不会送出。
immediate: false
(默认)
开启 trailing-edge,可以执行时也必须延后至少 wait 个时间才能执行。
如果点击过快,除了某时间段内的最后一次点击,其余所有的查询都不会送出。
var now = Date.now || function () {
return new Date().getTime();
};
//防反跳
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
var debounce = function (func, wait, immediate) {
var timeout, args, context, timestamp, result;
// 定时器设置的回调 later 方法的触发时间,和连续事件触发的最后一次时间戳的间隔
// 如果间隔为 wait(或者刚好大于 wait),则触发事件
var later = function () {
// 距上一次触发时间间隔
var last = now() - timestamp;
// 时间间隔 last 在 [0, wait) 中
// 还没到触发的点,则继续设置定时器
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;//?
}
}
};
return function () {
context = this;
args = arguments;
// 每次触发函数,更新时间戳
// later 方法中取 last 值时用到该变量
// 判断距离上次触发事件是否已经过了 wait seconds
// 即需要距离最后一次事件触发 wait seconds 后触发这个回调方法
timestamp = now();
// 首次触发且immediate 参数为 true 则立即触发
var callNow = immediate && !timeout;
// 如果延时不存在,重新设定延时
// 设置 wait seconds 后触发 later 方法
// 无论是否 callNow(如果是 callNow,也进入 later 方法,去 later 方法中判断是否执行相应回调函数)
// 在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中
if (!timeout) timeout = setTimeout(later, wait);
// 如果是立即触发
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
let a = document.getElementById('debounce');
a.addEventListener('click', debounce(() => {
console.log('click');
}, 500, true))
简略思路
var timeoutId = null;
var n = 0;
var counter = 0;
//button为页面上的按钮,快速不间断点击button,click时间的处理函数会调用多次,但是业务逻辑只会调用一次
$('#button').click(function () {
clearTimeout(timeoutId);
console.log('n: ', ++n);
timeoutId = setTimeout(function () {
console.log('counter: ', ++counter);
}, 200);
});
简单实现
/**
* 空闲控制 返回函数连续调用时,空闲时间必须大于或等于 idle,action 才会执行
* @param idle {number} 空闲时间,单位毫秒
* @param action {function} 请求关联函数,实际应用需要调用的函数
* @return {function} 返回客户调用函数
*/
var debounce = function (action, idle) {
var last;
return function () {
var ctx = this, args = arguments;
clearTimeout(last);
last = setTimeout(function () {
action.apply(ctx, args);
}, idle);
}
}
debounce(action, idle);
throttle
throttle 函数提供了第三个参数 options 来进行选项配置,并且支持如下两个参数:
- leading:是否设置节流前缘 -- leading edge。前缘的作用是保证第一次尝试调用的 func 会被立即执行,否则第一次调用也必须等待 wait 时间,默认为 true。
- trailing:是否设置节流后缘 -- trailing edge。后缘的作用是:当最近一次尝试调用 func 时,如果 func 不能立即执行,会延后 func 的执行,默认为 true。
Leading:true, trailing:true
无论点击再快,查询总是间隔发出,第一次查询立即发出。我们尝试连续点击两次,第一次立即送出后,第二次延迟送出。
Leading:false, trailing:false
第一次查询无法立即送出。必须执行过一次点击后,之后的点击才会送出请求。
Leading:true, trailing:false
和第一种情况类似,无论点击再快,查询总是间隔发出,第一次查询立即发出。我们尝试连续点击两次,第一次立即送出后,第二次未能送出。
Leading:false, trailing:true
第一次点击不会立即送出查询,但不同于第二种情况,这次点击会延迟执行。
//节流阀
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
var throttle = function (func, wait, options) {
// context和args缓存func执行时需要的上下文
// result缓存func执行结果
var context, args, result;
// setTimeout 的 handler
//标识最近一次被追踪的调用
var timeout = null;
// 标记时间戳
// 上一次执行回调的时间戳
var previous = 0;
if (!options) options = {};
// 延迟执行函数
var later = function () {
// 如果 options.leading === false
// 则每次触发回调后将 previous 置为 0
// 否则置为当前时间戳
previous = options.leading === false ? 0 : now();
// 清空为此次执行设置的定时器
timeout = null;//?
result = func.apply(context, args);
if (!timeout) context = args = null;
};
// 返回一个throttle化的函数
return function () {
// 尝试调用func时,会首先记录当前时间戳
var currentTime = now();
// 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
if (!previous && options.leading === false) previous = now;
// 延迟执行时间间隔 = 预设的最小等待期-(当前时间-上一次调用的时间)
var remaining = wait - (currentTime - previous);
// 记录执行时需要的上下文和参数
context = this;
args = arguments;
// 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口
// remaining大于时间窗口wait,表示客户端系统时间被调整过
if (remaining <= 0 || remaining > wait) {
// 清除之前的设置的延时执行,就不存在某些回调一同发生的情况了
if (timeout) {
clearTimeout(timeout);
// 防止内存泄露
timeout = null;
}
// 刷新最近一次func调用的时间点
previous = currentTime;
// 执行func调用
result = func.apply(context, args);
// 再次检查timeout,因为func执行期间可能有新的timeout被设置,如果timeout被清空了,代表不再有等待执行的func,也清空context和args
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 如果已经存在一个定时器,则不会进入该 if 分支
// 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
// 间隔 remaining milliseconds 后触发 later 方法
timeout = setTimeout(later, remaining);
}
return result;
};
};
let b = document.getElementById('throttle');
b.addEventListener('click', throttle(() => {
console.log('click');
}, 500))
/**
* 频率控制 返回函数连续调用时,action 执行频率限定为 次 / delay
* @param delay {number} 延迟时间,单位毫秒
* @param action {function} 请求关联函数,实际应用需要调用的函数
* @return {function} 返回客户调用函数
*/
var throttle = function (action, delay) {
var last = 0;
return function () {
var curr = +new Date();
if (curr - last > delay) {
action.apply(this, arguments);
last = curr;
}
}
}
throttle(action, delay);
isEmpty 、 each
var _ = {};
var property = function (key) {
return function (obj) {
return obj == null ? void 0 : obj[key];
};
};
// Helper for collection methods to determine whether a collection
// should be iterated as an array or as an object
// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
// Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var isArrayLike = function (collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var
push = ArrayProto.push,
slice = ArrayProto.slice,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind,
nativeCreate = Object.create;
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
var isArray = nativeIsArray || function (obj) {
return toString.call(obj) === '[object Array]';
};
// Internal function that returns an efficient (for current engines) version
// of the passed-in callback, to be repeatedly applied in other _
// functions.
var optimizeCb = function (func, context, argCount) {
if (context === void 0) { return func; }
switch (argCount == null ? 3 : argCount) {
case 1: return function (value) {
return func.call(context, value);
};
case 2: return function (value, other) {
return func.call(context, value, other);
};
case 3: return function (value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function (accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function () {
return func.apply(context, arguments);
};
};
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles raw objects in addition to array-likes. Treats all
// sparse array-likes as if they were dense.
var each = _.forEach = function (obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function (name) {
_['is' + name] = function (obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
// Is a given variable an object?
var isObject = _.isObject = function (obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
// Shortcut function for checking if an object has a given property directly
// on itself (in other words, not on a prototype).
var has = function (obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
};
// Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
// IE 11 (#1621), and in Safari 8 (#1929).
var isFunction;
if (typeof /./ != 'function' && typeof Int8Array != 'object') {
isFunction = _.isFunction = function (obj) {
return typeof obj == 'function' || false;
};
}
// Retrieve the values of an object's properties.
var values = function (obj) {
var keys = _.keys(obj);
var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};
// Is the given value `NaN`? (NaN is the only number which does not equal itself).
var isNaN = function (obj) {
return _.isNumber(obj) && obj !== +obj;
};
// Generator function to create the indexOf and lastIndexOf functions
function createIndexFinder(dir, predicateFind, sortedIndex) {
return function (array, item, idx) {
var i = 0, length = getLength(array);
if (typeof idx == 'number') {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), isNaN);
return idx >= 0 ? idx + i : -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) { return idx; }
}
return -1;
};
}
// Keep the identity function around for default iteratees.
_.identity = function (value) {
return value;
};
// An internal function for creating assigner functions.
var createAssigner = function (keysFunc, undefinedOnly) {
return function (obj) {
var length = arguments.length;
if (length < 2 || obj == null) { return obj; }
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) { obj[key] = source[key]; }
}
}
return obj;
};
};
// Assigns a given object with all the own properties in the passed-in object(s)
// (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
_.extendOwn = _.assign = createAssigner(_.keys);
// Returns whether an object has a given set of `key:value` pairs.
_.isMatch = function (object, attrs) {
var keys = _.keys(attrs), length = keys.length;
if (object == null) { return !length; }
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = keys[i];
if (attrs[key] !== obj[key] || !(key in obj)) { return false; }
}
return true;
};
// Returns a predicate for checking whether an object has a given set of
// `key:value` pairs.
_.matcher = _.matches = function (attrs) {
attrs = _.extendOwn({}, attrs);
return function (obj) {
return _.isMatch(obj, attrs);
};
};
_.property = property;
// A mostly-internal function to generate callbacks that can be applied
// to each element in a collection, returning the desired result — either
// identity, an arbitrary callback, a property matcher, or a property accessor.
var cb = function (value, context, argCount) {
if (value == null) { return _.identity; }
if (_.isFunction(value)) { return optimizeCb(value, context, argCount); }
if (_.isObject(value)) { return _.matcher(value); }
return _.property(value);
};
// Generator function to create the findIndex and findLastIndex functions
function createPredicateIndexFinder(dir) {
return function (array, predicate, context) {
predicate = cb(predicate, context);
var length = getLength(array);
var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) {
if (predicate(array[index], index, array)) { return index; }
}
return -1;
};
}
// Returns the first index on an array-like that passes a predicate test
var findIndex = createPredicateIndexFinder(1);
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
var sortedIndex = function (array, obj, iteratee, context) {
iteratee = cb(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = getLength(array);
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < value) { low = mid + 1; } else { high = mid; }
}
return low;
};
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
var indexOf = createIndexFinder(1, findIndex, sortedIndex);
// Determine if the array or object contains a given item (using `===`).
// Aliased as `includes` and `include`.
var contains = _.includes = _.include = function (obj, item, fromIndex, guard) {
if (!isArrayLike(obj)) { obj = values(obj); }
if (typeof fromIndex != 'number' || guard) { fromIndex = 0; }
return indexOf(obj, item, fromIndex) >= 0;
};
// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
var hasEnumBug = !{ toString: null }.propertyIsEnumerable('toString');
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
var proto = (isFunction(constructor) && constructor.prototype) || ObjProto;
// Constructor is a special case.
var prop = 'constructor';
if (has(obj, prop) && !contains(keys, prop)) { keys.push(prop); }
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
if (prop in obj && obj[prop] !== proto[prop] && !contains(keys, prop)) {
keys.push(prop);
}
}
}
// Retrieve the names of an object's own properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
var keys = _.keys = function (obj) {
if (!isObject(obj)) { return []; }
if (nativeKeys) { return nativeKeys(obj); }
var keys = [];
for (var key in obj) { if (has(obj, key)) { keys.push(key); } }
// Ahem, IE < 9.
if (hasEnumBug) { collectNonEnumProps(obj, keys); }
return keys;
};
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
var isEmpty = function (obj) {
if (obj == null) { return true; }
if (isArrayLike(obj) && (isArray(obj) || _.isString(obj) || _.isArguments(obj))) { return obj.length === 0; }
return keys(obj).length === 0;
};
console.log(isEmpty([]))//true
console.log(isEmpty([1, 2, 3]))//false
console.log(isEmpty(''))//true
console.log(isEmpty('123'))//false
console.log(isEmpty({}))//true
console.log(isEmpty({ a: 'a', b: 'b' }))//false
不看不知道,一看吓一跳。。。。代码量如此之多,而且实现起来也是很粗暴的感觉,还有一些现在已经不太需要的兼容ie的方法。
建议使用es5+的原生方法实现。
判断非空数组
function isArray(arr) {
return Array.isArray(arr) && arr.length > 0;
}
console.log(isArray([]))//false
console.log(isArray([1, 2, 3]))//true
console.log(isArray(''))//false
console.log(isArray('123'))//false
console.log(isArray({}))//false
console.log(isArray({ a: 'a', b: 'b' }))//false
判断非空对象
function isObject(obj) {
return Object.prototype.toString.call(obj) == '[object Object]' && Object.keys(obj).length > 0;
}
console.log(isObject([]))//false
console.log(isObject([1, 2, 3]))//false
console.log(isObject(''))//false
console.log(isObject('123'))//false
console.log(isObject({}))//false
console.log(isObject({ a: 'a', b: 'b' }))//true
至于遍历你们就不要那么懒了,数组用forEach或者for 、for...of 都可以的。
对象就用for ... in 。
underscore也只是把2个包了一下而已。
intersection
var _ = {};
// Helper for collection methods to determine whether a collection
// should be iterated as an array or as an object
// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
// Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var property = function (key) {
return function (obj) {
return obj == null ? void 0 : obj[key];
};
};
var getLength = property('length');
var isArrayLike = function (collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// Retrieve the values of an object's properties.
_.values = function (obj) {
var keys = _.keys(obj);
var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};
// Generator function to create the indexOf and lastIndexOf functions
function createIndexFinder(dir, predicateFind, sortedIndex) {
return function (array, item, idx) {
var i = 0, length = getLength(array);
if (typeof idx == 'number') {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) { return idx; }
}
return -1;
};
}
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
// Determine if the array or object contains a given item (using `===`).
// Aliased as `includes` and `include`.
_.contains = _.includes = _.include = function (obj, item, fromIndex, guard) {
if (!isArrayLike(obj)) { obj = _.values(obj); }
if (typeof fromIndex != 'number' || guard) { fromIndex = 0; }
return _.indexOf(obj, item, fromIndex) >= 0;
};
// Produce an array that contains every item shared between all the
// passed-in arrays.
_.intersection = function (array) {
var result = [];
var argsLength = arguments.length;
for (var i = 0, length = getLength(array); i < length; i++) {
var item = array[i];
if (_.contains(result, item)) { continue; }
for (var j = 1; j < argsLength; j++) {
if (!_.contains(arguments[j], item)) { break; }
}
if (j === argsLength) { result.push(item); }
}
return result;
};
console.log(intersection([2, 2, 3, 3, 4],[1, 2, 3]));//2,3
underscore的intersection是会去重且取交集的。
如果不需要去重可以用这个
function intersection(a, b) {
return a.filter(function (v) {
return b.indexOf(v) > -1;
});
}
console.log(_.intersection([1, 2, 3], [2, 2, 3, 3, 4]));//2,3
defer
var _ = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype;
var slice = ArrayProto.slice;
// Determines whether to execute a function as a constructor
// or a normal function with the provided arguments
var executeBound = function (sourceFunc, boundFunc, context, callingContext, args) {
if (!(callingContext instanceof boundFunc)) { return sourceFunc.apply(context, args); }
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
if (_.isObject(result)) { return result; }
return self;
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function (func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function () {
return func.apply(null, args);
}, wait);
};
// Partially apply a function by creating a version that has had some of its
// arguments pre-filled, without changing its dynamic `this` context. _ acts
// as a placeholder, allowing any combination of arguments to be pre-filled.
_.partial = function (func) {
var boundArgs = slice.call(arguments, 1);
var bound = function () {
var position = 0, length = boundArgs.length;
var args = Array(length);
for (var i = 0; i < length; i++) {
args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
}
while (position < arguments.length) { args.push(arguments[position++]); }
return executeBound(func, bound, this, this, args);
};
return bound;
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = _.partial(_.delay, _, 1);
一般不用这么复杂,直接用
setTimeout(()=>{
//
},0)