前几天在稀土上看到一篇面试的帖子,里面微信有一道题是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
这道题考察的肯定不是实现这个函数的能力问题,应该是流程控制的问题。解决思路应该是将所有的人如都存放到一个数组中,并在所有的方法执行完之后一次性的输出,实现的代码如下:
function _LazyMan(name) { this.tasks = []; var self = this; var fn =(function(n){ var name = n; return function(){ console.log("Hi! This is " + name + "!"); self.next(); } })(name); this.tasks.push(fn); setTimeout(function(){ self.next(); }, 0); // 在下一个事件循环启动任务 } /* 事件调度函数 */ _LazyMan.prototype.next = function() { var fn = this.tasks.shift(); fn && fn(); } _LazyMan.prototype.eat = function(name) { var self = this; var fn =(function(name){ return function(){ console.log("Eat " + name + "~"); self.next() } })(name); this.tasks.push(fn); return this; // 实现链式调用 } _LazyMan.prototype.sleep = function(time) { var self = this; var fn = (function(time){ return function() { setTimeout(function(){ console.log("Wake up after " + time + "s!"); self.next(); }, time * 1000); } })(time); this.tasks.push(fn); return this; } _LazyMan.prototype.sleepFirst = function(time) { var self = this; var fn = (function(time) { return function() { setTimeout(function() { console.log("Wake up after " + time + "s!"); self.next(); }, time * 1000); } })(time); this.tasks.unshift(fn); return this; } /* 封装 */ function LazyMan(name){ return new _LazyMan(name); }
我自己在思考解决方法的时候最让我困惑的就是如果判断Lazyman对象的方法被调用结束了?我甚至为了这个问题坐过了站。。。后来当我看到这段代码的时候,发现一个简单的 setTimeout
就解决了这个问题,为什么呢???
这张图是MDN对Event Loop的解释,这张图上分为三个部分,分别是队列、栈、和堆。我们在理解lazyman的过程中需要知道的就是队列和栈。首先我们来讲这个栈:
在js中,每当有函数被执行的时候都会在当前的执行堆栈中创建一个新的堆栈帧,并放到栈顶。这个堆栈帧中包含当前执行的函数的参数和局部变量。(有没有感觉很熟悉,没错,这就是我们理解作用域链的时候的那个栈)而当我们的函数执行完之后,这个堆栈帧就会从当前栈中移除。
队列就是JS中用来处理异步事件的队列,每当有新的异步事件发生,就会添加一个新的消息到队列的尾部。当之前提到的栈为空时,JS就会来处理队列中的消息。
举个例子来说就是:
var a = function() { setTimeout(function(){console.log(1)},0) } var b = function(){ a() console.log(2) } // 2 // 1
这里需要 注意 的有
就算你不在函数中使用 setTimeout
,而是在全局环境中使用, setTimeout
也是在正常的同步代码执行完之后执行,这是因为还有宿主环境在。
setTimeOut
是经过一段时间之后直接向队列中加入一个消息,而普通的http请求是等到有返回结果了才会将消息加入到队列中。
普通的异步事件如果没有事件监听器的话是不会操作队列的,消息是直接被忽视掉。
Lazyman中的`setTimeout不是单单的在函数中执行,而是在对象链式调用中执行。因为是链式调用,所以代码一直在同一个作用于中执行,也就是说当前的堆栈帧一直没有被移出栈。上面的代码中就是利用了这个特点解决了如何判断对象调用结束的问题。