Управление большим количеством рекурсии обратного вызова в Nodejs

в Nodejs, практически нет блокирующих операций ввода-вывода. Это означает, что почти весь код ввода-вывода nodejs включает в себя много обратных вызовов. Это касается чтения и записи в/из баз данных, файлов, процессов и т. д. Типичным примером этого является следующее:

var useFile = function(filename,callback){
    posix.stat(filename).addCallback(function (stats) {
        posix.open(filename, process.O_RDONLY, 0666).addCallback(function (fd) {
            posix.read(fd, stats.size, 0).addCallback(function(contents){
                callback(contents);
            });
        });
    });
};

...

useFile("test.data",function(data){
    // use data..
});

Я ожидаю написания кода, который сделает много операции ввода-вывода, так что я буду писать!--4-->много обратные вызовы. Мне вполне комфортно использовать обратные вызовы, но я беспокоюсь обо всех рекурсия. Есть ли опасность столкнуться с слишком большой рекурсией и продуть стек где-нибудь? Если я сделаю тысячи отдельных записей в хранилище ключей с тысячами обратных вызовов, моя программа в конечном итоге рухнет?

Я неправильно понимаю или недооцениваю воздействие? Если нет, есть ли способ обойти это, все еще используя стиль кодирования обратного вызова Nodejs?

6 ответов


ни один из кода, который вы показываете, не использует рекурсию. Когда вы звоните useFile Он называет posix.stat(), который возвращает, и useFile завершает работу по мере ее завершения. В какое-то более позднее время, когда вызов posix.stat() завершено в базовой системе и результаты доступны, функция обратного вызова, которую вы добавили для этого, будет выполнена. Это зовет posix.open(), а затем завершается по мере его завершения. После успешного открытия файла функция обратного вызова для это будет выполняться, назвав posix.read(), а затем завершится, поскольку он тоже побежал к завершению. Наконец, когда результаты чтения будут доступны, будет выполнена самая внутренняя функция.

важным моментом является то, что каждая функция запускается до завершения, так как вызовы posix.*() функции не блокируются: то есть они возвращаются немедленно, вызвав некоторую магию, которая будет запущена в базовой системе. Таким образом, каждая из ваших функций завершается, а затем событие приведет к выполнению следующей функции; но ни в какой момент не существует рекурсии.

вложенная структура кода может создать впечатление, что материал внутри должен будет закончить, прежде чем материал снаружи сможет добраться до своей конечной точки. Но в этом стиле асинхронного событийного программирования имеет смысл видеть вложенность в терминах вглубь => происходит-позже, чем.

EDIT: попробуйте сразу добавить некоторые операторы регистрации перед окончанием каждой вложенной функции; это поможет проиллюстрировать, что порядок, в котором они завершаются, извне внутрь.


тот же пример, с добавленным выходом отладки (см. ниже для вывода):

usefile.js:

var sys = require("sys"),
  posix = require("posix");

var useFile = function(filename,callback){
    posix.stat(filename).addCallback(function (stats) {
        posix.open(filename, process.O_RDONLY, 0666).addCallback(function (fd) {
            posix.read(fd, stats.size, 0).addCallback(function(contents){
                callback(contents);
                sys.debug("useFile callback returned");
            });
            sys.debug("read returned");
        });
        sys.debug("open returned");
    });
    sys.debug("stat returned");
};

useFile("usefile.js",function(){});

выход:

DEBUG: stat returned
DEBUG: open returned
DEBUG: read returned
DEBUG: useFile callback returned

Вы можете попробовать

http://github.com/creationix/do

или свернуть свой собственный, как я сделал. Не обращайте внимания на отсутствие обработки ошибок (просто игнорируйте это);)

var sys = require('sys');

var Simplifier = exports.Simplifier = function() {}

Simplifier.prototype.execute = function(context, functions, finalFunction) {
  this.functions = functions;
  this.results = {};
  this.finalFunction = finalFunction;
  this.totalNumberOfCallbacks = 0
  this.context = context;
  var self = this;

  functions.forEach(function(f) {
    f(function() {
      self.totalNumberOfCallbacks = self.totalNumberOfCallbacks + 1;
      self.results[f] = Array.prototype.slice.call(arguments, 0);     
      if(self.totalNumberOfCallbacks >= self.functions.length) {
        // Order the results by the calling order of the functions
        var finalResults = [];
        self.functions.forEach(function(f) {
          finalResults.push(self.results[f][0]);
        })
        // Call the final function passing back all the collected results in the right order 
        finalFunction.apply(self.context, finalResults);
      }
    });
  });
}

и простой пример его использования

// Execute 
new simplifier.Simplifier().execute(
  // Context of execution
  self,  
  // Array of processes to execute before doing final handling
  [function(callback) {
      db.collection('githubusers', function(err, collection) {
        collection.find({}, {limit:30}, function(err, cursor) {
          cursor.toArray(function(err, users) { callback(users); })
        });
      });      
    },

    function(callback) {
      db.collection('githubprojects', function(err, collection) {
        collection.find({}, {limit:45, sort:[['watchers', -1]]}, function(err, cursor) {
          cursor.toArray(function(err, projects) { callback(projects); })
        });
      });              
    }
  ],  
  // Handle the final result
  function(users, projects) {
    // Do something when ready
  }
);

ваши вещи в порядке. Я делаю рекурсивные вызовы в Express, чтобы следовать перенаправлениям HTTP, но то, что вы делаете, - это "обход", а не рекурсия


также взгляните на "шаг" (http://github.com/creationix/step) или' flow-js ' на github. Это позволяет писать потоки обратного вызова в более естественном стиле. Это также даст понять, что рекурсии не происходит.


Как и в любом JavaScript, можно совершать рекурсивные вызовы с помощью Node.js. Если вы столкнетесь с проблемами глубины рекурсии (как указывает Никфиц, вам это не грозит), вы можете часто переписывать свой код, чтобы использовать интервальный таймер.