由淺入深、逐個擊破 30SecondsOfCode 中函數系列全部源碼片斷,帶你領略源碼之美。javascript
本系列是對名庫 30SecondsOfCode 的深刻刨析。java
本篇是其中的函數篇,能夠在極短的時間內培養你的函數式思惟。git
內容根據源碼的難易等級進行排版,目錄以下:es6
const checkProp = (predicate, prop) => obj => !!predicate(obj[prop]); const lengthIs4 = checkProp(l => l === 4, 'length'); lengthIs4([]); // false lengthIs4([1, 2, 3, 4]); // true lengthIs4(new Set([1, 2, 3, 4])); // false (Set uses Size, not length) const session = { user: {} }; const validUserSession = checkProp(u => u.active && !u.disabled, 'user'); validUserSession(session); // false session.user.active = true; validUserSession(session); // true const noLength = checkProp(l => l === undefined, 'length'); noLength([]); // false noLength({}); // true noLength(new Set()); // true
做用:檢查參數是否存在給定的屬性。github
解析:給定一個檢查函數,和所需檢查的屬性名,返回一個函數。可經過調用 返回的函數,去斷定 傳入的對象參數是否符合檢查函數。面試
const functionName = fn => (console.debug(fn.name), fn); functionName(Math.max); // max (logged in debug channel of console)
做用:打印函數名。編程
解析:使用console.debug
API 和函數的name
屬性,把 函數類型參數的名字 打印到控制檯的debug channel中。後端
const negate = func => (...args) => !func(...args); [1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]
做用:反轉 謂詞函數(返回類型爲布爾的函數)的返回結果。數組
解析:假設有一謂詞函數爲func = args => bool
,咱們想要反轉其結果,即可對它的調用方式進行進一步的抽象,把反轉結果的邏輯放置抽象中。promise
在本函數中,只須要一個 邏輯非運算符!func(...args)
。
而擴展運算符...
是對參數的抽象,表明的是傳入的全部參數,咱們要將全部參數一個不差地傳遞,不可破環 謂詞函數的「純潔性」。
const unary = fn => val => fn(val); ['6', '8', '10'].map(unary(parseInt)); // [6, 8, 10]
做用:參數函數調用時 只接受 參數函數的第一個參數,忽略其餘參數。
解析:包裝一個函數,並不作任何處理:wrap = fn => (...args) => fn(...args)
很顯然,若是想對傳入的參數進行處理,只需對args
動刀,而本例直接使用了單獨的一個變量,忽略了其餘參數。
const ary = (fn, n) => (...args) => fn(...args.slice(0, n)); const firstTwoMax = ary(Math.max, 2); [[2, 6, 'a'], [6, 4, 8], [10]].map(x => firstTwoMax(...x)); // [6, 6, 1
做用:參數函數調用時 只接受 參數函數的前 n 個參數,忽略其餘參數。
解析:和上列邏輯一模一樣,只不過處理參數的邏輯換成了...args.slice(0, n)
,只要前n個。
const attempt = (fn, ...args) => { try { return fn(...args); } catch (e) { return e instanceof Error ? e : new Error(e); } }; var elements = attempt(function(selector) { return document.querySelectorAll(selector); }, '>_>'); if (elements instanceof Error) elements = []; // elements = []
做用:對 參數函數 進行異常捕獲,若是有異常則拋出。
解析:對 參數函數 進行進一步封裝,本例封裝的邏輯是try catch
,即捕獲參數函數的異常。
好久以前,我看到過一個關於java8
的 attempt 片斷,裏面還增長了重試邏輯。
js 實現代碼以下:
const attempt = (fn, ...args, count, bound) => { try { return fn(...args); } catch (e) { if(count == bound){ return e instanceof Error ? e : new Error(e); } return attempt(fn, ...args, count + 1, bound) } };
const bind = (fn, context, ...boundArgs) => (...args) => fn.apply(context, [...boundArgs, ...args]); function greet(greeting, punctuation) { return greeting + ' ' + this.user + punctuation; } const freddy = { user: 'fred' }; const freddyBound = bind(greet, freddy); console.log(freddyBound('hi', '!')); // 'hi fred!'
做用:原生API-bind
的另外一種實現。
fn.bind(context,...args)
=> bind(fn,context,...args)
bind()
方法建立一個新的函數,在bind()
被調用時,這個新函數的this
被指定爲bind()
的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。
解析:首先,使用了apply
將給定的 上下文參數 應用於 參數函數。
其次,利用 apply 只接受數組做爲參數的規定,將最初傳入的參數,和後續傳入的參數按順序合併在一個數組中傳遞進去。
const bindKey = (context, fn, ...boundArgs) => (...args) => context[fn].apply(context, [...boundArgs, ...args]); const freddy = { user: 'fred', greet: function(greeting, punctuation) { return greeting + ' ' + this.user + punctuation; } }; const freddyBound = bindKey(freddy, 'greet'); console.log(freddyBound('hi', '!')); // 'hi fred!'
做用:把上列中的fn
換成了context[fn]
。
解析:咱們原來的 參數函數 變成了一個 上下文參數的一個屬性,而將這個屬性依附於上下文對象就成了一個函數context[fn]
。
能夠說,這個一個調用方式特殊的bind
。
const call = (key, ...args) => context => context[key](...args); Promise.resolve([1, 2, 3]) .then(call('map', x => 2 * x)) .then(console.log); // [ 2, 4, 6 ] const map = call.bind(null, 'map'); Promise.resolve([1, 2, 3]) .then(map(x => 2 * x)) .then(console.log); // [ 2, 4, 6 ]
做用:動態改變函數執行的上下文。
解析:給定一個屬性參數,再給定一組調用參數,返回一個接受上下文對象的函數,並最終組合調用。
其實這裏面暗含了一個約束,很顯然,context[key]
必須是一個函數。
這個片斷本質是對上下文的抽象。舉個例子:
const filterMen = call('filter', person => person.sex === 'man') filterMen([{sex:'woman',...},{sex:'man',...},...]) // 若是有其餘 上下文對象,本例中也就是數組 須要相同的 邏輯過濾呢?
const chainAsync = fns => { let curr = 0; const last = fns[fns.length - 1]; const next = () => { const fn = fns[curr++]; fn === last ? fn() : fn(next); }; next(); }; chainAsync([ next => { console.log('0 seconds'); setTimeout(next, 1000); }, next => { console.log('1 second'); setTimeout(next, 1000); }, () => { console.log('2 second'); } ]);
做用:將 函數數組轉換爲有決策權的鏈式函數調用。
我爲何稱之有決策權的鏈式函數調用呢?
由於每一個函數都會接受一個next方法參數,它表明的就是調用鏈中的下一個函數,因此何時調用下一個函數,要不要調用,決策權在你。
解析:其實這個片斷很簡單。
首先,fns
類型一個函數數組,其中除了最後一個函數都有隱含的約束,能夠選擇接受 next 參數。
而 next 參數的含義就是調用鏈中的下一個函數,說白了 就是數組中的下一個成員。
而最後一個函數是無參函數。
片斷中複雜點在於:利用閉包存儲了兩個關鍵變量。
第一個是 調用鏈中的函數遊標:curr
;第二個是結束標誌,最後一個函數:last
。
每次鏈式向下調用前,都會進行一些邏輯處理:
const next = () => { const fn = fns[curr++]; fn === last ? fn() : fn(next); };
先取出當前遊標所在函數,再把遊標指向下一個函數。
而後,判斷是不是最後一個函數,是則直接調用,結束;反之,傳入 next 調用。
若是,你是一個後端開發者,能夠把其理解爲中間件的工做模式。
const collectInto = fn => (...args) => fn(args); const Pall = collectInto(Promise.all.bind(Promise)); let p1 = Promise.resolve(1); let p2 = Promise.resolve(2); let p3 = new Promise(resolve => setTimeout(resolve, 2000, 3)); Pall(p1, p2, p3).then(console.log); // [1, 2, 3] (after about 2 seconds)
做用:將接受數組的函數更改成接受可變參數。
分析:利用了擴展運算符的性質,...args
表明的是全部參數組成的數組,而後將這數組傳遞進去調用。
可別小看了這一片斷,調用方式的改變會決定不少上層邏輯。
日常咱們大機率都會,創建一個數組,收集所需的異步函數。
在本例中,很明顯的看到 從參數爲數組類型的約束 中解放了出來。
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))); const substract3 = x => x - 3; const add5 = x => x + 5; const multiply = (x, y) => x * y; const multiplyAndAdd5AndSubstract3 = compose( substract3, add5, multiply ); multiplyAndAdd5AndSubstract3(5, 2); // 12
做用:將傳入的多個[異步]函數以組合的方式 調用。
先將參數傳入最後一個[異步]函數,而後將獲得的結果,傳入倒數第二個[異步]函數,以此類推。
compose
能夠說是函數式編程的經典片斷。
它的具體意義能夠說是邏輯分層。像洋蔥同樣,一層一層地處理數據。
解析:fns 表明的是 傳入的多個函數 組成的數組。
利用reduce
方法實現函數的「洋蔥」包裹。
由於這種邏輯語義表示效果很差,就直接上上面例子的代碼流程了。
reduce 第一次循環: f: substract3; g: add5; 返回結果:(...args) => substract3(add5(...args)); reduce 第二次循環: f: (...args) => substract3(add5(...args)); g: multiply; 返回結果: (...args1) => ((...args2) => substract3(add5(...args2)))(multiply(...args1)) 優化後: (...args) => substract3(add5(multiply(...args))); 循環下去,以此類推... 最後的返回的形式: (...args) => 第一個函數(第二個函數(第三個函數(...最後一個函數(...args))))
PS: 說實話,我並不喜歡 compose,在上例中就能夠很明顯的看到缺點。
把不少函數組合起來,第一是缺乏語義化,與之對應的例子就是 Promise 的 then 調用鏈,語義鮮明;
第二是沒法添加函數與函數之間的抽象邏輯,只能一次寫好。
第三是各個函數之間存在隱含的參數約束,很可怕的。
const composeRight = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args))); const add = (x, y) => x + y; const square = x => x * x; const substract3 = x => x - 3; const addAndSquare = composeRight(add, square,substract3); addAndSquareAndSubstract3(1, 2); // 6
做用:將傳入的多個[異步]函數以組合的方式 調用。
先將參數傳入第一個[異步]函數,而後將獲得的結果,傳入第二個[異步]函數,以此類推。
const converge = (converger, fns) => (...args) => converger(...fns.map(fn => fn.apply(null, args))); const average = converge((a, b) => a / b, [ arr => arr.reduce((a, v) => a + v, 0), arr => arr.length ]); average([1, 2, 3, 4, 5, 6, 7]); // 4
做用:將 函數數組的返回結果 傳遞到converger
函數,進一步處理,可用做分析統計。
解析: 使用map
和apply
將參數數據傳遞給每一個處理函數,並將處理後的結果交給converger
函數。
const curry = (fn, arity = fn.length, ...args) => arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args); curry(Math.pow)(2)(10); // 1024 curry(Math.min, 3)(10)(50)(2); // 2
做用:函數柯里化。
柯里化無論在是函數式思惟的理解,仍是現實面試中,都很是的重要。
解析:這個bind
用得真是神了,藉助它積累每次傳進來的參數,等到參數足夠時,再調用。
const debounce = (fn, ms = 0) => { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), ms); }; }; window.addEventListener( 'resize', debounce(() => { console.log(window.innerWidth); console.log(window.innerHeight); }, 250) ); // Will log the window dimensions at most every 250ms
做用:函數防抖。
什麼是防抖和節流?有什麼區別?如何實現? 一文中關於防抖解釋:
觸發高頻事件後n秒內函數只會執行一次,若是n秒內高頻事件再次被觸發,則從新計算時間。
一樣,防抖也是面試必考的點。
解析: 傳入需防抖的函數,和防抖的時間間隔,返回一個已防抖化的函數。
主要藉助setTimeout
和function + apply
保存上下文完成。
每次調用函數前,都執行一遍clearTimeout
,保證從新計算調用時間。
不管是調用多麼頻繁的函數都會在指定時間的間隔後只運行一次。
const defer = (fn, ...args) => setTimeout(fn, 1, ...args); // Example A: defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a' // Example B: document.querySelector('#someElement').innerHTML = 'Hello'; longRunningFunction(); // Browser will not update the HTML until this has finished defer(longRunningFunction); // Browser will update the HTML then run the function
做用:推遲調用函數,直到清除當前調用堆棧。
可適用於推遲 cpu 密集型計算,以避免阻塞渲染引擎工做。
分析:使用setTimeout
(超時時間爲1ms)將 函數參數 添加到瀏覽器事件隊列末尾。
由於 JavaScript 是單線程執行,先是主線程執行完畢,而後在讀取事件隊列中的代碼執行。
若是主線程有運行時間太長的函數,會阻塞頁面渲染,因此將其放置到事件隊列。
const delay = (fn, wait, ...args) => setTimeout(fn, wait, ...args); delay( function(text) { console.log(text); }, 1000, 'later' ); // Logs 'later' after one second.
做用:延遲函數執行。
是的,它和defer
很是像,但使用場景倒是不同。
defer 的目的是將佔據主線程時間長的函數推遲到事件隊列。
而 delay 只是字面意思,延遲執行。
解析:對 setTimeout
進行語義化封裝。
const flip = fn => (first, ...rest) => fn(...rest, first); let a = { name: 'John Smith' }; let b = {}; const mergeFrom = flip(Object.assign); let mergePerson = mergeFrom.bind(null, a); mergePerson(b); // == b b = {}; Object.assign(b, a); // == b
做用:對 參數函數 的輸入數據進行進一步處理,將數據的第一個參數與其他參數位置對調。
解析:主要利用 擴展運算符的性質,對參數的位置進行調整。
若是你不瞭解這一語言特性,可參考阮一峯老師的ES6入門。
const hz = (fn, iterations = 100) => { const before = performance.now(); for (let i = 0; i < iterations; i++) fn(); return (1000 * iterations) / (performance.now() - before); }; // 10,000 element array const numbers = Array(10000) .fill() .map((_, i) => i); // Test functions with the same goal: sum up the elements in the array const sumReduce = () => numbers.reduce((acc, n) => acc + n, 0); const sumForLoop = () => { let sum = 0; for (let i = 0; i < numbers.length; i++) sum += numbers[i]; return sum; }; // `sumForLoop` is nearly 10 times faster Math.round(hz(sumReduce)); // 572 Math.round(hz(sumForLoop)); // 4784
做用:返回函數每秒執行一次的次數。
hz是赫茲的單位(頻率的單位)定義爲每秒一個週期。
解析:經過兩次使用performance.now
獲取iterations
次迭代先後的毫秒差。
而後將毫秒轉換爲秒併除以通過的時間,能夠獲得每秒的函數執行次數。
PS: 此處,並無太好的我的理解,翻譯自官方。
const once = fn => { let called = false; return function(...args) { if (called) return; called = true; return fn.apply(this, args); }; }; const startApp = function(event) { console.log(this, event); // document.body, MouseEvent }; document.body.addEventListener('click', once(startApp)); // only runs `startApp` once upon click
做用:確保一個函數只被調用一次。
分析:由於 JavaScript 是單線程執行環境,不須要考慮併發環境,直接一個內部變量存到閉包中,每次調用前判斷,並在第一次調用時,修改其值,讓後續調用所有失效。
給你看一下 Go 的 once,官方是經過atomic
庫實現的:
package sync import ( "sync/atomic" ) type Once struct { m Mutex done uint32 } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 1 { return } o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
const over = (...fns) => (...args) => fns.map(fn => fn.apply(null, args)); const minMax = over(Math.min, Math.max); minMax(1, 2, 3, 4, 5); // [1,5]
做用:利用函數數組,對接下來的輸入數據進行處理,獲得每一個函數處理後的結果數組。
解析:使用map
和apply
將輸入的數據傳遞到每一個函數中進行處理。
const overArgs = (fn, transforms) => (...args) => fn(...args.map((val, i) => transforms[i](val))); const square = n => n * n; const double = n => n * 2; const fn = overArgs((x, y) => [x, y], [square, double]); fn(9, 3); // [81, 6]
做用:利用 transforms 函數數組,分別處理相應位置的輸入數據,並把結果傳遞進給定函數。
解析:transforms 函數數組 和參數必須位置對應,這個約束有點強啊。
const partial = (fn, ...partials) => (...args) => fn(...partials, ...args); const greet = (greeting, name) => greeting + ' ' + name + '!'; const greetHello = partial(greet, 'Hello'); greetHello('John'); // 'Hello John!'
做用:將調用函數的數據分爲兩次輸入,並按正序調用。
解析:兩次使用擴展運算符(...),保存不一樣時期的數據,最後調用。
const partialRight = (fn, ...partials) => (...args) => fn(...args, ...partials); const greet = (greeting, name) => greeting + ' ' + name + '!'; const greetJohn = partialRight(greet, 'John'); greetJohn('Hello'); // 'Hello John!'
做用:將調用函數的數據分爲兩次輸入,並按反序調用。
解析:兩次使用擴展運算符(...),保存不一樣時期的數據,最後調用。
const pipeAsyncFunctions = (...fns) => arg => fns.reduce((p, f) => p.then(f), Promise.resolve(arg)); const sum = pipeAsyncFunctions( x => x + 1, x => new Promise(resolve => setTimeout(() => resolve(x + 2), 1000)), x => x + 3, async x => (await x) + 4 ); (async () => { console.log(await sum(5)); // 15 (after one second) })();
做用:將傳入的多個[異步]函數按照正序 依次調用。
解析:結合reduce
和Promise.then
,將數據按照正序傳遞到每一個[異步]函數,進行處理,處理的結果又傳給下一個[異步]函數,以此類推。
const promisify = func => (...args) => new Promise((resolve, reject) => func(...args, (err, result) => (err ? reject(err) : resolve(result))) ); const delay = promisify((d, cb) => setTimeout(cb, d)); delay(2000).then(() => console.log('Hi!')); // // Promise resolves after 2s
做用:將回調函數改成Promise
方式處理結果。
在 Node8+ ,你可使用util.promisify
解析:首先接受給定的回調函數,而後直接在 Promise 中調用該函數。
由於回調函數的結果按照規範永遠是最後一個參數,咱們只須要在函數調用時,把最後一個參數換成 Promise 的方式,即:若是回調函數出現錯誤則 reject,反之 resolve。
注意:被 promisify 的函數必須接受回調參數且後續會調用。
const rearg = (fn, indexes) => (...args) => fn(...indexes.map(i => args[i])); var rearged = rearg( function(a, b, c) { return [a, b, c]; }, [2, 0, 1] ); rearged('b', 'c', 'a'); // ['a', 'b', 'c']
做用:根據指定的索引從新排列傳入的參數。
解析:利用map
結合擴展運算符,從新排列傳入的參數,並將轉換後的參數傳遞給fn。
const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve()); const delay = d => new Promise(r => setTimeout(r, d)); runPromisesInSeries([() => delay(1000), () => delay(2000)]); // Executes each promise sequentially, taking a total of 3 seconds to complete
做用:按照正序 運行給定的多個返回類型爲 Promise 函數。
解析:使用reduce
建立一個Promise鏈,每次運行完一個傳入的 Promise,都會返回最外部的Promise.then
,從而進行下一次調用。
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); async function sleepyWork() { console.log("I'm going to sleep for 1 second."); await sleep(1000); console.log('I woke up after 1 second.'); }
做用: 延遲異步函數的執行。
解析:建立一個接受毫秒數的函數,並結合setTimeout
,在給定的毫秒數後,返回一個resolve
狀態的Promise。
使用場景:利用異步函數的「同步」機制(await),使其在異步函數中達到「睡眠」的效果。
const spreadOver = fn => argsArr => fn(...argsArr); const arrayMax = spreadOver(Math.max); arrayMax([1, 2, 3]); // 3
做用:將接受可變參數的函數更改成接受數組。
若是你認真讀了文章,就會發現這是collectInto
函數的反模式。
分析:利用了擴展運算符的性質,將傳遞進來的數組解構再交給處理函數。
const times = (n, fn, context = undefined) => { let i = 0; while (fn.call(context, i) !== false && ++i < n) {} }; var output = ''; times(5, i => (output += i)); console.log(output); // 01234
做用:將給定的函數,迭代執行n次。
分析:使用Function.call
迭代調用給定的函數,並把迭代的次數傳進函數第一個參數。
若是函數返回 false 可提早退出。
const uncurry = (fn, n = 1) => (...args) => { const next = acc => args => args.reduce((x, y) => x(y), acc); if (n > args.length) throw new RangeError('Arguments too few!'); return next(fn)(args.slice(0, n)); }; const add = x => y => z => x + y + z; const uncurriedAdd = uncurry(add, 3); uncurriedAdd(1, 2, 3); // 6
做用:函數反柯里化。
柯里化是將接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數。
而反柯里化就是將多個接受參數的層層函數,鋪平。
解析:反柯里化的關鍵代碼在於 args.reduce((x, y) => x(y), acc)
。
在上例中, args: [1,2,3] acc: x => y => z => x + y + z 第一次循環: x:x => y => z => x + y + z y:1 返回結果:y => z => 1 + y + z 第二次循環: x: y => z => 1 + y + z y: 2 返回結果:z => 1 + 2 + z 最後一次循環的結果,即 1 + 2 +3
能夠看出,每次一循環,都會利用閉包」填充」一個所需變量。
返回的結果分爲兩種狀況:
一是 一個保留了 n 個前置參數的函數。
二是層疊函數中最後一個函數的返回結果。
值得一提的是,在源碼中使用了slice(0,n)
保留適當數量的參數。
若是提供的參數的個數小於給定的解析長度,就會拋出錯誤。
const unfold = (fn, seed) => { let result = [], val = [null, seed]; while ((val = fn(val[1]))) result.push(val[0]); return result; }; var f = n => (n > 50 ? false : [-n, n + 10]); unfold(f, 10); // [-10, -20, -30, -40, -50]
做用:使用種子值以及特殊的數據存儲與迭代方式構建一個數組。
解析: 我爲何說數據存儲與迭代方式很特殊呢?
迭代的變量與結果值,保存在同一數組裏,用01下標區分。
而迭代的函數,也須要知足這一規範,返回一樣的數組[value,nextSeed],保證下一次迭代,或者返回false終止過程。
const when = (pred, whenTrue) => x => (pred(x) ? whenTrue(x) : x); const doubleEvenNumbers = when(x => x % 2 === 0, x => x * 2); doubleEvenNumbers(2); // 4 doubleEvenNumbers(1); // 1
做用:根據pred
函數測試給定數據。如結果爲真,則執行whenTrue
函數;反之,返回數據。
解析: 我喜歡語義化的封裝,可大幅提高代碼的可讀性,減小邏輯負擔。
const memoize = fn => { const cache = new Map(); const cached = function(val) { return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val); }; cached.cache = cache; return cached; }; // See the `anagrams` snippet. const anagramsCached = memoize(anagrams); anagramsCached('javascript'); // takes a long time anagramsCached('javascript'); // returns virtually instantly since it's now cached console.log(anagramsCached.cache); // The cached anagrams map
做用:爲給定的函數添加緩存功能。
解析: 經過實例化一個新的Map
對象來建立一個空的緩存。
並對函數的調用進一步的封裝,若是調用時,傳入了一個以前已經傳遞過的參數,將從緩存中直接返回結果,執行時間爲O(1);若是是首次傳遞,則需運行函數,將獲得結果緩存,並返回。
其實,咱們還能夠藉助這個片斷,看到一絲 JavaScript 語法的殘缺。
到目前爲止,一個社區公認的私有屬性語法都沒有,TC39 一直提議用#
號,並闡述了不少緣由、聲明。
哎,說白了,就是 JavaScript 從一開始設計的失誤,到如今已經沒法挽回了。
const throttle = (fn, wait) => { let inThrottle, lastFn, lastTime; return function() { const context = this, args = arguments; if (!inThrottle) { fn.apply(context, args); lastTime = Date.now(); inThrottle = true; } else { clearTimeout(lastFn); lastFn = setTimeout(function() { if (Date.now() - lastTime >= wait) { fn.apply(context, args); lastTime = Date.now(); } }, Math.max(wait - (Date.now() - lastTime), 0)); } }; }; window.addEventListener( 'resize', throttle(function(evt) { console.log(window.innerWidth); console.log(window.innerHeight); }, 250) ); // Will log the window dimensions at most every 250ms
做用: 函數節流。
什麼是防抖和節流?有什麼區別?如何實現? 一文中關於防抖解釋:
高頻事件觸發,但在 n 秒內只會執行一次,因此節流會稀釋函數的執行頻率。
一樣,節流也是面試必考的點。
解析:第一次執行時,當即執行給定函數,保存當前的時間,並設置標記變量。
標記變量主要用於判斷是否第一次調用,若是是第一次則馬上運行。
反之不是第一次運行,過了等待的毫秒後纔可繼續運行。
主要邏輯是每次運行前先清除上一個的定時器,而後計算出上一次運行的時間與給定的運行間隔所差的毫秒數,並利用其數據新建一個定時器運行。
定時器裏的函數除了調用給定函數,還會更新上一次運行的時間變量。
節流的實現,網上的文章有不少版本,但多少都有點瑕疵。
呼,花了很長的時間,終於搞定了這篇文章。
之後的 30s 源碼刨析系列會挑選一些源碼片斷去解析,而不是針對某一分類了。
本篇文章涉及了個人一些思考,但願能對你有幫助。
轉載文章請註明做者和出處 一個壞掉的番茄,請勿用於任何商業用途。