一道关于 sleep 的考题

考题

最近遇到一道面试题

new Human(‘小明’).sleep(3).go(‘home’).eat(‘apple’).sleep(2).eat(‘apple 2’)
输出
// I am 小明
// waiting: sleep for 3s
// go home
// eat an apple
// waiting: sleep for 2s
// eat an apple 2

解析

这道题的难点在于 sleep 方法。

常见的 sleep 方式实现:

live demo

1
2
3
4
5
6
7
8
9
10
11
function sleep(time) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}

(async function() {
console.log("start");
await sleep(3000);
console.log("end");
})();

题目中的调用方式是链式调用,如果要做到 sleep,则需要把所有调用的方法进行收集,等待 sleep 之后再调用堆栈中的下一项。所以,我们把在方法内构造一个匿名函数,然后将函数 push 进堆栈,等待之后调用。

实现

live demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class Human {
name: string;
tasks: any[];
constructor(name: string) {
this.name = name;
this.tasks = [];
this.say("I am " + this.name);
this.start();
}

start() {
const that = this;
setTimeout(() => {
that.runNext();
}, 0);
}

say(what) {
console.log(what);
}

addTask(fn) {
this.tasks.push(fn);
}

runNext() {
const task = this.tasks.shift();
task && task();
}

sleep(time) {
const that = this;
const anon = function() {
console.log("waiting: sleep for " + time);
setTimeout(() => {
that.runNext();
}, time * 1000);
};
this.addTask(anon);
return this;
}

go(where) {
const that = this;
const anon = function() {
that.say("go " + where);
that.runNext();
};
this.addTask(anon);
return this;
}

eat(what) {
const that = this;
const anon = function() {
that.say("Eat an " + what);
that.runNext();
};
this.addTask(anon);
return this;
}
}

var aa = new Human("小明")
.sleep(3)
.go("home")
.eat("apple")
.sleep(2)
.eat("apple too");

上边的实现虽然满足了要求,但是代码却很多重复,我们 decorator 进行重构下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function taskDecorator(autoNext) {
return function(target, name, descriptor) {
if (descriptor) {
const oldValue = descriptor.value;

descriptor.value = function(...args) {
const that = this;
const newValue = function() {
const ret = oldValue.apply(
that,
args.concat(that.runNext.bind(that))
);
if (autoNext) {
that.runNext();
}

return ret;
};

this.addTask(newValue);
return this;
};
return descriptor;
}
};
}

class Human {
name: string;
tasks: any[];
constructor(name: string) {
this.name = name;
this.tasks = [];
this.say("I am " + this.name);
this.start();
}

start() {
const that = this;
setTimeout(() => {
that.runNext();
}, 0);
}

say(what) {
console.log(what);
}

addTask(fn) {
this.tasks.push(fn);
}

runNext() {
const task = this.tasks.shift();
task && task();
}

@taskDecorator(false)
sleep(time, next?) {
console.log("waiting: sleep for " + time);
setTimeout(() => {
next && next();
}, time * 1000);
return this;
}

@taskDecorator(true)
go(where) {
this.say("go: " + where);
return this;
}

@taskDecorator(true)
eat(what) {
this.say("Eat an " + what);
return this;
}
}

var aa = new Human("jack")
.sleep(3)
.go("home")
.eat("apple")
.sleep(2)
.eat("apple too");

我们提取了 @taskDecorator 装饰器,用于装饰需要添加到任务队列的方法。装饰器支持一个参数,告诉装饰器是否自动调用 runNext 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
descriptor.value = function(...args) {
const that = this;
const newValue = function() {
const ret = oldValue.apply(that, args.concat(that.runNext.bind(that)));
if (autoNext) {
that.runNext();
}

return ret;
};

this.addTask(newValue);
return this;
};

对不自动调用的方法,提供了参数注入的形式,注入到方法的最后一个参数上:

1
const ret = oldValue.apply(that, args.concat(that.runNext.bind(that)));