SegmentFault 望春风最新的文章
2017-02-11T16:17:09+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
JavaScript 中如何实现函数队列?(一)
https://segmentfault.com/a/1190000008320677
2017-02-11T16:17:09+08:00
2017-02-11T16:17:09+08:00
lijsh
https://segmentfault.com/u/lijsh
5
<p>假设你有几个函数<code>fn1</code>、<code>fn2</code>和<code>fn3</code>需要按顺序调用,最简单的方式当然是:</p>
<pre><code>fn1();
fn2();
fn3();</code></pre>
<p>但有时候这些函数是运行时一个个添加进来的,调用的时候并不知道都有些什么函数;这个时候可以预先定义一个数组,添加函数的时候把函数push 进去,需要的时候从数组中按顺序一个个取出来,依次调用:</p>
<pre><code>var stack = [];
// 执行其他操作,定义fn1
stack.push(fn1);
// 执行其他操作,定义fn2、fn3
stack.push(fn2, fn3);
// 调用的时候
stack.forEach(function(fn) { fn() });</code></pre>
<p>这样函数有没名字也不重要,直接把匿名函数传进去也可以。来测试一下:</p>
<pre><code>var stack = [];
function fn1() {
console.log('第一个调用');
}
stack.push(fn1);
function fn2() {
console.log('第二个调用');
}
stack.push(fn2, function() { console.log('第三个调用') });
stack.forEach(function(fn) { fn() }); // 按顺序输出'第一个调用'、'第二个调用'、'第三个调用'</code></pre>
<p>这个实现目前为止工作正常,但我们忽略了一个情况,就是异步函数的调用。异步是JavaScript 中无法避免的一个话题,这里不打算探讨JavaScript 中有关异步的各种术语和概念,请读者自行查阅(例如某篇著名的<a href="https://link.segmentfault.com/?enc=cQk0bQ21SSecFSQkBo6Phw%3D%3D.hlz7IW%2FffEBfwYrwkdakSb49nHqvsmCGiJ7XKpGnBC5iOFqciyG3bTdmEykJfERRIWBBb4CxxGWiYkd6TYfKDg%3D%3D" rel="nofollow">评注</a>)。如果你知道下面代码会输出1、3、2,那请继续往下看:</p>
<pre><code>console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
console.log(3);</code></pre>
<p>假如stack 队列中有某个函数是类似的异步函数,我们的实现就乱套了:</p>
<pre><code>var stack = [];
function fn1() { console.log('第一个调用') };
stack.push(fn1);
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
}, 0);
}
stack.push(fn2, function() { console.log('第三个调用') });
stack.forEach(function(fn) { fn() }); // 输出'第一个调用'、'第三个调用'、'第二个调用'</code></pre>
<p>问题很明显,<code>fn2</code>确实按顺序调用了,但<code>setTimeout</code>里的<code>function fn2Timeout() { console.log('第二个调用') }</code>却不是立即执行的(即使把timeout 设为0);<code>fn2</code>调用之后马上返回,接着执行<code>fn3</code>,<code>fn3</code>执行完了然才真正轮到<code>fn2Timeout</code>。<br>怎么解决?我们分析下,这里的关键在于<code>fn2Timeout</code>,我们必须等到它真正执行完才调用<code>fn3</code>,理想情况下大概像这样:</p>
<pre><code>function fn2() {
setTimeout(function() {
fn2Timeout();
fn3();
}, 0);
}</code></pre>
<p>但这样做相当于把原来的<code>fn2Timeout</code>整个拿掉换成一个新函数,再把原来的<code>fn2Timeout</code>和<code>fn3</code>插进去。这种动态改掉原函数的写法有个专门的名词叫<strong>Monkey Patch</strong>。按我们程序员的口头禅:“做肯定是能做”,但写起来有点拧巴,而且容易把自己绕进去。有没更好的做法?<br>我们退一步,不强求等<code>fn2Timeout</code>完全执行完才去执行<code>fn3</code>,而是在<code>fn2Timeout</code>函数体的最后一行去调用:</p>
<pre><code>function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
fn3(); // 注{1}
}, 0);
}</code></pre>
<p>这样看起来好了点,不过定义<code>fn2</code>的时候都还没有<code>fn3</code>,这<code>fn3</code>哪来的?</p>
<p>还有一个问题,<code>fn2</code>里既然要调用<code>fn3</code>,那我们就不能通过<code>stack.forEach</code>去调用<code>fn3</code>了,否则<code>fn3</code>会重复调用两次。</p>
<p>我们不能把<code>fn3</code>写死在<code>fn2</code>里。相反,我们只需要在<code>fn2Timeout</code>末尾里找出<code>stack</code>中<code>fn2</code>的下一个函数,再调用:</p>
<pre><code>function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
next();
}, 0);
}</code></pre>
<p>这个<code>next</code>函数负责找出stack 中的下一个函数并执行。我们现在来实现<code>next</code>:</p>
<pre><code>var index = 0;
function next() {
var fn = stack[index];
index = index + 1; // 其实也可以用shift 把fn 拿出来
if (typeof fn === 'function') fn();
}</code></pre>
<p><code>next</code>通过<code>stack[index]</code>去获取<code>stack</code>中的函数,每调用<code>next</code>一次<code>index</code>会加1,从而达到取出下一个函数的目的。</p>
<p><code>next</code>这样使用:</p>
<pre><code>var stack = [];
// 定义index 和next
function fn1() {
console.log('第一个调用');
next(); // stack 中每一个函数都必须调用`next`
};
stack.push(fn1);
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二个调用');
next(); // 调用`next`
}, 0);
}
stack.push(fn2, function() {
console.log('第三个调用');
next(); // 最后一个可以不调用,调用也没用。
});
next(); // 调用next,最终按顺序输出'第一个调用'、'第二个调用'、'第三个调用'。</code></pre>
<p>现在<code>stack.forEach</code>一行已经删掉了,我们自行调用一次<code>next</code>,<code>next</code>会找出<code>stack</code>中的第一个函数<code>fn1</code>执行,<code>fn1</code> 里调用<code>next</code>,去找出下一个函数<code>fn2</code>并执行,<code>fn2</code>里再调用<code>next</code>,依此类推。</p>
<p>每一个函数里都必须调用<code>next</code>,如果某个函数里不写,执行完该函数后程序就会直接结束,没有任何机制继续。</p>
<p>了解了函数队列的这个实现后,你应该可以解决下面这道面试题了:</p>
<pre><code>// 实现一个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
*/
// 以此类推。</code></pre>
<p>Node.js 中大名鼎鼎的<code>connect</code>框架正是这样实现中间件队列的。有兴趣可以去看看它的<a href="https://link.segmentfault.com/?enc=%2B93X93Ip%2FV58jBJkhK2grw%3D%3D.f7pDcTb1dAWVRBRXDhce%2F3cjgmz0r%2B46sSRH4pJ4qB91hjlJSPBR9Z%2FJTf5Zut5s54Y%2BALaE4%2FQcKaSOcWD0gg%3D%3D" rel="nofollow">源码</a>或者这篇解读<a href="https://link.segmentfault.com/?enc=oQFZV0Sq6jAZedP0LIzv0w%3D%3D.JvvLXh%2Fyb%2BNI5Q8BgledQgRozc5MFvOeUJfkTW48mw%2BSSWeH9bhN701xBdIhYDnDZVnXbFL6sMheFk6cU%2BdHDA%3D%3D" rel="nofollow">《何为 connect 中间件》</a>。</p>
<p>细心的你可能看出来,这个<code>next</code>暂时只能放在函数的末尾,如果放在中间,原来的问题还会出现:</p>
<pre><code>function fn() {
console.log(1);
next();
console.log(2); // next()如果调用了异步函数,console.log(2)就会先执行
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=w7Q3zv4DCFN1aP7eGadVVQ%3D%3D.Yp1FDaEWDMUWBfTSwcX%2Fpw6lQd0SIW51LyKSYLnT%2FxQ%3D" rel="nofollow">redux</a> 和<a href="https://link.segmentfault.com/?enc=gZofTwGsLxyTNiBXsVpsOA%3D%3D.94fDe8SbOiTwkHo5ND5a%2F402k9sYiBLVrAjDTxAK3Rc%3D" rel="nofollow">koa</a> 通过不同的实现,可以让<code>next</code>放在函数中间,执行完后面的函数再折回来执行<code>next</code>下面的代码,非常巧妙。有空再写写。</p>