Состав функции асинхронной работы Javascript

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

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

Ответ 1

Роберт Россманн прав. Но я готов отвечать исключительно в академических целях.

Позвольте упростить код:

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

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

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

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

Ответ 3

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

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

Edit:

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

Ответ 4

У меня были некоторые мысли об этой проблеме и был создан следующий код, который соответствует вашим требованиям. Тем не менее - я знаю, что эта концепция далека от совершенства. Причины прокомментированы в коде и ниже.

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

Ответ 5

Проблема заключается в том, что 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');