На Хабре было уже довольно много статей про новые возможности стандарта ECMAScript 6 (например,«Отказываемся от коллбэков: Генераторы в ECMAScript 6») и многие используют эти возможности.

Используя примеры из приведенной статьи, мы можем:
1. Легко написать любой итератор, например, итератор по дереву
2. Написать псевдо-синхронный код (борьба с callback)

Но что делать, если нам надо написать рекурсивный итератор по дереву, а получение дочерних узлов требует вызова асинхронной функции с передачей callback?

Простой рекурсивный итератор (без вызова асинхронных функций) мог бы выглядеть довольно просто и кратко:

function* iterTree(treenode) {
    var children = getChildren(treenode);
    if (children) { // inner node
        for (let i=0; i < children.length; i++) {
            yield* iterTree(children[i]); // (*) recursion
        }
    } else { // leaf node
        yield treenode;
    }	
}

Приведенный выше пример прост, поскольку использует синхронный вызов функции getChildren. Если же функция getChildren асинхронная и требует передачи callback (например, это получение данных с диска или по сети) – то все существенно усложняется. Мы уже не можем так просто написать (следуя примерам из приведенной выше статьи)

let children = yield getChildren(treenode, resumecallback);

В этом случае функция iterTree остановится в месте вызова оператора yield и передаст управление вызывающей функции. Но у нас рекурсивная функция – и вызывающая функция тоже передаст управление выше (оператор yield *). В результате пользователь нашего итератора будет получать узлы дерева, перемежаемые неожиданными значениями.

Можно конечно написать специальные коды внутри той функции, которая использует этот итератор, чтобы различать возвращаемые значения итератора и возвращаемые значения при вызове асинхронных функций. Можно также запоминать путь от корня до текущей вершины (фактически создав структуру вроде стека) и использовать этот путь в callback функции. Но в любом случае код становится довольно сложным – по крайней мере перестает быть легко читаемым.

И все же, можно ли написать простой и читабельный итератор обхода дерева, требующий вызова асинхронных функций?

Несколько удивительно, но новые возможности языка прекрасно сочетаются со старым добрым node-fiber. Существует множество библиотек, основанных на node-fiber, например, wait.for или node-sync.
Например, используя wait.for, можно написать код, практически идентичный простому итератору, приведенному в начале статьи:

function* iterTree(treenode) {
    var children = wait.for(getChildren, treenode); //единственное отличие
    if (children) { // inner node
        for (let i=0; i < children.length; i++) {
            yield* iterTree(children[i]); // (*) recursion
        }
    } else { // leaf node
        yield treenode;
    }
}

Используя node-sync можно получить аналогичный результат.

Совместное использование новых возможностей ES6 и node-fiber (а также библиотек, на их основе) позволяет существенно упростить коды.

Иногда объединение альтернативных подходов дает удивительный результат.

Комментарии (0)