前言
這是underscore.js源碼分析的第六篇,如果你對(duì)這個(gè)系列感興趣,歡迎點(diǎn)擊
underscore-analysis/ watch一下,隨時(shí)可以看到動(dòng)態(tài)更新。
指定調(diào)用次數(shù)(after, before)下劃線中有非常多很有趣的方法,可以用比較巧妙的方式解決我們?nèi)粘I钪杏龅降膯?wèn)題,比如_.after,_.before,_.defer...等,也許你已經(jīng)用過(guò)他們了,今天我們來(lái)深入源碼,一探究竟,他們到底是怎么實(shí)現(xiàn)的。
把這兩個(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ù)是null護(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"
用下劃線中的compose方法怎么做呢
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ì)另外多帶帶寫一篇文章介紹,歡迎前往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)幫助。
點(diǎn)一個(gè)小星星吧???
點(diǎn)一個(gè)小星星吧???
點(diǎn)一個(gè)小星星吧???
good night ?
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/50805.html
前言 這是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)的。 showIm...
摘要:類總所周知,不像其他面向?qū)ο笳Z(yǔ)言那樣支持類,但是可以通過(guò)函數(shù)和原型來(lái)模擬類。如果你學(xué)習(xí)過(guò)或者其他面向?qū)ο笳Z(yǔ)言的話,你會(huì)覺(jué)得很熟悉。結(jié)論下一個(gè)版本的會(huì)帶來(lái)一個(gè)更加簡(jiǎn)單更加友好的語(yǔ)法來(lái)幫助那些從面向?qū)ο笳Z(yǔ)言轉(zhuǎn)過(guò)來(lái)的開發(fā)者的學(xué)習(xí)。 原文地址:http://www.frontendjournal.com/javascript-es6-learn-important-features-in-a-...
摘要:第一部分介紹了如何使用和開發(fā)接口。由于系統(tǒng)變得越來(lái)越復(fù)雜,人們提出了稱為預(yù)處理器和后處理器的工具來(lái)管理復(fù)雜性。當(dāng)您第一次得知有預(yù)處理器和后處理器時(shí),你很有可能在任何地方已經(jīng)使用它們。我之前建議的文章,,也涵蓋了預(yù)處理器相關(guān)的知識(shí)。 我記得我剛開始學(xué)習(xí)前端開發(fā)的時(shí)候。我看到了很多文章及資料,被學(xué)習(xí)的資料壓得喘不過(guò)氣來(lái),甚至不知道從哪里開始。 本指南列出前端學(xué)習(xí)路線,并提供了平時(shí)收藏的一些...
閱讀 923·2023-04-26 01:34
閱讀 3356·2023-04-25 20:58
閱讀 3263·2021-11-08 13:22
閱讀 2108·2019-08-30 14:17
閱讀 2522·2019-08-29 15:27
閱讀 2673·2019-08-29 12:45
閱讀 2996·2019-08-29 12:26
閱讀 2811·2019-08-28 17:51