标签 Underscore 下的文章

Underscore 源码学习(七) - flatten

Underscore 中间 flatten 相关的方法之前一直不是很理解,现在完全搞懂了,稍微说一下。

var flatten = function(input, shallow, strict, output) {
    output = output || [];
    var idx = output.length;
    for (var i = 0, length = getLength(input); i < length; i++) {
        var value = input[i];
        // 若value为数组,把里面东西去出来赋值给output
        // 否则直接赋值给output
        // isArrayLike的判断可以去掉,保留的原因是因为他用来判断value是否为数组很快,可以迅速筛选掉非数组
        if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
        // Flatten current level of array or arguments object.
        if (shallow) {
            // 如果给了shallow参数,只只遍历一层
            var j = 0, len = value.length;
            while (j < len) output[idx++] = value[j++];
        } else {
            // 一直遍历下去,如果是元素则按下面赋值,如果是数组则继续遍历
            flatten(value, shallow, strict, output);
            idx = output.length;
        }
        } else if (!strict) {
        output[idx++] = value;
        }
    }
    return output;
};

这个方法不难看懂,作用是将input平铺展开,如果 shallowtrue,则只展开一层。

_.flatten([1, 2, [3], [[4, [5]]]])              // [1, 2, 3, 4, 5]
_.flatten([1, 2, [3], [[4, [5]]]], true)        // [1, 2, 3, [4, [5]]]

这里的 strict 参数就是之前一直卡住的原因,就是下面这个地方:

_.without = restArgs(function(array, otherArrays) {
    return _.difference(array, otherArrays);
});

_.difference = restArgs(function(array, rest) {
    rest = flatten(rest, true, true);
    // 遍历array,如果array中一个元素包含在rest中,则去掉该元素
    return _.filter(array, function(value){
        return !_.contains(rest, value);
    });
});

这是两个方法,那时候想 without 方法调用的时候, otherArrays是一个数组了,到 difference 方法的时候,这个数组去调用 flatten 方法的时候不是会出问题吗?

_.flatten([1, 2, 3], true, true)        // []

脑子里面就这样想...卡了好久,等我基本看了全部源码才会过来看才理解了。
difference 方法的 restArgs 很重要,他们两个是各自独立的方法,但是 without 可以共用 difference 的逻辑。
上面那样子理解是有问题的,因为在 without 方法中 otherArrays 如果是[1, 2, 3],到了 flatten 调用的时候因为 restArgs 的关系他变成了 [[1, 2, 3]],调用最后返回结果[1, 2, 3]。然后我就纳闷了,加了一层又解除这是何解...
不过抛开 without 方法去看 difference 方法就能理解了。

- 阅读剩余部分 -

Underscore 源码学习(六) - mixin

关于 Underscore 的 mixin 方法。
首先先看个例子:

function Panel() {
  consoe.log(this, this instanceof Panel);
  if(this instanceof Panel) {
    return this;
  } else {
    return new Panel();
  }
}
a = Panel();        // Window   false
b = new Panel();    // Panel{}  true

当函数作为构造器使用时,函数内的 this 执行被新建的对象。当函数被调用时,函数内的 this 则为被调用的对象,在这里是 Window

  var _ = function(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
  };

同样的,如果我们使用下面方法调用:

var under = _();

第二个条件成立,所以新建一个 _ 对象后返回,注意这里是再次调用这个函数。
如果我们这样调用:

var under = new _();

就好像上面第二次调用一样,这时候就构造了 under 这个对象,如果传入了参数 obj,则把 obj 存入 under 这个对象的 _wrapped 属性中。
Underscore 提供了一个 OO 的调用方法,即:

var chainResult = function(instance, obj) {
    return instance._chain ? _(obj).chain() : obj;
};

_.mixin = function(obj) {
    // 遍历obj中的函数
    _.each(_.functions(obj), function(name) {
        // 避免原型链查找,提升性能
        var func = _[name] = obj[name];
        _.prototype[name] = function() {
            // 把wrapped作为数组第一个参数(context),其余传参push到这个数组中
            var args = [this._wrapped];
            push.apply(args, arguments);
            // 如果this是一个_实例,则使用func调用的结果来新建_实例后返回以供继续链式调用
            // 如果this不是一个_实例,则直接返回func调用的结果
            return chainResult(this, func.apply(_, args));
        };
    });
    return _;
};

// 把Underscore对象mixin化,这样就可以直接在_上调用方法
_.mixin(_);

当然我们还可以把自己写的方法通过 mixin 加入到 Underscore 对象中。

- 阅读剩余部分 -

Underscore 源码学习(五) - Throttle 和 Debounce

Underscore 的函数大部分还是挺好理解的,感觉过一遍就行了,不过今天看到两个函数感觉还是挺有意思的,并且也挺常用。这两个函数就是 throttle 和 debounce。

throttle

_.throttle = function(func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  var later = function() {
    // 更改previous即上一次执行时间为当前时间
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var now = _.now();
    // 如果leading为false时禁用第一次首先执行,previous等于now(效果同已经执行过一次,所以第一次被禁用)
    // 这个if语句只在第一次执行该函数的时候有效
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 超时处理和未到时的处理
    if (remaining <= 0 || remaining > wait) {
      // timeout不为null时清除掉并设置为null
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      // 立即调用
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {    // 如果没有禁用最后一次执行
      timeout = setTimeout(later, remaining);               // remaining毫秒后执行later
    }
    // 返回调用的结果
    return result;
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
};

其实一步步去理解也不难,就不细说这个方法了。这个方法有可选项设置,分别为 {leading: false} 和 {trailing: false}。

所以一般有四类情况:

  • 默认情况
    第一次调用时立即响应,之后每个周期内最多执行一次,周期内触发会产生定时执行使在上一次执行时间 preview 周期时间后再次执行。
  • 设置 leadingfalse
    同默认情况区别在于第一次调用不会立即执行而是等待周期时间后再次执行,如果在周期时间内触发,一样等待上一次执行时间 preview 周期时间后再执行。
  • 设置 trailingfalse
    最后周期内最多执行一次,但在周期时间内调用不会触发 timeout,只能在上一次 timeout 失效后调用才能生效并且此时调用将立即执行。
  • 设置 leadingtrailingfalse
    如果同时还设置 leadingfalse 的话,那么第一次调用不会立即执行而是等待周期时间后才执行,在这段时间内调用都不会有效果。

- 阅读剩余部分 -

Underscore 源码学习(四) - Reduce

终于看完了 Underscore 的集合部分了,看 Underscore 源码真的是长见识了,感觉真的受益匪浅。
但是集合里面方法也挺多的,我都不知道该拿哪些出来讲下,最近接触了 Redux,就说下 createReduce 这个方法吧,为后面讲 Redux 做个铺垫。
先看源码

var createReduce = function(dir) {
  var reducer = function(obj, iteratee, memo, initial) {
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        index = dir > 0 ? 0 : length - 1;
    if(!initial) {
      memo = obj[keys ? keys[index] : index];
      index += dir;
    }
    for(; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      memo = iteratee(memo, obj[currentKey], currentKey, obj);
    }
    return memo;
  };

  return function(obj, iteratee, memo, context) {
    var initial = arguments.length >= 3;
    return reducer(obj, opitimizeCb(iteratee, context, 4), memo, initial);
  };
};

这是 reduce 函数的工厂函数,用于生成一个 reducer ,dir 是决定方向用的。
我们从最后一个 return 开始看起,即

return function(obj, iteratee, memo, context) {
  var initial = arguments.length >= 3;
  return reducer(obj, opitimizeCb(iteratee, context, 4), memo, initial);
};

我们使用 reduce 的时候,如果没有指定 memo 值,这时候参数个数只有两个即 obj 和 iteratee,所以 initial 为 false 表示没有初始化。对于没初始的情况,就是增加了一个 if 语句里面的内容而已,作用是把第一个元素作为 memo 值。
接着就是有没有初始化都共用的部分了,通过一个 for 循环把 keys 遍历,并把相应的信息交给 iteratee 去处理,参数 memo 是上一次处理结果。遍历完后把最后的处理结果 memo 返回就完了。
这个函数派生了两个方法,即

_.reduce = _.foldl = _.inject = createReduce(1);
_.reduceRight = _.foldr = createReduce(-1);

只是方向不同而已。
举个例子方便理解些,例如:

var sum = _.reduce([1, 2, 3, 4, 5], function(accumulator, value, index, collection) {
  return accmulator + value;
}, 0);

结果为 15 这个应该很明显,js 原生也有 reduce 方法,如下:

[1, 2, 3, 4, 5].reduce(function(left, right) {
  return left + right;
});

- 阅读剩余部分 -

Underscore 源码学习(三) - 剩余参数

这次主要说剩余参数。
在 ES5 中,如果想要函数接收任意数量的参数,必须使用特殊变量 arguments,举个例子,我们要实现一个加法函数,要求第一个数乘2,然后与其他数相加。

function add() {
    var sum = arguments[0] * 2;
    for(var i = 1; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

在 ES6 中,我们可以使用 ... 操作符,例如:

function add(first, ...args) {
    var sum = first*2;
    for(let arg of args) {
        sum += arg;
    }
    return sum;
}

使用 ES5 我们无法给函数定义参数,而只能通过 arguments 来获取参数,这样写明显带来了可读性的降低。而 ES6 我们就可以在函数声明里面写明参数,对于不定长的参数,则可以使用 ... 操作符。
... 还有另一个常用的应用场景,比如下面例子:

function test() {
    console.log(arguments);
    console.log(arguments instanceof Array);
    console.log(arguments instanceof Object);
}
test(1, 2, 3);
// Result:
[1, 2, 3]
false
true

如果细看输出的[1, 2, 3]会发现他是这样的:
result1.png
我们再试试下面的

var arr = [1, 2, 3];
console.log(arr);
console.log(arr instanceof Array);
console.log(arr instanceof Object);
// Result:
[1, 2, 3]
true
true

再看下[1, 2, 3]这行输出里面是什么:
result2.png
instanceof 我们就知道了 arguments 并不是真正的数组。伪数组实质是一个对象。
要把一个伪数组转为数组,可以这样用

var arr = Array.prototype.slice.call(arguments);

上面这种做法在很多地方都可以看到。除了上面这样做之外,我们还可以使用 ES6 的 Array.from 来处理,如下:

var arr = Array.from(arguments);

但在 ES6 中,我们使用 ... 运算符并不存在这个问题,比如上面第二个例子,args 是一个数组。
鉴于此,我们应该尽量使用 ES6 剩余参数写法和 Array.from 的写法,因为这样更容易理解,而且写起来更简洁。
另外,我们还可以使用 ... 操作符来复制数组,如下:

- 阅读剩余部分 -

Underscore 源码学习(二) - 迭代

Underscore 源码的学习落下了好几天,因为前几天一直正在重构项目和搞 React,不过这几天应该会花较多时间在 Underscore 上面了。
这次主要说下 Underscore 两个比较重要的函数吧,一个是optimizeCb,另一个是cb,这两个花了我挺长时间看的,而且是整个 Underscore 非常重要的函数,后面很多地方都使用到了它。

optimizeCb 函数

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 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);
    };
};

这个地方 switch 只是一个性能的优化,其实简化来看就是这样的

var optimizeCb = function(func, context, argCount) {
    if (context === void 0) return func;
    return function() {
      return func.apply(context, arguments);
    };
};

之所以有那段 switch 前面一篇已经有提到了,只是一个优化而已。使用 call 快于 apply。不过好像最新的 Chrome 已经可以自己优化这个过程,但为了提升性能,加上也无妨。
解释下段代码的意思,字如起名 optimizeCb 优化回调。这个函数传入三个参数依次是函数,上下文,参数个数。如果没有指定上下文则返回函数本身,如果有,则对该上下文绑定到传入的函数,根据传入的参数个数,在做一个性能优化。这个函数就是这个意思。我们看下他的使用。

  _.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;
  };

这个函数是用来实现数组或者对象的遍历的,他是怎么做到呢?
首先是

iteratee = optimizeCb(iteratee, context);

- 阅读剩余部分 -