qianlongo/underscore-analysis

你要看看这些有趣的函数方法吗?

qianlongo opened this issue · 0 comments

前言

这是underscore.js源码分析的第六篇,如果你对这个系列感兴趣,欢迎点击

underscore-analysis/ watch一下,随时可以看到动态更新。

下划线中有非常多很有趣的方法,可以用比较巧妙的方式解决我们日常生活中遇到的问题,比如_.after,_.before_.defer...等,也许你已经用过他们了,今天我们来深入源码,一探究竟,他们到底是怎么实现的。

function

指定调用次数(after, before)

把这两个方法放在前面也是因为他们俩能够解决我们工作中至少以下两个问题

  1. 如果你要等多个异步请求完成之后才去执行某个操作fn,那么你可以用_.after,而不必写多层异步回调地狱去实现需求

  2. 有一些应用可能需要进行初始化操作而且仅需要一次初始化就可以,一般的做法是在入口处对某个变量进行判断,如果为真那么认为已经初始化过了直接return掉,如果为假那么进行参数的初始化工作,并在完成初始化之后设置该变量为真,那么下次进入的时候便不必重复初始化了。

对于问题1

let async1 = (cb) => {
  setTimeout(() => {
    console.log('异步任务1结束了')
    cb()
  }, 1000)
}

let async2 = (cb) => {
  setTimeout(() => {
    console.log('异步任务2结束了')
    cb()
  }, 2000)
}

let fn = () => {
  console.log('我是两个任务都结束了才进行的任务')
}

如果要在任务1,和任务2都结束了才进行fn任务,我们一般的写法是啥?
可能会下面这样写

async1(() => {
  async2(fn)
})

这样确实可以保证任务fn是在前面两个异步任务都结束之后才进行,但是相信你是不太喜欢回调的写法的,这里举的异步任务只有两个,如果多了起来,恐怕就要蛋疼了。别疼,用下划线的after函数可以解救你。

fn = _.after(2, fn)

async1(fn)
async2(fn)

运行截图

after举例

有木有很爽,不用写成回调地狱的形式了。那么接下来我们看看源码是怎么实现的。

after源码实现

_.after = function(times, func) {
  return function() {
    // 只有返回的函数被调用times次之后才执行func操作
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
};

源码简单到要死啊,但是就是这么神奇,妥妥地解决了我们的问题1。

对于问题2

let app = {
  init (name, sex) {
    if (this.initialized) {
      return
    }
    // 进行参数的初始化工作
    this.name = name
    this.sex = sex
    // 初始化完成,设置标志
    this.initialized = true
  },
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// 传参数进行应用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意这里打印出来的是第一次传入的参数

一般需要且只进行一次参数初始化工作的时候,我们可能会像上面那样做。但是其实如果用下划线中的before方法我们还可以这样做。

let app = {
  init: _.before(2, function (name, sex) {
    // 进行参数的初始化工作
    this.name = name
    this.sex = sex
  }) ,
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// 传参数进行应用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意这里打印出来的是第一次传入的参数

好玩吧,让我们看看_.before是怎么实现的。

// 创建一个函数,这个函数调用次数不超过times次
// 如果次数 >= times 则最后一次调用函数的返回值将被记住并一直返回该值

_.before = function(times, func) {
  var memo;
  return function() {
    // 返回的函数每次调用都times减1
    if (--times > 0) { 
      // 调用func,并传入外面传进来的参数
      // 需要注意的是,后一次调用的返回值会覆盖前一次
      memo = func.apply(this, arguments);
    }
    // 当调用次数够了,就将func销毁设置为null
    if (times <= 1) func = null;
    return memo;
  };
};

让函数具有记忆的功能

在程序中我们经常会要进行一些计算的操作,当遇到比较耗时的操作时候,如果有一种机制,对于同样的输入,一定得到相同的输出,并且对于同样的输入,后续的计算直接从缓存中读取,不再需要将计算程序运行那就非常赞了。

举例

let calculate = (num, num2) => {
  let result = 0
  let start = Date.now()
  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num
  }

  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num2
  }
  let end = Date.now()
  console.log(end - start)
  return result
}

calculate(1, 2) // 30000000
// log 得到235
calculate(1, 2) // 30000000
// log 得到249

对于上面这个calculate函数,同样的输入1, 2,两次调用的输出都是一样的,并且两次都走了两个耗时的循环,看看下划线中的memoize函数,如何为我们省去第二次的耗时操作,直接给出300000的返回值

let calculate = _.memoize((num, num2) => {
  let start = Date.now()
  let result = 0
  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num
  }

  for (let i = 0; i< 10000000; i++) { // 这里只是模拟耗时的操作
    result += num2
  }
  let end = Date.now()
  console.log(end - start)
  return result
}, function () {
  return [].join.call(arguments, '@') // 这里是为了给同样的输入指定唯一的缓存key
})

calculate(1, 2) // 30000000
// log 得到 238
calculate(1, 2) // 30000000
// log 啥也没有打印出,因为直接从缓存中读取了

源码实现

 _.memoize = function(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    // 注意hasher,如果传了hasher,就用hasher()执行的结果作为缓存func()执行的结果的key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key); 
    // 如果没有在cache中查找到对应的key就去计算一次,并缓存下来
    if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 
    // 返回结果
    return cache[address];
  };
  memoize.cache = {};
  return memoize; // 返回一个具有cache静态属性的函数
};

相信你已经看懂了源码实现,是不是很简单,但是又很实用有趣。

来一下延时(.delay和.defer)

下划线中在原生延迟函数setTimeout的基础上做了一些改造,产生以上两个函数

*_.delay(function, wait, arguments)

就是延迟wait时间去执行functionfunction需要的参数由*arguments提供

使用举例

var log = _.bind(console.log, console)
_.delay(log, 1000, 'hello qianlongo')
// 1秒后打印出 hello qianlongo

源码实现

_.delay = function(func, wait) {
  // 读取第三个参数开始的其他参数
  var args = slice.call(arguments, 2);
  return setTimeout(function(){
    // 执行func并将参数传入,注意apply的第一个参数是null护着undefined的时候,func内部的this指的是全局的window或者global
    return func.apply(null, args); 
  }, wait);
};

不过有点需要注意的是_.delay(function, wait, *arguments)``function中的this指的是window或者global

*_.defer(function, arguments)

延迟调用function直到当前调用栈清空为止,类似使用延时为0的setTimeout方法。对于执行开销大的计算和无阻塞UI线程的HTML渲染时候非常有用。 如果传递arguments参数,当函数function执行时, arguments 会作为参数传入

源码实现

_.defer = _.partial(_.delay, _, 1);

所以主要还是看_.partial是个啥

可以预指定参数的函数_.partial

局部应用一个函数填充在任意个数的 参数,不改变其动态this值。和bind方法很相近。你可以在你的参数列表中传递_来指定一个参数 ,不应该被预先填充(underscore中文网翻译)

使用举例

let fn = (num1, num2, num3, num4) => {
  let str = `num1=${num1}`
  str += `num2=${num2}`
  str += `num3=${num3}`
  str += `num4=${num4}`
  return str
}

fn = _.partial(fn, 1, _, 3, _)
fn(2,4)// num1=1num2=2num3=3num4=4

可以看到,我们传入了_(这里指的是下划线本身)进行占位,后续再讲2和4填充到对应的位置去了。

源码具体怎么实现的呢?

_.partial = function(func) {
  // 获取除了传进回调函数之外的其他预参数
  var boundArgs = slice.call(arguments, 1); 
  var bound = function() {
    var position = 0, length = boundArgs.length;
    // 先创建一个和boundArgs长度等长的空数组
    var args = Array(length); 
    // 处理占位元素_
    for (var i = 0; i < length; i++) { 
      // 如果发现boundArgs中有_的占位元素,就依次用arguments中的元素进行替换boundArgs
      args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; 
    }
    // 把auguments中的其他元素添加到boundArgs中
    while (position < arguments.length) args.push(arguments[position++]); 
    // 最后执行executeBound,接下来看看executeBound是什么
    return executeBound(func, bound, this, this, args);
  };
  return bound;
};

在上一篇文章如何写一个实用的bind?
有详细讲解,这里我们再回顾一下
executeBound

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 如果调用方式不是new func的形式就直接调用sourceFunc,并且给到对应的参数即可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
   // 处理new调用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

先看一下这些参数都�代表什么含义

  1. sourceFunc:原函数,待绑定函数
  2. boundFunc: 绑定后函数
  3. context:绑定后函数this指向的上下文
  4. callingContext:绑定后函数的执行上下文,通常就是 this
  5. args:绑定后的函数执行所需参数

这里其实就是执行了这句,所以关键还是如果处理预参数,和后续参数的逻辑

sourceFunc.apply(context, args);

管道式函数组合

你也许遇到过这种场景,任务A,任务B,任务C必须按照顺序执行,并且A的输出作为B的输入,B的输出作为C的输入,左后再得到结果。用一张图表示如下

管道

那么一般的做法是什么呢

let funcA = (str) => {
  return str += '-A'
}

let funcB = (str) => {
  return str += '-B'
}

let funcC = (str) => {
  return str += '-C'
}

funcC(funcB(funcA('hello')))
// "hello-A-B-C"

``` javascript
用下划线中的`compose`方法怎么做呢

``` javascript
let fn = _.compose(funcC, funcB, funcA)
fn('hello')
// "hello-A-B-C"

看起来没有一般的做法那样,层层绕进去了,而是以一种非常扁平的方式使用。

同样我们看看源码是怎么实现的。

_.compose源码

_.compose = function() {
  var args = arguments;
  // 从最后一个参数开始处理
  var start = args.length - 1;
  return function() {
    var i = start;
    // 执行最后一个函数,并得到结果result
    var result = args[start].apply(this, arguments); 
    // 从后往前一个个调用传进来的函数,并将上一次执行的结果作为参数传进下一个函数
    while (i--) result = args[i].call(this, result); 
    // 最后将结果导出
    return result;
  };
};

给多个函数绑定同样的上下文(_.bindAll(object, *methodNames))

将多个函数methodNames绑定上下文环境为object

😪 😪 😪,好困,写文章当真好要时间和精力,到这里已经快写了3个小时了,夜深,好像躺下睡觉啊!!!啊啊啊,再等等快说完了(希望不会误人子弟)。

var buttonView = {
  label  : 'underscore',
  onClick: function(){ alert('clicked: ' + this.label); },
  onHover: function(){ console.log('hovering: ' + this.label); }
};
_.bindAll(buttonView, 'onClick', 'onHover');

$('#underscore_button').bind('click', buttonView.onClick);

我们用官网给的例子说一下,默认的jQuery中$(selector).on(eventName, callback)callback中的this指的是当前的元素本身,当时经过上面的处理,会弹出underscore

_.bindAll源码实现

 _.bindAll = function(obj) {
  var i, length = arguments.length, key;
  // 必须要指定需要绑定到obj的函数参数
  if (length <= 1) throw new Error('bindAll must be passed function names');
  // 从第一个实参开始处理,这些便是需要绑定this作用域到obj的函数
  for (i = 1; i < length; i++) { 
    key = arguments[i];
    // 调用内部的bind方法进行this绑定
    obj[key] = _.bind(obj[key], obj); 
  }
  return obj;
};

内部使用了_.bind进行绑定,如果你对_.bind原生是如何实现的可以看这里如何写一个实用的bind?

拾遗

最后关于underscore.js中function篇章还有两个函数说一下,另外节流函数throttle以及debounce_会另外单独写一篇文章介绍,欢迎前往underscore-analysis/ watch一下,随时可以看到动态更新。

_.wrap(function, wrapper)

将第一个函数 function 封装到函数 wrapper 里面, 并把函数 function 作为第一个参数传给 wrapper. 这样可以让 wrapper 在 function 运行之前和之后 执行代码, 调整参数然后附有条件地执行.

直接看源码实现吧

_.wrap = function(func, wrapper) {
    return _.partial(wrapper, func);
  };

还记得前面说的partial吧,他会返回一个函数,内部会执行wrapper,并且func会作为wrapper的一个参数被传入。

_.negate(predicate)

将predicate函数执行的结果取反。

使用举例

let fn = () => {
  return true
}

_.negate(fn)() // false

看起来好像没什么软用,但是。。。。

let arr = [1, 2, 3, 4, 5, 6]

let findEven = (num) => {
  return num % 2 === 0
}

arr.filter(findEven) // [2, 4, 6]

如果要找到奇数呢?

let arr = [1, 2, 3, 4, 5, 6]

let findEven = (num) => {
  return num % 2 === 0
}

arr.filter(_.negate(findEven)) // [1, 3, 5]

源码实现

_.negate = function(predicate) {
  return function() {
    return !predicate.apply(this, arguments);
  };
};

源码很简单,就是把你传进来的predicate函数执行的结果取反一下,但是应用还是蛮多的。

结尾

这几个是underscore库中function相关的api,大部分已经说完了,如果对你有一点点帮助。

点一个小星星吧😀😀😀

点一个小星星吧😀😀😀

点一个小星星吧😀😀😀

good night 🌙