我有几个具有不同数量参数的异步函数,每个参数都是一个回调函数。我希望按顺序打电话。例如。
function getData(url, callback){
}
function parseData(data, callback){
}
使用这个:
Function.prototype.then = function(f){
var ff = this;
return function(){ ff.apply(null, [].slice.call(arguments).concat(f)) }
}
可以像这样调用这些函数,并将输出打印到console.log。
getData.then(parseData.then(console.log.bind(console)))('/mydata.json');
我一直试图使用这种语法,但无法使Then函数正确。有什么想法吗?
getData.then(parseData).then(console.log.bind(console))('/mydata.json');
答案 0 :(得分:13)
实现允许您链接上述方法的函数或库是一项非常重要的任务,需要大量工作。上面示例的主要问题是不断更改上下文 - 在没有内存泄漏的情况下管理调用链的状态非常困难(即将所有链接函数的引用保存到模块级变量中 - > GC永远不会释放内存中的功能。)
如果您对这种编程策略感兴趣,我强烈建议您使用现有的,经过良好测试的库,例如Promise或q。我个人推荐前者,因为它试图尽可能接近ECMAScript 6的Promise规范。
出于教育目的,我建议你看看Promise库如何在内部工作 - 我相信你会通过检查它的源代码并玩弄它来学到很多东西。
答案 1 :(得分:3)
Robert Rossmann是对的。但我愿意纯粹出于学术目的回答。
让我们将您的代码简化为:
Function.prototype.then = function (callback){
var inner = this;
return function (arg) { return inner(arg, callback); }
}
和
function getData(url, callback) {
...
}
让我们分析每个功能的类型:
getData
是(string, function(argument, ...)) → null
。function(argument, function).then
是(function(argument, ...)) → function(argument)
。这是问题的核心。当你这样做时:
getData.then(function (argument) {})
它实际上返回一个类型为function(argument)
的函数。这就是为什么.then
无法调用它的原因,因为.then
期望被调用到function(argument, function)
类型。
你想要做的是包装回调函数。 (如果是getData.then(parseData).then(f)
,您希望parseData
包含f
,而不是getData.then(parseData)
的结果。
这是我的解决方案:
Function.prototype.setCallback = function (c) { this.callback = c; }
Function.prototype.getCallback = function () { return this.callback; }
Function.prototype.then = function (f) {
var ff = this;
var outer = function () {
var callback = outer.getCallback();
return ff.apply(null, [].slice.call(arguments).concat(callback));
};
if (this.getCallback() === undefined) {
outer.setCallback(f);
} else {
outer.setCallback(ff.getCallback().then(f));
}
return outer;
}
答案 2 :(得分:3)
这看起来非常适合Promise对象。 Prom通过为异步计算提供通用接口来提高回调函数的可重用性。 Promises允许您将函数的异步部分封装在Promise对象中,而不是让每个函数都接受一个回调参数。然后,您可以使用Promise方法(Promise.all,Promise.prototype.then)将异步操作链接在一起。以下是您的示例的翻译方式:
// Instead of accepting both a url and a callback, you accept just a url. Rather than
// thinking about a Promise as a function that returns data, you can think of it as
// data that hasn't loaded or doesn't exist yet (i.e., promised data).
function getData(url) {
return new Promise(function (resolve, reject) {
// Use resolve as the callback parameter.
});
}
function parseData(data) {
// Does parseData really need to be asynchronous? If not leave out the
// Promise and write this function synchronously.
return new Promise(function (resolve, reject) {
});
}
getData("someurl").then(parseData).then(function (data) {
console.log(data);
});
// or with a synchronous parseData
getData("someurl").then(function (data) {
console.log(parseData(data));
});
另外,我应该注意到Promise目前还没有出色的浏览器支持。幸运的是,你已经被覆盖,因为有很多填充物,例如this one,它提供了与本机Promise相同的功能。
编辑:
或者,不是改变Function.prototype,而是如何实现一个链接方法,该方法将异步函数列表作为输入,种子值和管道通过每个异步函数来生成值:
function chainAsync(seed, functions, callback) {
if (functions.length === 0) callback(seed);
functions[0](seed, function (value) {
chainAsync(value, functions.slice(1), callback);
});
}
chainAsync("someurl", [getData, parseData], function (data) {
console.log(data);
});
再次编辑:
如果您想要更广泛的解决方案,请查看https://github.com/caolan/async之类的内容,上面介绍的解决方案远非强大。
答案 3 :(得分:2)
我对这个问题有一些想法,并创建了以下符合您要求的代码。仍然 - 我知道这个概念远非完美。原因在代码及以下内容中进行了评论。
Function.prototype._thenify = {
queue:[],
then:function(nextOne){
// Push the item to the queue
this._thenify.queue.push(nextOne);
return this;
},
handOver:function(){
// hand over the data to the next function, calling it in the same context (so we dont loose the queue)
this._thenify.queue.shift().apply(this, arguments);
return this;
}
}
Function.prototype.then = function(){ return this._thenify.then.apply(this, arguments) };
Function.prototype.handOver = function(){ return this._thenify.handOver.apply(this, arguments) };
function getData(json){
// simulate asyncronous call
setTimeout(function(){ getData.handOver(json, 'params from getData'); }, 10);
// we cant call this.handOver() because a new context is created for every function-call
// That means you have to do it like this or bind the context of from getData to the function itself
// which means every time the function is called you have the same context
}
function parseData(){
// simulate asyncronous call
setTimeout(function(){ parseData.handOver('params from parseData'); }, 10);
// Here we can use this.handOver cause parseData is called in the context of getData
// for clarity-reasons I let it like that
}
getData
.then(function(){ console.log(arguments); this.handOver(); }) // see how we can use this here
.then(parseData)
.then(console.log)('/mydata.json'); // Here we actually starting the chain with the call of the function
// To call the chain in the getData-context (so you can always do this.handOver()) do it like that:
// getData
// .then(function(){ console.log(arguments); this.handOver(); })
// .then(parseData)
// .then(console.log).bind(getData)('/mydata.json');

问题和事实:
至少对于第一个问题,您可以通过不在相同的上下文中调用链中的下一个函数来解决它,而是将队列作为参数提供给下一个功能。我稍后会尝试这种方法。这也许可以解决第3点提到的冲突。
对于其他问题,您可以在评论中使用示例代码
PS:当您运行剪切时,请确保您的控制台已打开以查看输出
PPS:欢迎对此方法发表评论!
答案 4 :(得分:2)
问题是then
返回当前函数的包装器,连续的链式调用将再次包装它,而不是包装前一个回调。实现这一目标的一种方法是使用闭包并在每次调用时覆盖then
:
Function.prototype.then = function(f){
var ff = this;
function wrapCallback(previousCallback, callback) {
var wrapper = function(){
previousCallback.apply(null, [].slice.call(arguments).concat(callback));
};
ff.then = wrapper.then = function(f) {
callback = wrapCallback(callback, f); //a new chained call, so wrap the callback
return ff;
}
return wrapper;
}
return ff = wrapCallback(this, f); //"replace" the original function with the wrapper and return that
}
/*
* Example
*/
function getData(json, callback){
setTimeout( function() { callback(json) }, 100);
}
function parseData(data, callback){
callback(data, 'Hello');
}
function doSomething(data, text, callback) {
callback(text);
}
function printData(data) {
console.log(data); //should print 'Hello'
}
getData
.then(parseData)
.then(doSomething)
.then(printData)('/mydata.json');