Как я могу запланировать что-то после серии асинхронных задач с GJS?

Я пишу простое настольное приложение на JavaScript с GJS и платформой GNOME: GTK+, GLib, Gio, GObject. Приведенный ниже код иллюстрирует ситуацию, с которой я столкнулся, и его легче воспроизвести, поскольку ему не требуется доступ к файлам, которые использует приложение. Короче говоря, я хотел бы запустить указанную строку кода после завершения ряда асинхронных задач (загрузка содержимого ряда файлов). Как я могу сделать это в GJS, желательно используя что-то из Gio или, может быть, из самого JavaScript?

#!/usr/bin/gjs

const Lang = imports.lang;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

const Home = new Lang.Class({
  Name: "Home",

  // Start snippet 1            
  _enumerateChildrenAsyncCallback: function(dir, result) {
    let fileEnumerator = dir.enumerate_children_finish(result);
    let displayName, file, fileInfo, fileType, iter;
    while ((fileInfo = fileEnumerator.next_file(null))) {
      iter = this.model.append();
      displayName = fileInfo.get_display_name();
      this.model.set(iter, [0], [displayName], 1);

      file = dir.get_child(fileInfo.get_name());
      fileType = file.query_file_type(Gio.FileQueryInfoFlags.NONE, null);
      if (fileType != Gio.FileType.REGULAR) continue;

      file.load_contents_async(null, function(file, result) {
        let [success, contents, etag] = file.load_contents_finish(result);
        let message = "";
        if (success) {
          message = "Finished loading file %s";
        } else {
          message = "Couldn't load file %s";
        }
        log(message.replace("%s", file.get_basename()));
      });
    }
  },

  _init: function() {
    this.application = new Gtk.Application();
    this.application.connect("activate", Lang.bind(this, this._onActivate));
    this.application.connect("startup", Lang.bind(this, this._onStartup));
  },

  _onActivate: function() {
    this._window.show_all();
  },

  _onStartup: function() {
    this.model = new Gtk.ListStore();
    this.model.set_column_types([GObject.TYPE_STRING]);
    let renderer = new Gtk.CellRendererText();

    let dir = Gio.file_new_for_path(GLib.get_home_dir());
    dir.enumerate_children_async("standard::*",
      Gio.FileQueryInfoFlags.NONE,
      GLib.PRIORITY_DEFAULT,
      null,
      Lang.bind(this, this._enumerateChildrenAsyncCallback),
      null);
    /*
     * I would like this line to be run after all files have been read.
     *
     */
    this.model.set_sort_column_id(0, Gtk.SortType.ASCENDING);

    let column = new Gtk.TreeViewColumn({
      title: "Files"
    });
    column.pack_start(renderer, true);
    column.add_attribute(renderer, "text", 0);
    let view = new Gtk.TreeView({
      model: this.model
    });
    view.append_column(column);
    let scrolled = new Gtk.ScrolledWindow();
    scrolled.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC;
    scrolled.Vscrollbar_policy = Gtk.PolicyType.AUTOMATIC;
    scrolled.add(view);
    this._window = new Gtk.ApplicationWindow({
      application: this.application,
      default_height: 300,
      default_width: 400,
      title: "Home, sweet home"
    });
    this._window.add(scrolled);
  }
});

let home = new Home();
home.application.run(ARGV);

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


person Leonardo Fontenelle    schedule 22.08.2015    source источник


Ответы (1)


Если я правильно понимаю, что вы пытаетесь сделать, вам нужно что-то вроде Promise.all(). К сожалению, у GJS нет библиотеки промисов (пока).

Я бы предложил сделать что-то подобное в обратном вызове перечисления детей (я отредактировал некоторые части, которые не имеют отношения к тому, что я пытаюсь проиллюстрировать):

_enumerateChildrenAsyncCallback: function(dir, result) {
  let fileEnumerator = dir.enumerate_children_finish(result);
  let displayName, file, fileInfo, fileType, iter;

  let pendingOperations = new Set();

  while ((fileInfo = fileEnumerator.next_file(null))) {
    file = dir.get_child(fileInfo.get_name());
    fileType = file.query_file_type(Gio.FileQueryInfoFlags.NONE, null);
    if (fileType != Gio.FileType.REGULAR) continue;

    pendingOperations.add(file);

    file.load_contents_async(null, (file, result) => {
      let [success, contents, etag] = file.load_contents_finish(result);

      pendingOperations.delete(file);
      if (pendingOperations.size === 0)
        this.model.set_sort_column_id(0, Gtk.SortType.ASCENDING);
    });
  }
},

На первый взгляд может показаться, что это состояние гонки между pendingOperations очисткой первого элемента и добавлением второго элемента, но load_contents_async обратные вызовы не начнут вызываться до следующей итерации основного цикла, так что вы должны быть в безопасности .

person ptomato    schedule 23.08.2015
comment
Спасибо, помидор! Я не думал о таком подходе. Гораздо легче читать, чем что-либо, зависящее от классов Gio. Однако, прежде чем я приму ваш ответ, я должен попросить вас рассмотреть одно предложение. После запуска вашего кода я получил TypeError: this.model is undefined. Замена function(f, r) {} на Lang.bind(this, function(f, r) {}) позволила избежать сообщений об ошибках и заработала сортировка. На странице Руководство по стилю GJS на вики GNOME говорится, что Lang.bind вещь необходима для this работают как положено внутри затворов. - person Leonardo Fontenelle; 25.08.2015
comment
Вы правы, хотя совет немного устарел - Lang.bind() больше не нужен, потому что GJS уже много лет поддерживает Function.bind(), а также функции толстых стрелок, которые автоматически связывают this, а также начиная с GJS 1.40. Я обновил пример, чтобы использовать функцию толстой стрелки. - person ptomato; 26.08.2015