摘要:最后畫幾張粗糙的圖,簡單描述一下這個執(zhí)行的過程因為是鏈式調用,所以在鏈上的都會入棧然后執(zhí)行,額,執(zhí)行棧少畫了和。。。
前言:昨天在群里討(jin)論(chui)技(niu)術(pi),有位老鐵發(fā)了一道他面的某公司面試題,順手保存了。今早花了一點時間把這題做了出來,發(fā)現(xiàn)挺有意思的,決定在今天認真工(hua)作(shui)前,與大家分享我的解題方案和思考過程。
題目如下(可以自己先思考一會,沒準可以想出比我更好的方法):
小眼一撇,這幾個需求都是要實現(xiàn)鏈式調用,而鏈式調用最常見的是 jQuery,還有就是我們非常熟悉的 Promise。
jQuery中鏈式調用的原理是在函數(shù)的末尾return this(即返回這個對象自身),使得對象可以繼續(xù)調用自身的函數(shù)從而達到支持鏈式調用。
知道了這個套路之后,接下來我們可以按照這個套路飛快的先寫出符合第一個小需求的函數(shù)。
const LazyMan = function (name) { console.log(`Hi i am ${name}`); } LazyMan("Tony") // Hi i am Tony
雖然只有短短三行代碼,但沒有報一點錯,而且運行起來飛快的,完美的實現(xiàn)了第一個小需求。
某路人:“等等,,不就是一個簡單的函數(shù),套路用在哪呢?”
嘖嘖,被你發(fā)現(xiàn)了,小伙子不錯嘛,好,現(xiàn)在就用鏈式調把第二個小需求實現(xiàn)了:
const LazyMan = function (name) { console.log(`Hi i am ${name}`); class F { sleep(timeout) { setTimeout(function () { console.log(`等待了${timeout}秒`); return this; }, timeout) }; eat(food) { console.log(`I am eating ${food}`); return this; } } return new F(); } LazyMan("Tony").sleep(10).eat("lunch")
丟瀏覽器里面跑一下,一段紅條條蹦了出來
Uncaught TypeError: Cannot read property "eat" of undefined
納尼,eat為什么會在undefined上調用,我不是在sleep中返回了this么???是不是 Chrome 又偷偷更新,加了一個新 bug,,,
不過 google 工程師應該沒有這么不靠譜吧。難道是我寫錯了?
掃一遍代碼,發(fā)現(xiàn)return this是在setTimeout中的處理函數(shù)返回的,而不是sleep返回的,小改一下。
// ... sleep(timeout) { setTimeout(function () { console.log(`等待了${timeout}秒....`); }, timeout) return this; }; // ...
再跑一下,沒有紅條條了,嘿。
但仔細一看,跟需求中的順序不一致,我們現(xiàn)在的輸出是這樣的:
Hi i am Tony I am eating lunch 等待了10秒
emmmmm,看來,現(xiàn)在得拿出一點 JavaScript 硬本事了。
JavaScript 中有同步任務和異步任務,同步任務就是按照我們編寫順序推入執(zhí)行棧,一步一步執(zhí)行;而setTimeout屬于異步任務,在瀏覽器中是由定時觸發(fā)器線程負責,這個線程會進行計時,當計時完成后將這個事件的handler推入到任務隊列中,任務隊列中的任務需要等待執(zhí)行棧中為空時把隊列中的任務丟入執(zhí)行棧中進行執(zhí)行(從這里也可以知道handler并不能準時執(zhí)行)。
(隨手畫了一張草圖,有點丑,不過應該不影響我想要表達的意思)
如果不太了解,可以參考這篇文章 這一次,徹底弄懂 JavaScript 執(zhí)行機制 ,寫的非常易懂了
知道了這個知識后,然并卵,它不能幫我們寫出所需要的代碼。。。
在空氣安靜了數(shù)十分鐘后,我還是毫無頭緒,只好拿起杯子,準備起身去倒杯水壓壓驚,突然猶有一道閃電擊到了我一般,腦海中浮現(xiàn)了 vue 中實現(xiàn)nextTick這一方法實現(xiàn)的代碼,代碼雖模糊不清(我根本記不清楚了),但我造這應該可以幫助我解決點什么問題。so,我放下杯子,熟練的打開某 hub,在里面找到了nextTick的實現(xiàn)代碼(在這里next-tick.js)。
快速從第一行到最后一行掃了一遍,可以獲取到的東東是:它用一個callbacks數(shù)組存儲需要執(zhí)行的函數(shù),然后利用micro task和macro task的優(yōu)先級特性,從而可以在 DOM 渲染之前執(zhí)行callbacks中的回調。emmmmm,跟我現(xiàn)在的需求好像扯不上什么關系,并不能給什么幫助。不過我也可以把需要執(zhí)行的函數(shù)加入一個數(shù)組中,在最后執(zhí)行它。說干就干,可以快速寫出如下代碼:
const LazyMan = function (name) { console.log(`Hi i am ${name}`); function _eat(food){ console.log(`I am eating ${food}`); } const callbacks = []; class F { sleep(timeout) { setTimeout(function () { console.log(`等待了${timeout}秒....`); callbacks.forEach(cb=>cb()) }, timeout); return this; }; eat(food) { callbacks.push(_eat.bind(null,food)); return this; } } return new F(); }; LazyMan("Tony").sleep(10).eat("lunch") // Hi i am Tony // 等待了10秒.... // I am eating lunch
執(zhí)行完,輸出跟需求一模一樣,嘿嘿嘿。
接著按照第三個小需求執(zhí)行一下,結果如下:
//... LazyMan("Tony").eat("lunch").sleep(10).eat("dinner") // Hi i am Tony // 等待了10秒 // I am eating lunch // I am eating dinner //...
沒有報錯,很好,但順序又錯了。。。這可不好辦。
眼看著空氣又要安靜下來了,我不能干耗著,決定使用一些常用套路了,比如加個flag,區(qū)分是否是需要在 sleep 之后執(zhí)行的方法,改寫后如下:
const LazyMan = function (name) { console.log(`Hi i am ${name}`); function _eat(food) { console.log(`I am eating ${food}`); } const callbacks = []; let isNeedSleep = false; class F { sleep(timeout) { setTimeout(function () { console.log(`等待了${timeout}秒`); callbacks.forEach(cb => cb()) }, timeout); isNeedSleep = true; return this; }; eat(food) { if (isNeedSleep) { callbacks.push(_eat.bind(null, food)); } else { _eat.call(null, food); } return this; } } return new F(); };
跑一下,跟第三個小需求輸出一模一樣,嘿嘿嘿,小菜一碟。
到最后這個小需求中,鏈式調用中多了一個sleepFirst,其效果是會將sleep提至鏈式調用的最前端來執(zhí)行,也就是說sleepFirst的優(yōu)先級最高。
容我思考一下: 能夠根據(jù)優(yōu)先級來操作的數(shù)據(jù)結構,在我所知的范圍內只有優(yōu)先隊列,而優(yōu)先隊列可以用數(shù)組來實現(xiàn),so,是不是說可以用數(shù)組來實現(xiàn)優(yōu)先級callbacks的調用,即用嵌套數(shù)組。答曰:你想的沒有錯啦。
擼起袖子繼續(xù)干,于是數(shù)分鐘后有了下面這個函數(shù)
const LazyMan = function (name) { console.log(`Hi i am ${name}`); function _eat(food) { console.log(`I am eating ${food}`); } const callbackQueue = []; let index = 0; class F { sleep(timeout) { const _callbacks = callbackQueue.shift(); _callbacks && _callbacks.forEach(cb => cb()); setTimeout(function () { console.log(`等待了${timeout}秒....`); const _callbacks = callbackQueue.shift(); _callbacks && _callbacks.forEach(cb => cb()) }, timeout); index ++; return this; }; eat(food) { if(!callbackQueue[index]) callbackQueue[index] = []; callbackQueue[index].push(_eat.bind(null, food)); return this; }; sleepFirst(timeout){ setTimeout(function () { console.log(`等待了${timeout}秒....`); const _callbacks = callbackQueue.shift(); _callbacks && _callbacks.forEach(cb => cb()) }, timeout); index ++; return this; } } return new F(); };
我的想法是 每經過一次sleep后,index會+1,表示有新的一組callback,當執(zhí)行eat時,判斷是否存在當前index對應的數(shù)組,不存在則創(chuàng)建一個對應的空數(shù)組,然后把對應需要調用的函數(shù)添加入這個數(shù)組中,最后把這個數(shù)組存到callbackQueue中,當添加完成后,會按照順序一步一步從callbackQueue中取出并執(zhí)行。
雖然我思路這思路應該是對的,但我還是隱隱約約感覺到了里面蘊含的紅條條,先丟瀏覽器中跑一下試試。
結果如下:
Hi i am Tony I am eating lunch I am eating dinner 等待了5秒.... 等待了10秒....
果然,沒有按照所需的順序執(zhí)行,因為這里還是沒有能夠處理sleepFirst優(yōu)先級的這個根本問題。。。
等等。。。我剛剛說了啥,"優(yōu)先級",咱們往上翻,我前面好像提到過這個詞!
沒錯,vue中的nextTick中就用到了,我們可以參考它,利用Event Loop中micro task和macro task執(zhí)行的優(yōu)先級來解決這個問題。
const LazyMan = function (name) { console.log(`Hi i am ${name}`); function _eat(food) { console.log(`I am eating ${food}`); } const callbackQueue = []; let index = 0; class F { sleep(timeout) { setTimeout(() => { const _callbacks = callbackQueue.shift(); _callbacks && _callbacks.forEach(cb => cb()); setTimeout(function () { console.log(`等待了${timeout}秒....`); const _callbacks = callbackQueue.shift(); _callbacks && _callbacks.forEach(cb => cb()) }, timeout); }) index++; return this; }; eat(food) { if (!callbackQueue[index]) callbackQueue[index] = []; callbackQueue[index].push(_eat.bind(null, food)); return this; }; sleepFirst(timeout) { Promise.resolve().then(() => { const _callbacks = callbackQueue.shift(); setTimeout(function () { console.log(`等待了${timeout}秒....`); _callbacks && _callbacks.forEach(cb => cb()) }, timeout); }) index++; return this; } } return new F(); };
丟瀏覽器執(zhí)行一下,完全冇問題,丟 node 中也一樣,歐耶,完美。
最后畫幾張粗糙的圖,簡單描述一下這個執(zhí)行的過程:
因為是鏈式調用,所以在鏈上的都會入棧然后執(zhí)行,額,執(zhí)行棧少畫了 sleep 和 sleepFirst。。。
Hi i am Tony
其中 setTimeout 的 handler 為宏任務,加入marco task隊列中;Promise.resolve().then的回調為微任務,加入micro task隊列中
然后執(zhí)行棧被清空,micro task中未清空的任務加入執(zhí)行棧中被執(zhí)行,
因為其中有一個 setTimeout,所以把其 handler 加入macro task中
前面的微任務執(zhí)行完就出棧了,這時候macro task中第一個任務入執(zhí)行棧中進行執(zhí)行
這個時候如果有 callbacks 就會執(zhí)行
因為函數(shù)內部又有一個 setTimeout,于是把它的 handler 加入macro task中
然后清空執(zhí)行棧,繼續(xù)執(zhí)行下一個宏任務
等待了5秒.... I am eating lunch I am eating dinner
執(zhí)行棧為空,把最后一個宏任務丟進棧中執(zhí)行
等待了10秒.... I am eating junk food
最后總結一下,這道題的難點是能否想到用event loop來解決,如果能往這方向去想了,做起來就很簡單了。
還有平時不怎么動筆的(比如我),一開始寫起文章來就會如鯁在喉,許多內容都寫漏了。所以平時有時間就要多動動筆,寫寫文章,但也不是說東拼西湊一篇,而是真的要有自己的思考和感悟。
最最最后給各位看官老爺多添加一個小需求練練手:
LazyMan("Tony").eat("lunch").eat("dinner").sleepFirst(5).sleep(10).eat("junk food").eat("healthy food") // Hi i am Tony // 等待了5秒 // I am eating lunch // I am eating dinner // 等待了10秒 // I am eating junk food // I am eating healthy food
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/101460.html
摘要:想必面試題刷的多的同學對下面這道題目不陌生,能夠立即回答出輸出個,可是你真的懂為什么嗎為什么是輸出為什么是輸出個這兩個問題在我腦邊縈繞。同步任務都好理解,一個執(zhí)行完執(zhí)行下一個。本文只是我對這道面試題的一點思考,有誤的地方望批評指正。 想必面試題刷的多的同學對下面這道題目不陌生,能夠立即回答出輸出10個10,可是你真的懂為什么嗎?為什么是輸出10?為什么是輸出10個10?這兩個問題在我腦...
摘要:一篇文章和一道面試題最近,有篇名為張圖幫你一步步看清和的執(zhí)行順序的文章引起了我的關注。作者用一道年今日頭條的前端面試題為引子,分步講解了最終結果的執(zhí)行原因。從字面意思理解,讓我們等等。當前的最新版本,在這里的執(zhí)行順序上,的確存在有問題。 一篇文章和一道面試題 最近,有篇名為 《8張圖幫你一步步看清 async/await 和 promise 的執(zhí)行順序》 的文章引起了我的關注。 作者用...
摘要:然后最外層這個函數(shù)會返回一個新對象,對象里面有一個屬性,名為,而這個屬性的值是一個匿名函數(shù),它會返回。 最近看到一條有意思的閉包面試題,但是看到原文的解析,我自己覺得有點迷糊,所以自己重新做一下這條題目。 閉包面試題原題 function fun(n, o) { // ① console.log(o); return { // ② fun: function(m) ...
摘要:對于這種會退出的情況,數(shù)組顯然不能像鏈表一樣直接斷開,因此采用標記法先生成一個長度為的布爾型數(shù)組,用填充。中對整個進行遍歷才能得到此時數(shù)組中的數(shù)量。 文中的速度測試部分,時間是通過簡單的 System.currentTimeMillis() 計算得到的, 又由于 Java 的特性,每次測試的結果都不一定相同, 對于低數(shù)量級的情況有 ± 20 的浮動,對于高數(shù)量級的情況有的能有 ± 10...
摘要:中的算法附道面試常見算法題解決方法和思路關注每日一道面試題詳解面試過程通常從最初的電話面試開始,然后是現(xiàn)場面試,檢查編程技能和文化契合度。值得記住的數(shù)組方法有和。一個好的解決方案是使用內置的方法。 JavaScript中的算法(附10道面試常見算法題解決方法和思路) 關注github每日一道面試題詳解 Introduction 面試過程通常從最初的電話面試開始,然后是現(xiàn)場面試,檢查編程...
閱讀 2051·2023-04-25 15:11
閱讀 3516·2021-09-23 11:57
閱讀 1387·2021-07-26 23:38
閱讀 1327·2019-08-30 15:54
閱讀 645·2019-08-30 15:53
閱讀 3256·2019-08-26 13:36
閱讀 997·2019-08-26 12:01
閱讀 2873·2019-08-23 16:21