2014-12-30 12 views
11

У меня есть несколько асинхронных функций с различным количеством параметров, в каждом последнем параметре есть обратный вызов. Я хочу привести их в порядок. Например.Javascript async function composition

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'); 
+2

'getData.then()' не будет включать вызов функции 'getData()'. Думаю, вы ошибетесь. – Pointy

+7

Могу ли я спросить, почему вы не просто используете библиотеку обещаний (например, q)? https://github.com/kriskowal/q – Antiga

+1

FYI, ['.promisify()'] (https://github.com/petkaantonov/bluebird/blob/master/API.md#promisepromisifyfunction-nodefunction--dynamic -receiver --- function) в таких библиотеках, как Bluebird, сделает все это для вас, а затем вы сделаете что-то вроде 'getDataAsync (...). then (parseDataAsync)'. Если вы хотите реализовать эту функциональность самостоятельно, не используя стороннюю библиотеку, вы можете посмотреть, как она реализована в Bluebird и учиться на этом. – jfriend00

ответ

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; 
} 
13

Реализация функции или библиотеки, которая позволяет вам методы цепи, как выше, является нетривиальной задачей и требует значительных усилий. Основная проблема с приведенным выше примером - постоянное изменение контекста - очень сложно управлять состоянием цепочки вызовов без утечек памяти (т. Е. Сохранять ссылку на все связанные функции в переменной уровня модуля -> GC никогда не освободит функции из памяти).

Если вас интересует такая стратегия программирования, я настоятельно рекомендую вам использовать существующую, установленную и хорошо протестированную библиотеку, например Promise или q. Я лично рекомендую первый, поскольку он пытается вести себя как можно ближе к спецификации Promise ECMAScript 6.

В образовательных целях я рекомендую вам взглянуть на то, как библиотека Promise работает внутри - я уверен, что вы многому научитесь, проверив ее исходный код и играя с ним.

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');

Проблемы и факты:

  • полная цепочка выполнена в контексте функции первого
  • вы должны использовать саму функцию вызовите handOver, по крайней мере, с сначала Элемент цепи
  • если вы создаете новую цепочку с помощью функции вы уже используется, он будет конфликтовать, когда он бежит к же время
  • можно использовать функцию дважды в цепи (напримерGetData)
  • из-за общей conext вы можете установить свойство в одной функции и прочитать его в одном из следующих функций

По крайней мере, для первой задачи можно решить с не вызывая следующую функцию в цепочке в в том же контексте и вместо этого предоставить следующую очередь в качестве параметра. Я попробую этот подход позже. Возможно, это решило бы конфликты, упомянутые в пункте 3.

Для другой проблемы, которую вы могли бы использовать образец кода в комментариях

PS: При запуске отрезала убедитесь, что консоль открыта, чтобы увидеть выходного

PPS: Каждый комментарий на этот подход приветствуется!

+0

'_thenify' должен быть создан для каждого экземпляра функции. В другом случае вы управляете очередью, разделяемой между всеми экземплярами функций. – Ginden

+0

Да - я разделяю очередь в одной цепочке, чтобы каждая функция цепи могла получить к ней доступ. Проблема в том, что каждая функция возвращает контекст первой функции, чтобы убедиться, что мы вызываем первую функцию в конце. – Fuzzyma

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');

3

Это выглядит как отличное применение для Promise объекта.Обещания улучшают возможность повторного использования функций обратного вызова, обеспечивая общий интерфейс для асинхронных вычислений. Вместо того, чтобы каждая функция принимала параметр обратного вызова, 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)); 
}); 

Кроме того, я должен отметить, что в настоящее время обещают отличную поддержку браузера. К счастью, вы покрыты, так как существует множество полиполков, таких как this one, которые обеспечивают большую часть той же функциональности, что и родные обещания.

Редактировать:

В качестве альтернативы, вместо изменения 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.

+0

Почему-то я предположил, что OP не хочет реорганизовать код существующих функций. Однако я не вижу этого нигде. Хороший ответ, показывает практическое использование библиотеки Promise. –