Cuuube / blog

blog on Mirror
1 stars 0 forks source link

[js]lazy man的实现 #46

Open Cuuube opened 7 years ago

Cuuube commented 7 years ago

lazy man的实现

原题

之前微信的面试题,实现一个lazyman,要求创建一个lazyman之后,输入一串指令,结束后lazy按次序执行。

原题似乎是这样的:(抄自LazyMan的深入解析和实现 - 简书 - wall_wxk

实现一个LazyMan,可以按照以下方式调用:

LazyMan("Hank")输出:

Hi! This is Hank!

LazyMan("Hank").sleep(10).eat("dinner")输出

Hi! This is Hank!

//等待10秒.. Wake up after 10 Eat dinner~

LazyMan("Hank").eat("dinner").eat("supper")输出 Hi This is Hank! Eat dinner~ Eat supper~   LazyMan("Hank").sleepFirst(5).eat("supper")输出 //等待5秒 Wake up after 5 Hi This is Hank! Eat supper   以此类推。

初步实现

因为一般而言,调用某方法的话,会立刻执行方法中的函数。但是此题需要输入完所有方法再延迟执行,因此需要建立一个队列,最后输入完任务,执行队列。

我的实现(以下代码可复制到较新版chrome浏览器中直接执行):

class Lazyman {
    constructor (name) {
        this.name = name;
        this.taskList = [];
        console.log(`Lazy man named ${name} was born!`);
    }
    next () {
        if (this.taskList.length > 0) {
            this.taskList.shift()();
        }
        return this;
    }
    eat (what) {
        this.taskList.push(() => {
            console.log(this.name + ' eat ' + what + '!');
            this.next();
        })
        return this;
    }
    sleep (num) {
        this.taskList.push(() => {
            console.log(`${this.name} sleep ${num} seconds!`);
            setTimeout(() => {
                this.next();
            }, num*1000);
        })
        return this;
    }
    sleepFirst (num) {
        this.taskList.unshift(() => {
            console.log(`${this.name} sleep ${num} seconds at first!`);
            setTimeout(() => {
                this.next();
            }, num*1000);
        })
        return this;
    }
    do (fn) {
        this.taskList.push(() => {
            fn(this.name);
            this.next();
        })
        return this;
    }
    doFirst (fn) {
        this.taskList.unshift(() => {
            fn(this.name);
            this.next();
        })
        return this;
    }
}

const lazyman = (name) => new Lazyman(name);

lazyman('Tom')
.eat('dinner')
.sleep(1)
.eat('Ball')
.sleep(2)
.eat('huaji')
.doFirst(name => console.log(`${name} eat apple!`))
.sleepFirst(3)
.do(name => console.log(`${name} laugh!`))
.next();

核心就是,每次命令lazyman做事时,将做的事插入到队列taskList最后端,如果是要首先去做的事(doFirst),就插入到队列最前端。

并且做的事中都要再次调用next方法,确保有事可做时,会一直做到最后。

命令输入完毕时,执行.next,顺序执行。

注意的几点

  1. 方法返回this链式调用
  2. setTimeout的定时,配合next方法可以起到delay的作用。
  3. poppushshiftunshift的异同

反思

有几点我不满意:

  1. 执行最后都必须跟上.next(),手动开始。能不能不用这条语句,lazyman自动检测队列输入完毕?
  2. 如果我要加功能,.doLast(),最后执行的,应该怎么做?
  3. 我要加入.break(),执行到这一步,lazyman不想做后面的事了,应该怎么办?
  4. 看别人的实现,有句话很赞同,console.log最好抽象出方法。因为lazyman的输出可能会改变,到时候代码中的每句console.log都去改就太麻烦了。

更新:实现break

于是马上实现了下break:

// ... Lazyman 中添加break方法:  
    break () {
        this.taskList.push(() => {
            console.log(`${this.name} don't want to do anything.`);
            this.taskList = [];
        })
        return this;
    }
// ...

执行后:

Lazy man named Tom was born!
Tom sleep 3 seconds at first!
Tom eat apple!
Tom eat dinner!
Tom sleep 1 seconds!
Tom eat Ball!
Tom don't want to do anything.

确实中断了队列后面的任务。

可惜无法中断doFirst的任务。因为doFirst任务会插在队列头部。

要实现也可以,一种方法是将任务抽象出类,需要包含一个是否丢弃的boolean值。默认为false,一旦调用break,后面新增的task任务都改为true(要丢弃),next的时候不执行。

class LazymanTask {
    constructor (config) {
        this.ignore = config.ignore;
        this.action = config.action;
    }
}

Lazyman的改动:

// 增加tired字段    
    constructor (name) {
        this.name = name;
        this.taskList = [];
        this.tired = false;
        console.log(`Lazy man named ${name} was born!`);
    }
// 一般任务方法:
    eat (what) {
        let task = new LazymanTask({
            ignore: this.tired,
            action: () => {
                console.log(this.name + ' eat ' + what + '!');
                this.next();
            }
        });
        this.taskList.push(task)
        return this;
    }
// break方法:
    break () {
        let task = new LazymanTask({
            ignore: this.tired,
            action: () => {
                console.log(`${this.name} don't want to do anything.`);
                this.taskList = [];
            }
        });
        this.taskList.push(task);
        this.tired = true;        
        return this;
    }
// next方法:
    next () {
        this.taskList = this.taskList.filter(val => !val.ignore);
        if (this.taskList.length > 0) {
            this.taskList.shift().action();
        }
        return this;
    }

运行下列代码:

lazyman('Tom')
.eat('dinner')
.sleep(1)
.eat('Ball')
.break()
.sleep(2)
.eat('huaji')
.sleepFirst(3)
.next();

// ->结果
Lazy man named Tom was born!
Tom eat dinner!
Tom sleep 1 seconds!
Tom eat Ball!
Tom don't want to do anything.

看来还是有效的。

更新: 实现doLast

思路:

last的列表不能被污染,必须放到队列最尾。

做法:

Lazyman类中加入另一个task列表:lastTask

如果使用doLast类型的方法加入任务,则插入到改列表中。

运行时,将lastTask合并至taskList,然后清空taskList。

最后和原来一样按序执行taskList就可以了。

代码:

// 增加lastTask列表 
    constructor (name) {
        this.name = name;
        this.taskList = [];
        this.lastTask = [];
        this.tired = false;
        console.log(`Lazy man named ${name} was born!`);
    }
// doLast类型的方法
    eatLast (what) {
        let task = new LazymanTask({
            ignore: this.tired,
            action: () => {
                console.log(this.name + ' eat ' + what + '!');
                this.next();
            }
        });
        this.lastTask.push(task)
        return this;
    }
// next方法:
    next () {
        if (this.lastTask.length > 0) {
            this.taskList = this.taskList.concat(this.lastTask);
            this.lastTask = [];
        }
        this.taskList = this.taskList.filter(val => !val.ignore);
        if (this.taskList.length > 0) {
            this.taskList.shift().action();
        }
        return this;
    }

运行就略了。

更新:实现代码串最后省略.next

实现起来很简单,先放结论:

构造函数中最后增加一行代码:

    constructor (name) {
        this.name = name;
        this.taskList = [];
        this.lastTask = [];
        this.tired = false;
        console.log(`Lazy man named ${name} was born!`);
        setTimeout(() => this.next(), 0);    // 这里
    }

请注意,setTimeout函数中需要用把this.next()放到箭头函数内。

只有放到箭头函数内,this才可以直接取到当前Lazyman对象。若需要用es5形式,则应加闭包,把环境this传进去:

setTimeout(
   (function (This) { 
        return function () {This.next()}
    })(this)
, 0);

闭包问题不再细说。

运行lazyman('Tom').eat('dinner').sleep(1).eat('Ball'):

Lazy man named Tom was born!
Tom eat dinner!
Tom sleep 1 seconds!
Tom eat Ball!

我们没有在执行链最后加next,而是放到构造函数中,就成功了。

但是为何用setTimeout(fn, 0)的形式呢?

此处那就是下一个话题了。