晴明的博客园 GitHub      CodePen      CodeWars     

[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)
posted @ 2017-10-16 19:56  晴明桑  阅读(232)  评论(0编辑  收藏  举报