前言:昨天在群里讨(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