引子
在学Node的时候,遇到了一个关于回调的问题:
有若干个进行资源调用的函数(比如它们共享一个mongoDB的连接),要确保在它们都执行完成后,执行一个资源销毁的函数,怎么做比较好?
Node提供的是一个纯异步的执行环境,决定了至少这样的代码是行不通的:
1 2 3 |
a(resource); b(resource); destory(resource); |
因为:很可能在函数a或b执行完成之前,destory已经被触发。
那这样行不行:
1 2 3 4 5 6 7 8 9 |
a(resource, function(err){ if(!err){ b(resource, function(err){ if(!err){ destory(resource); } }); } }); |
当然行,随之而来的问题也很明显:
- 函数互相间的侵入性太强,基本弹不上什么模块化
- 这只是举例就两个方法a和b,执行链的扩容和回调嵌套的深度是成比例线性上升的,}}}}}}}}}}}}…太可怕了,我不保证不会有typo
回调嵌套形成的结构,被形象的称之为 “Pyramid of Doom” 🙂
事实上, NPM上有很优秀的库可以解决这种问题, 比如:async, wind。
在上述这种场景中,实际需要的是一个工具:异步调用各个函数时,能汇总各函数的完成状态,在全部完成时,统一善后。
场景
更真实的场景大概是,有若干个方法共享一个MongoDB实例连接,在执行完成后,需要断开连接:
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 |
var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/test'); /* definition & ODM */ var Person = mongoose.model('Person', new mongoose.Schema({ name: String, sex: String })); function savePerson(name, sex, callback) { var person = new Person({ name: name, sex: sex }); person.save(function (err) { if (err) console.log(err); if (callback) callback(); }); } function findPersons(name, callback) { Person.find({name: name}, function (err, persons) { if (err) console.log(err); console.log(persons); if (callback) callback(persons); }); } function findPersonsAndDelete(name, callback) { findPersons(name, function (persons) { if (persons) persons.forEach(function (element, index, array) { element.remove(); }); if (callback) callback(); }); } function disconnectMongo() { mongoose.disconnect(); } |
思路1
能不能将要执行的函数构造成一个数组,写一个工具函数去迭代执行它们,并与它们协定,在执行完成前通知工具函数,工具函数负责记数以确定所有函数是否执行完成?
很容易想到事件是不是,使用EventEmitter,协定好事件,调与被调生殖隔离:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var events = require('events'); var emitter = new events.EventEmitter(); /* based on event */ var done = function () { emitter.emit('done'); }; var seq = function (methods, callback) { if (Object.prototype.toString.call(methods) !== '[object Array]') throw new Error('methods is not an array!'); var successful = 0; function doneHandler() { successful++; if (successful == methods.length) callback(); else emitter.once('done', doneHandler); }; emitter.once('done', doneHandler); methods.forEach(function (method) { method(); }); }; |
调用:
1 2 3 4 5 6 7 8 9 |
seq([ function () { savePerson('Mo Ye', 'Male', done); }, function () { findPersonsAndDelete('Mo Ye', done); } ], disconnectMongo); |
结论:丑,不满意 🙁
思路2
在进行数组迭代时,把计数器变量闭包到一个回调函数中,每一个函数执行完成时,都去回调它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var seq2 = function (methods, callback) { if (Object.prototype.toString.call(methods) !== '[object Array]') throw new Error('methods is not an array!'); var successful = 0; function complete() { successful++; if (successful === methods.length) callback(); } methods.forEach(function (method) { method.call(null, complete); }); }; |
调用:
1 2 3 4 5 6 7 8 9 |
seq2([ function (callback) { savePerson('Mo Ye', 'Male', callback); }, function (callback) { findPersonsAndDelete('Mo Ye', callback); } ], disconnectMongo); |
小结:貌似更丑了,而且对于实际执行的数组构造,有了更强的侵入 🙁
小结
虽然抓住了执行流汇总的本质,但上述思路有一个潜在的问题:并不是真串行,仅仅只是汇总。
也许Promise / Deferred 模式会是一个更好的方案——先到这里,我得去充电了 🙂
打赏作者
您的支持将激励我继续创作!