前言:昨天在羣裏討(jin)論(chui)技(niu)術(pi),有位老鐵發了一道他面的某公司面試題,順手保存了。今早花了一點時間把這題做了出來,發現挺有意思的,決定在今天認真工(hua)作(shui)前,與大家分享我的解題方案和思考過程。
題目如下(可以自己先思考一會,沒準可以想出比我更好的方法):
小眼一撇,這幾個需求都是要實現鏈式調用,而鏈式調用最常見的是 jQuery,還有就是我們非常熟悉的 Promise。
jQuery中鏈式調用的原理是在函數的末尾return this
(即返回這個對象自身),使得對象可以繼續調用自身的函數從而達到支持鏈式調用。
知道了這個套路之後,接下來我們可以按照這個套路飛快的先寫出符合第一個小需求的函數。
const LazyMan = function (name) {
console.log(`Hi i am ${name}`);
}
LazyMan('Tony')
// Hi i am Tony
雖然只有短短三行代碼,但沒有報一點錯,而且運行起來飛快的,完美的實現了第一個小需求。
路人甲:“等等,,不就是一個簡單的函數,套路在哪”
嘖嘖,被你發現了,小夥子不錯嘛,好,現在就用鏈式調把第二個小需求實現了:
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 工程師應該沒有這麼不靠譜吧。難道是我寫錯了?
掃一遍代碼,發現return this
是在setTimeout
中的處理函數返回的,而不是sleep
返回的,小改一下。
// ...
sleep(timeout) {
setTimeout(function () {
console.log(`等待了${timeout}秒....`);
}, timeout)
return this;
};
// ...
再跑一下,沒有紅條條了,嘿。
但仔細一看,跟需求中的順序不一致,我們現在的輸出是這樣的:
Hi i am Tony
I am eating lunch
等待了10秒
emmmmm,看來,現在得拿出一點 JavaScript 硬本事了。
JavaScript 中有同步任務和異步任務,同步任務就是按照我們編寫順序推入執行棧,一步一步執行;而setTimeout
屬於異步任務,在瀏覽器中是由定時觸發器線程負責,這個線程會進行計時,當計時完成後將這個事件的handler
推入到任務隊列中,任務隊列中的任務需要等待執行棧中爲空時把隊列中的任務丟入執行棧中進行執行(從這裏也可以知道handler
並不能準時執行)。
(隨手畫了一張草圖,有點醜,不過應該不影響我想要表達的意思)
如果不太瞭解,可以參考這篇文章 這一次,徹底弄懂 JavaScript 執行機制 ,寫的非常易懂了
知道了這個知識後,然並卵,它不能幫我們寫出所需要的代碼。。。
在空氣安靜了數十分鐘後,我還是毫無頭緒,只好拿起杯子,準備起身去倒杯水壓壓驚,突然猶有一道閃電擊到了我一般,腦海中浮現了 vue 中實現nextTick
這一方法實現的代碼,代碼雖模糊不清(我根本記不清楚了),但我造這應該可以幫助我解決點什麼問題。so,我放下杯子,熟練的打開某 hub,在裏面找到了nextTick
的實現代碼(在這裏next-tick.js)。
快速從第一行到最後一行掃了一遍,可以獲取到的東東是:它用一個callbacks
數組存儲需要執行的函數,然後利用micro task
和macro task
的優先級特性,從而可以在 DOM 渲染之前執行callbacks
中的回調。emmmmm,跟我現在的需求好像扯不上什麼關係,並不能給什麼幫助。不過我也可以把需要執行的函數加入一個數組中,在最後執行它。說幹就幹,可以快速寫出如下代碼:
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
執行完,輸出跟需求一模一樣,嘿嘿嘿。
接着按照第三個小需求執行一下,結果如下:
//...
LazyMan('Tony').eat('lunch').sleep(10).eat('dinner')
// Hi i am Tony
// 等待了10秒
// I am eating lunch
// I am eating dinner
//...
沒有報錯,很好,但順序又錯了。。。這可不好辦。
眼看着空氣又要安靜下來了,我不能乾耗着,決定使用一些常用套路了,比如加個flag
,區分是否是需要在 sleep
之後執行的方法,改寫後如下:
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
提至鏈式調用的最前端來執行,也就是說sleepFirst
的優先級最高。
容我思考一下: 能夠根據優先級來操作的數據結構,在我所知的範圍內只有優先隊列,而優先隊列可以用數組來實現,so,是不是說可以用數組來實現優先級callbacks
的調用,即用嵌套數組。答曰:你想的沒有錯啦。
擼起袖子繼續幹,於是數分鐘後有了下面這個函數
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
,當執行eat
時,判斷是否存在當前index
對應的數組,不存在則創建一個對應的空數組,然後把對應需要調用的函數添加入這個數組中,最後把這個數組存到callbackQueue
中,當添加完成後,會按照順序一步一步從callbackQueue
中取出並執行。
雖然我思路這思路應該是對的,但我還是隱隱約約感覺到了裏面蘊含的紅條條,先丟瀏覽器中跑一下試試。
結果如下:
Hi i am Tony
I am eating lunch
I am eating dinner
等待了5秒....
等待了10秒....
果然,沒有按照所需的順序執行,因爲這裏還是沒有能夠處理sleepFirst
優先級的這個根本問題。。。
等等。。。我剛剛說了啥,'優先級',咱們往上翻,我前面好像提到過這個詞!
沒錯,vue
中的nextTick
中就用到了,我們可以參考它,利用Event Loop
中micro task
和macro task
執行的優先級來解決這個問題。
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();
};
丟瀏覽器執行一下,完全冇問題,丟 node 中也一樣,歐耶,完美。
最後畫幾張粗糙的圖,簡單描述一下這個執行的過程:
因爲是鏈式調用,所以在鏈上的都會入棧然後執行,額,執行棧少畫了 sleep 和 sleepFirst。。。
Hi i am Tony
其中 setTimeout 的 handler 爲宏任務,加入marco task
隊列中;Promise.resolve().then
的回調爲微任務,加入micro task
隊列中
然後執行棧被清空,micro task
中未清空的任務加入執行棧中被執行,
因爲其中有一個 setTimeout,所以把其 handler 加入macro task
中
前面的微任務執行完就出棧了,這時候macro task
中第一個任務入執行棧中進行執行
這個時候如果有 callbacks
就會執行
因爲函數內部又有一個 setTimeout,於是把它的 handler 加入macro task
中
然後清空執行棧,繼續執行下一個宏任務
等待了5秒....
I am eating lunch
I am eating dinner
執行棧爲空,把最後一個宏任務丟進棧中執行
等待了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