前言
這是underscore.js源碼分析的第六篇,如果你對(duì)這個(gè)系列感興趣,歡迎點(diǎn)擊
underscore-analysis/ watch一下,隨時(shí)可以看到動(dòng)態(tài)更新。
下劃線中有非常多很有趣的方法,可以用比較巧妙的方式解決我們?nèi)粘I钪杏龅降膯?wèn)題,比如
_.after
,_.before
,_.defer
...等,也許你已經(jīng)用過(guò)他們了,今天我們來(lái)深入源碼,一探究竟,他們到底是怎么實(shí)現(xiàn)的。
指定調(diào)用次數(shù)(after
, before
)
把這兩個(gè)方法放在前面也是因?yàn)樗麄儌z能夠解決我們工作中至少以下兩個(gè)問(wèn)題
如果你要等多個(gè)異步請(qǐng)求完成之后才去執(zhí)行某個(gè)操作
fn
,那么你可以用_.after
,而不必寫多層異步回調(diào)地獄去實(shí)現(xiàn)需求有一些應(yīng)用可能需要進(jìn)行初始化操作而且僅需要一次初始化就可以,一般的?做法是在入口處對(duì)某個(gè)變量進(jìn)行判斷,如果為真那么認(rèn)為已經(jīng)初始化過(guò)了直接
return
掉,如果為假那么進(jìn)行?參數(shù)的初始化工作,并在完成初始化之后設(shè)置該變量為?真,那么?下次進(jìn)入的時(shí)候便不必重復(fù)初始化了。
對(duì)于問(wèn)題1
let async1 = (cb) => {
setTimeout(() => {
console.log('異步任務(wù)1結(jié)束了')
cb()
}, 1000)
}
let async2 = (cb) => {
setTimeout(() => {
console.log('異步任務(wù)2結(jié)束了')
cb()
}, 2000)
}
let fn = () => {
console.log('我是兩個(gè)任務(wù)都結(jié)束了才進(jìn)行的任務(wù)')
}
如果要在任務(wù)1,和任務(wù)2都結(jié)束了才進(jìn)行fn
任務(wù),我們一般的寫法是啥?
可能會(huì)下面這樣寫
async1(() => {
async2(fn)
})
這樣?確實(shí)可以保證任務(wù)fn
是在前面兩個(gè)異步任務(wù)都結(jié)束之后才進(jìn)行,但是相信你是不太?喜歡回調(diào)的寫法的,這里舉的異步任務(wù)只有兩個(gè),如果多了起來(lái),恐怕就要蛋疼了。別疼,用下劃線的after
函數(shù)可以解救你。
fn = _.after(2, fn)
async1(fn)
async2(fn)
運(yùn)行截圖
有木有很爽,不用寫成回調(diào)地獄的形式了。那么接下來(lái)我們看看源碼是怎么實(shí)現(xiàn)的。
after源碼實(shí)現(xiàn)
_.after = function(times, func) {
return function() {
// 只有返回的函數(shù)被調(diào)用times次之后才執(zhí)行func操作
if (--times < 1) {
return func.apply(this, arguments);
}
};
};
源碼?簡(jiǎn)單到要死啊,但是就是這么神奇,妥妥地解決了我們的問(wèn)題1。
對(duì)于問(wèn)題2
let app = {
init (name, sex) {
if (this.initialized) {
return
}
// 進(jìn)行參數(shù)的初始化工作
this.name = name
this.sex = sex
// 初始化完成,設(shè)置標(biāo)志
this.initialized = true
},
showInfo () {
console.log(this.name, this.sex)
}
}
// ?傳參數(shù)進(jìn)行應(yīng)用的初始化
app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意?這里打印出來(lái)的是第一次傳入的參數(shù)
一般需要且只進(jìn)行一次參數(shù)初始化工作的時(shí)候,我們可能會(huì)像上面那樣做。但是其實(shí)如果用下劃線中的before
方法我們還可以這樣做。
let app = {
init: _.before(2, function (name, sex) {
// 進(jìn)行參數(shù)的初始化工作
this.name = name
this.sex = sex
}) ,
showInfo () {
console.log(this.name, this.sex)
}
}
// ?傳參數(shù)進(jìn)行應(yīng)用的初始化
app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意?這里打印出來(lái)的是第一次傳入的參數(shù)
好玩吧,讓我們看看_.before
是怎么實(shí)現(xiàn)的。
// 創(chuàng)建一個(gè)函數(shù),這個(gè)函數(shù)調(diào)用次數(shù)不超過(guò)times次
// 如果次數(shù) >= times 則最后一次調(diào)用函數(shù)的返回值將被記住并一直返回該值
_.before = function(times, func) {
var memo;
return function() {
// 返回的函數(shù)每次調(diào)用都times減1
if (--times > 0) {
// 調(diào)用func,并傳入外面?zhèn)鬟M(jìn)來(lái)的參數(shù)
// 需要注意的是,后一次調(diào)用的返回值會(huì)覆蓋前一次
memo = func.apply(this, arguments);
}
// 當(dāng)調(diào)用次數(shù)夠了,就將func銷毀設(shè)置為null
if (times <= 1) func = null;
return memo;
};
};
讓函數(shù)具有記憶的功能
在程序中我們經(jīng)常會(huì)要進(jìn)行一些計(jì)算的操作,當(dāng)遇到比較耗時(shí)的操作時(shí)候,如果有一種機(jī)制,對(duì)于同樣的輸入,一定得到相同的輸出,并且對(duì)于同樣的輸入,后續(xù)的計(jì)算直接從緩存中讀取,不再需要將計(jì)算程序運(yùn)行那就非常贊了。
舉例
let calculate = (num, num2) => {
let result = 0
let start = Date.now()
for (let i = 0; i< 10000000; i++) { // 這里只是模擬耗時(shí)的操作
result += num
}
for (let i = 0; i< 10000000; i++) { // 這里只是模擬耗時(shí)的操作
result += num2
}
let end = Date.now()
console.log(end - start)
return result
}
calculate(1, 2) // 30000000
// log 得到235
calculate(1, 2) // 30000000
// log 得到249
對(duì)于上面這個(gè)calculate函數(shù),同樣的輸入1, 2,兩次調(diào)用的輸出都?是一樣的,并且兩次都走了兩個(gè)耗時(shí)的循環(huán),看看下劃線中的memoize
函數(shù),如何為我們省去第二次的耗時(shí)操作,直接給出300000
的返回值
let calculate = _.memoize((num, num2) => {
let start = Date.now()
let result = 0
for (let i = 0; i< 10000000; i++) { // 這里只是模擬耗時(shí)的操作
result += num
}
for (let i = 0; i< 10000000; i++) { // 這里只是模擬耗時(shí)的操作
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 啥也沒(méi)有打印出,因?yàn)橹苯訌木彺嬷凶x取了
源碼實(shí)現(xiàn)
_.memoize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache;
// 注意hasher,如果傳了hasher,就用hasher()執(zhí)行的結(jié)果作為緩存func()執(zhí)行的結(jié)果的key
var address = '' + (hasher ? hasher.apply(this, arguments) : key);
// 如果沒(méi)有在cache中查找到對(duì)應(yīng)的key就去計(jì)算一次,并緩存下來(lái)
if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
// 返回結(jié)果
return cache[address];
};
memoize.cache = {};
return memoize; // 返回一個(gè)具有cache靜態(tài)屬性的函數(shù)
};
相信你已經(jīng)看懂了源碼實(shí)現(xiàn),是不是很簡(jiǎn)單,但是又很實(shí)用有趣。
來(lái)一下延時(shí)(.delay和.defer)
下劃線中在原生延遲函數(shù)
setTimeout
的基礎(chǔ)上做了一些改造,產(chǎn)生以上兩個(gè)?函數(shù)
*_.delay(function, wait, arguments)
就是延遲wait
時(shí)間去執(zhí)行function
,function
需要的參數(shù)由*arguments
提供
使用舉例
var log = _.bind(console.log, console)
_.delay(log, 1000, 'hello qianlongo')
// 1秒后打印出 hello qianlongo
源碼實(shí)現(xiàn)
_.delay = function(func, wait) {
// 讀取第三個(gè)參數(shù)開始的其他參數(shù)
var args = slice.call(arguments, 2);
return setTimeout(function(){
// 執(zhí)行func并將參數(shù)傳入,注意apply的第一個(gè)參數(shù)是nul?l護(hù)著undefined的時(shí)候,func內(nèi)部的this指的是全局的window或者global
return func.apply(null, args);
}, wait);
};
不過(guò)有點(diǎn)需要注意的是_.delay(function, wait, *arguments)``function
中的this
指的是window
或者global
*_.defer(function, arguments)
延遲調(diào)用function直到當(dāng)前調(diào)用棧清空為止,類似使用延時(shí)為0的setTimeout方法。對(duì)于執(zhí)行開銷大的計(jì)算和無(wú)阻塞UI線程的HTML渲染時(shí)候非常有用。 如果傳遞arguments參數(shù),當(dāng)函數(shù)function執(zhí)行時(shí), arguments 會(huì)作為參數(shù)傳入
源碼實(shí)現(xiàn)
_.defer = _.partial(_.delay, _, 1);
所以主要還是看_.partial
是個(gè)啥
可以預(yù)指定參數(shù)的函數(shù)_.partial
局部應(yīng)用一個(gè)函數(shù)填充在任意個(gè)數(shù)的 參數(shù),不改變其動(dòng)態(tài)this值。和bind方法很相近。你可以在你的參數(shù)列表中傳遞_來(lái)指定一個(gè)參數(shù) ,不應(yīng)該被預(yù)先填充(underscore中文網(wǎng)翻譯)
使用舉例
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
可以看到,我們傳入了_
(這里指的是下劃線本身)進(jìn)行占位,后續(xù)再講2和4填充到對(duì)應(yīng)的位置去了。
源碼具體怎么實(shí)現(xiàn)的呢?
_.partial = function(func) {
// 獲取除了傳進(jìn)回調(diào)函數(shù)之外的其他預(yù)參數(shù)
var boundArgs = slice.call(arguments, 1);
var bound = function() {
var position = 0, length = boundArgs.length;
// 先創(chuàng)建一個(gè)和boundArgs長(zhǎng)度等長(zhǎng)的空數(shù)組
var args = Array(length);
// 處理占位元素_
for (var i = 0; i < length; i++) {
// 如果發(fā)現(xiàn)boundArgs中有_的占位元素,就依次用arguments中的元素進(jìn)行替換boundArgs
args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
}
// 把a(bǔ)uguments中的其他元素添加到boundArgs中
while (position < arguments.length) args.push(arguments[position++]);
// 最后執(zhí)行executeBound,接下來(lái)看看executeBound是什么
return executeBound(func, bound, this, this, args);
};
return bound;
};
在上一篇文章如何寫一個(gè)實(shí)用的bind?
有詳細(xì)講解,這里我們?cè)倩仡櫼幌?br>
executeBound
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
// 如果調(diào)用方式不是new func的形式就直接調(diào)用sourceFunc,并且給到對(duì)應(yīng)的參數(shù)即可
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
// 處理new調(diào)用的形式
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
if (_.isObject(result)) return result;
return self;
};
先看一下這些參數(shù)都?代表什么含義
- sourceFunc:原函數(shù),待綁定函數(shù)
- boundFunc: 綁定后函數(shù)
- context:綁定后函數(shù)this指向的上下文
- callingContext:綁定后函數(shù)的執(zhí)行上下文,通常就是 this
- args:綁定后的函數(shù)執(zhí)行所需參數(shù)
這里?其實(shí)就是執(zhí)行了這句,所以關(guān)鍵還是如果處理預(yù)參數(shù),和后續(xù)參數(shù)的邏輯
sourceFunc.apply(context, args);
管道式函數(shù)組合
你也許遇到過(guò)這種場(chǎng)景,任務(wù)A,任務(wù)B,任務(wù)C必須按照?順序?執(zhí)行,并且A的?輸出作為B的輸入,B的輸出作為C的輸入,左后再得到結(jié)果。用一張圖表示如下
那么一般的做法是什么呢
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"
看起來(lái)沒(méi)有一般的做法那樣,層層繞進(jìn)去了,而是以一種非常扁平的方式使用。
?同樣我們看看源碼是怎么實(shí)現(xiàn)的。
_.compose源碼
_.compose = function() {
var args = arguments;
// 從最后一個(gè)參數(shù)開始處理
var start = args.length - 1;
return function() {
var i = start;
// 執(zhí)行最后一個(gè)函數(shù),并得到結(jié)果result
var result = args[start].apply(this, arguments);
// 從后往前一個(gè)個(gè)調(diào)用傳進(jìn)來(lái)的函數(shù),并將上一次執(zhí)行的結(jié)果作為參數(shù)傳進(jìn)下一個(gè)函數(shù)
while (i--) result = args[i].call(this, result);
// 最后將結(jié)果導(dǎo)出
return result;
};
};
給多個(gè)函數(shù)綁定同樣的上下文(_.bindAll(object, *methodNames))
將多個(gè)函數(shù)methodNames綁定上下文環(huán)境為
object
?? ?? ??,好困,寫文章當(dāng)真好要時(shí)間和精力,到這里已經(jīng)快寫了3個(gè)小時(shí)了,夜深,好像躺下睡覺(jué)?。。。“““?,再等等快說(shuō)完了(希望不會(huì)誤人子弟)。
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);
我們用官網(wǎng)給的例子說(shuō)一下,默認(rèn)的jQuery中$(selector).on(eventName, callback)
callback中?的this
指的是當(dāng)前的元素本身,當(dāng)時(shí)經(jīng)過(guò)上面的處理,會(huì)彈出underscore
。
_.bindAll源碼實(shí)現(xiàn)
_.bindAll = function(obj) {
var i, length = arguments.length, key;
// 必須要指定需要綁定到obj的函數(shù)參數(shù)
if (length <= 1) throw new Error('bindAll must be passed function names');
// 從第一個(gè)實(shí)參開始處理,這些便是需要綁定this作用域到obj的函數(shù)
for (i = 1; i < length; i++) {
key = arguments[i];
// 調(diào)用內(nèi)部的bind方法進(jìn)行this綁定
obj[key] = _.bind(obj[key], obj);
}
return obj;
};
內(nèi)部使用了_.bind
進(jìn)行綁定,如果你對(duì)_.bind
原生是如何實(shí)現(xiàn)的可以看這里如何寫一個(gè)實(shí)用的bind?
拾遺
最后關(guān)于underscore.js中function篇章還有兩個(gè)函數(shù)說(shuō)一下,另外節(jié)流函數(shù)
throttle
以及debounce_
會(huì)另外?單獨(dú)?寫一篇文章介紹,歡迎前往underscore-analysis/ watch一下,隨時(shí)可以看到動(dòng)態(tài)更新。
_.wrap(function, wrapper)
將第一個(gè)函數(shù) function 封裝到函數(shù) wrapper 里面, 并把函數(shù) function 作為第一個(gè)參數(shù)傳給 wrapper. 這樣可以讓 wrapper 在 function 運(yùn)行之前和之后 執(zhí)行代碼, 調(diào)整參數(shù)然后附有條件地執(zhí)行.
直接看源碼實(shí)現(xiàn)吧
_.wrap = function(func, wrapper) {
return _.partial(wrapper, func);
};
還記得前面說(shuō)的partial
吧,他會(huì)返回一個(gè)函數(shù),內(nèi)部會(huì)執(zhí)行wrapper
,并且func
?會(huì)作為wrapper
的一個(gè)參數(shù)被傳入。
_.negate(predicate)
將predicate函數(shù)執(zhí)行的結(jié)果取反。
使用舉例
let fn = () => {
return true
}
_.negate(fn)() // false
看起來(lái)好像沒(méi)什么軟用,但是。。。。
let arr = [1, 2, 3, 4, 5, 6]
let findEven = (num) => {
return num % 2 === 0
}
arr.filter(findEven) // [2, 4, 6]
如果要找到奇數(shù)呢?
let arr = [1, 2, 3, 4, 5, 6]
let findEven = (num) => {
return num % 2 === 0
}
arr.filter(_.negate(findEven)) // [1, 3, 5]
源碼實(shí)現(xiàn)
_.negate = function(predicate) {
return function() {
return !predicate.apply(this, arguments);
};
};
源碼很簡(jiǎn)單,就是把你傳進(jìn)來(lái)的predicate函數(shù)執(zhí)行的結(jié)果取反一下,但是應(yīng)用還是蠻多的。
結(jié)尾
這幾個(gè)是underscore庫(kù)中function相關(guān)的api,大部分已經(jīng)說(shuō)完了,如果對(duì)你有一點(diǎn)點(diǎn)幫助。
good night ??