Всем доброго времени суток. Сегодня я хочу рассказать о том, как писал реализацию механизма промисов для своего JS движка. Как известно, не так давно вышел новый стандарт ECMA Script 6, и концепция промисов выглядит довольно интересно, а также уже очень много где применяется веб-разработчиками. Поэтому для любого современного JS движка это, безусловно, must-have вещь.
Внимание: в статье довольно много кода. Код не претендует на красоту и высокое качество, поскольку весь проект писался одним человеком и всё ещё находится в бете. Цель данного повествования — показать, как же всё работает под капотом. Кроме того, после небольшой адаптации данный код можно использовать для создания проектов чисто на Java, без оглядки на JavaScript.

Первое, с чего стоило начать написание кода — это с изучения того, как всё должно работать в итоге. Архитектура получившегося модуля во многом определялась по ходу процесса.

Что такое Promise?


Promise — это специальный объект, который при создании находится в состоянии pending (пусть это будет константа равная 0).

Далее объект начинает исполнять функцию, которая была передана в его конструктор при создании. Если функция не была передана — следуя стандарту ES6, мы должны бросить исключение argument is not a function. Однако в нашей Java реализации можно ничего не кидать, и создать объект «как есть» (просто потом добавить дополнительную логику, я скажу об этом позже).

Итак, конструктор принимает функцию. В нашем движке это объект класса Function, реализующий метод call. Данный метод позволяет вызвать функцию, принимая на вход контекст исполнения, вектор с аргументами, и boolean параметр, определяющий режим вызова (вызов как конструктора или обычный режим).

Далее эта функция записывается в поле нашего объекта и потом может быть вызвана.

    public static int PENDING = 0;
    public static int FULFILLED = 1;
    public static int REJECTED = 2;

    ...

    private int state = 0;
    private Function func;

Заодно здесь же создадим константы для наших двух оставшихся состояний, и int поле, хранящее текущее состояние объекта.

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

Это — обычные функции с точки зрения JavaScript, а значит, объекты Function с точки зрения нашего движка. Добавим поля и для них:

    public Function onFulfilled = null;
    public Function onRejected = null;

Эти функции могут быть вызваны в любой момент нашей основной рабочей функцией, а значит, должны находиться в её области видимости (scope). Кроме того, отработав, они должны менять состояние нашего объекта на fulfilled и rejected, соответственно. Наши функции ничего не знают про промисы (и знать не должны). Поэтому, нам нужно создать некую обёртку, которая будет про них знать, и сможет инициировать смену состояния.

Также нам нужен метод setState() для нашего объекта (с дополнительными проверками: например, мы не имеем права менять состояние, если оно уже fulfilled или rejected).

Займёмся конструктором нашего объекта:

   public Promise(Function f) {
        func = f;
        onFulfilled = new PromiseHandleWrapper(this, null, Promise.FULFILLED);
        onRejected = new PromiseHandleWrapper(this, null, Promise.REJECTED);
        if (f != null) {
            Vector<JSValue> args = new Vector<JSValue>();
            args.add(onFulfilled);
            args.add(onRejected);
            func.call(null, args, false);
        }
    }

Здесь, кажется, всё понятно. Если функция передана — мы обязаны вызвать её немедленно. Если нет — то ничего пока что не делаем (а наш объект сохраняет состояние pending).

Теперь про установку самих этих обработчиков (ведь в основной функции мы только объявляем их имена как формальные параметры). Для этого стандартом предусмотрены три варианта: Promise.then(resolve, reject), Promise.then(resolve) (эквивалентно Promise.then(resolve, null)), и Promise.catch(reject) (эквивалентно Promise.then(null, reject)).

Насчёт функции then: очевидно, что лучше всего реализовать подробно метод с двумя аргументами, а оставшиеся два сделать как «шорткаты» на него. Так и поступим:

   public Promise then(Function f1, Function f2) {
        if (state == Promise.FULFILLED || state == Promise.REJECTED) {
            onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED);
            onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED);
            onFulfilled.call(null, new Vector<JSValue>(), false);
            return this;
        }
        ...
        onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED);
        onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED);
        if (func != null) {
            String name1 = func.getParamsCount() > 0 ? func.getParamName(0) : "resolve";
            String name2 = func.getParamsCount() > 1 ? func.getParamName(1) : "reject";
            func.injectVar(name1, onFulfilled);
            func.injectVar(name2, onRejected);
        }
        if (f1 != null) has_handler = true;
        if (f2 != null) has_error_handler = true;
        return this;
    }


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

Что за блок у нас в начале метода, спросите вы? А дело в том, что наш обработчик мог исполниться ещё до того, как мы в первый раз вызвали then (такое бывает, и это совершенно нормально). В этом случае мы должны вызвать нужный обработчик из переданных в метод немедленно.

В месте многоточия потом будет ещё код, про него чуть позже.

Далее идёт установка наших обработчиков в нужные поля.

А вот далее самое интересное. Предположим, наша рабочая функция исполняется достаточно долго (запрос по сети, или просто setTimeout для учебного примера). В этом случае она по сути как бы исполнится, но создаст ряд объектов (таймер, сетевой XmlHttpRequest интерфейс и т.д.) которые исполнят некоторый код позднее. И эти объекты имеют доступ к scope нашей функции!

Поэтому сейчас ещё может быть не поздно добавить нужные переменные в её область видимости (а если поздно — то исполнится код в начале метода). Для этого мы создаём новый метод в классе Function:

    public void injectVar(String name, JSValue value) {
        body.scope.put(name, value);
    }

    public void removeVar(String name) {
        body.scope.remove(name);
    }

Второй метод нам фактически не понадобится: он создан чисто ради полноты картины.

Теперь время реализовать шорткаты:

    public Promise then(Function f) {
        return then(f, null);
    }

    public Promise _catch(Function f) {
        return then(null, f);
    }

catch — зарезервированное слово в языке java, поэтому нам пришлось добавить знак подчёркивания.

Теперь опишем метод setState. В первом приближении он будет выглядеть так:

    public void setState(int value) {
        if (this.state > 0) return;
        this.state = value;
    }

Отлично, теперь мы сможем менять состояние из наших обработчиков — точнее, из обёрток над ними. Займёмся обёртками:

public class PromiseHandleWrapper extends Function {
    public PromiseHandleWrapper(Promise p, Function func, int type) {
        this.promise = p;
        this.func = func;
        this.to_state = type;
    }

    @Override
    public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) {
        return call(context, args);
    }

    @Override
    public JSValue call(JSObject context, Vector<JSValue> args) {
        JSValue result;
        if (func != null) {
            Block b = getCaller();
            if (b == null) {
                b = func.getParentBlock();
                while (b.parent_block != null) {
                    b = b.parent_block;
                }
            }
            func.setCaller(b);
            result = func.call(context, args, false);
        } else {
            result = Undefined.getInstance();
        }
        promise.setResult(result);
        promise.setState(to_state);
        return promise.getResult();
    }

    @Override
    public JSError getError() {
        return func.getError();
    }

    private Promise promise;
    private Function func;
    private int to_state = 0;
}

Типов обёрток у нас два, но класс один. А за тип отвечает целочисленное поле to_state. Вроде, неплохо :)

Обёртка имеет ссылки как на свою функцию, так и на свой промис. Это очень важно.

С конструктором всё понятно, давайте посмотрим на метод call, переопределяющий метод класса Function. Для нашего JS интерпретатора — обёртки такие же функции, то есть объекты с тем же интерфейсом, которые можно вызывать, получать их значения, и так далее.

Сначала нам нужно пробросить в функцию объект Caller, полученный при вызове обёртки — это нужно как минимум для корректного всплытия исключений.

Далее мы вызываем нашу функцию и сохраняем в поле результат её исполнения. Заодно устанавливаем его в объект промиса, для чего создадим там ещё один метод setResult:

    public JSValue getResult() {
        return result;
    }

    public void setResult(JSValue value) {
        result = value;
    }

Про последнюю строчку пока говорить не будем: это нужно для чейнинга. В самом тривиальном случае там вернётся то же самое значение, которое мы только что получили и передали.

Важный момент: рабочая функция может вызвать resolve или reject до того, как мы вызовем метод then или catch (или мы можем не вызвать их вовсе). Чтобы при этом у нас не возникло исключения, прямо при создании промиса у нас создаются две «дефолтных» обёртки, у которых нет функций-обработчиков. При вызове они всего лишь поменяют состояние нашего промиса (и потом при вызове then это будет учтено).

Чейнинг промисов


Если коротко, чейнинг — это возможность писать вещи вида p.then(f1, f2).then(f3, f4).catch(f5).
Именно для этого наши методы then и _catch возвращают объект Promise.

Первое, что говорит нам стандарт — это то, что метод then при наличии существующего обработчика должен создать новый промис и добавить его в цепочку. Поскольку наши промисы должны быть равны между собой — пускай у нас не будет никакого головного промиса, хранящего линейный список, а каждый промис будет хранить только ссылку на следующий (изначально она равна null):

    public Promise then(Function f1, Function f2) {
        if (state == Promise.FULFILLED || state == Promise.REJECTED) {
            onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED);
            onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED);
            onFulfilled.call(null, new Vector<JSValue>(), false);
            return this;
        }
        if (has_handler || has_error_handler) {
            if (next != null) {
                return next.then(f1, f2);
            }
            Promise p = new Promise(null);
            p.then(f1, f2);
            next = p;
            return p;
        }
        onFulfilled = new PromiseHandleWrapper(this, f1, Promise.FULFILLED);
        onRejected = new PromiseHandleWrapper(this, f2, Promise.REJECTED);
        if (func != null) {
            String name1 = func.getParamsCount() > 0 ? func.getParamName(0) : "resolve";
            String name2 = func.getParamsCount() > 1 ? func.getParamName(1) : "reject";
            func.injectVar(name1, onFulfilled);
            func.injectVar(name2, onRejected);
        }
        if (f1 != null) has_handler = true;
        if (f1 != null) has_error_handler = true;
        return this;
    }

    ...

    private Promise next = null;

Вот и наш недостающий блок: если у нас уже есть следующий промис — передаём вызов ему и выходим (а он, если надо, передаст следующему, и так до конца). А если его нет — создаём и назначаем ему обработчики, которые получили в метод, после чего возвращаем уже его. Всё просто.

Теперь доработаем метод setState:

    public void setState(int value) {
        if (this.state > 0) return;
        this.state = value;
        Vector<JSValue> args = new Vector<JSValue>();
        if (result != null) args.add(result);
        if (value == Promise.FULFILLED && next != null) {
            if (onFulfilled.getError() == null) {
                if (result != null && result instanceof Promise) {
                    ((Promise)result).then(next.onFulfilled, next.onRejected);
                    next = (Promise)result;
                } else {
                    result = next.onFulfilled.call(null, args, false);
                }
            } else {
                args = new Vector<JSValue>();
                args.add(onFulfilled.getError().getValue());
                result = next.onRejected.call(null, args, false);
            }
        }
        if (value == Promise.REJECTED && !has_error_handler && next != null) {
            result = next.onRejected.call(null, args, false);
        }
    }

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

Во-вторых — ошибки обрабатываются особым образом. Если успешный результат передаётся по цепочке (видоизменяясь) до конца, то вот возникшая в коде обработчика ошибка — передаётся только на один шаг, до следующего onrejected, либо всплывает наверх, если достигнут конец цепочки.

В-третьих — функции могут вернуть новый промис. В этом случае мы обязаны подменить наш next, если он уже задан, на него (перебросив имеющиеся обработчики). Это, опять же, позволяет сочетать а цепочке обработчики моментального исполнения, и асинхронные — которые сами возвращают Promise.

Вышеприведённый код адресует все эти сценарии.

Первые тесты


    JSParser jp = new JSParser("function cbk(str) { \"Promise fulfilled: \" + str } function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 1500) }");
    System.out.println();
    System.out.println("function cbk(str) { \"Promise fulfilled: \" + str }");
    System.out.println("function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 1500) }");
    System.out.println();
    Expression exp = Expression.create(jp.getHead());
    exp.eval();
    jsparser.Function f = (jsparser.Function)Expression.getVar("f", exp);
    f.setSilent(true);
    jsparser.Promise p = new jsparser.Promise(f);
    p.then((jsparser.Function)Expression.getVar("cbk", exp));

Пока что мы управляем всем со стороны Java кода. Тем не менее, всё уже работает: через полторы секунды мы увидим в консоли надпись «Promise fulfilled: OK». Кстати, наши функции resolve и reject, будучи вызванными из рабочей функции промиса, без чейнинга, могут принимать произвольное число аргументов. Весьма удобно. В этом примере мы передали строку «OK».

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

Пример посложнее:

    JSParser jp = new JSParser("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str } " +
    "function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" } " +
    "function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str } " +
    "function err(str) { \"An error has occured: \" + str } " +
    "function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }");
        System.out.println();
        System.out.println("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str }");
        System.out.println("function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" }");
        System.out.println("function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str }");
        System.out.println("function err(str) { \"An error has occured: \" + str }");
        System.out.println("function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }");
        System.out.println("(new Promise(f)).then(cbk1).then(cbk2).then(cbk3, err)");
        System.out.println();
        Expression exp = Expression.create(jp.getHead());
        ((jsparser.Function)Expression.getVar("f", exp)).setSilent(true);
        ((jsparser.Function)Expression.getVar("cbk2", exp)).setSilent(true);
        exp.eval();
        jsparser.Function f = (jsparser.Function)Expression.getVar("f", exp);
        f.setSilent(true);
        jsparser.Promise p = new jsparser.Promise(f);
        p.then((jsparser.Function)Expression.getVar("cbk1", exp))
                .then((jsparser.Function)Expression.getVar("cbk2", exp))
                .then((jsparser.Function)Expression.getVar("cbk3", exp),
                      (jsparser.Function)Expression.getVar("err", exp));

Вызвав данный пример, мы получим следующий вывод:

{}
"Promise 1 fulfilled: OK"
"OK"
"An error has occured: ERROR"
undefined
"Promise 2 fulfilled: OK"

Первые фигурные скобки — это объект промиса, который нам вернула наша цепочка вызовов then в результате чейнинга. В функции cbk1 мы вернули «OK» — и это значение было передано в cbk2, что мы и видим в последней строке. Внутри cbk2 мы бросаем ошибку со значением «ERROR» — поэтому cbk3 у нас не исполняется, зато исполняется err (как и должно быть при возникновении ошибки в обработчике предыдущего промиса в цепи). Но этот код исполняется моментально, а вот вывод cbk2 осуществляется через вспомогательную функцию, повешенную на таймер. Она имеет доступ к переменной str, как и должна, но её вывод идёт из-за этого ниже. Если исполнить данный пример в Chrome 49, мы получим ровно тот же вывод с одним исключением: переменная str не видна в анонимной функции, переданной в setTimeout. Это особенность поведения стрелочных функций в Хроме (а возможно, так нужно по стандарту, здесь я затрудняюсь сказать, в чём дело). Если поменять стрелочную функцию на обычную — вывод станет идентичным.

Проброс в JavaScript


Но это ещё не всё. Наша конечная цель — чтобы новые возможности мог использовать JS код, исполняемый нашим интерпретатором. Впрочем, это уже дело техники.

Создаём конструктор:

public class PromiseC extends Function {

    public PromiseC() {
        items.put("prototype", PromiseProto.getInstance());
        PromiseProto.getInstance().set("constructor", this);
    }

    @Override
    public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) {
        return call(context, args);
    }

    @Override
    public JSValue call(JSObject context, Vector<JSValue> args) {
        if (args.size() == 0) return new Promise(null);
        if (!args.get(0).getType().equals("Function")) {
            JSError e = new JSError(null, "Type error: argument is not a function", getCaller().getStack());
            getCaller().error = e;
            return new Promise(null);
        }
        return new Promise((Function)args.get(0));
    }

}

И объект-прототип с набором нужных методов:

public class PromiseProto extends JSObject {

    class thenFunction extends Function {
        @Override
        public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) {
            if (args.size() == 1 && args.get(0).getType().equals("Function")) {
                return ((Promise)context).then((Function)args.get(0));
            } else if (args.size() > 1 && args.get(0).getType().equals("Function") &&
                       args.get(1).getType().equals("Function")) {
                return ((Promise)context).then((Function)args.get(0), (Function)args.get(1));
            } else if (args.size() > 1 && args.get(0).getType().equals("null") &&
                       args.get(1).getType().equals("Function")) {
                return ((Promise)context)._catch((Function)args.get(1));
            }
            return context;
        }
    }

    class catchFunction extends Function {
        @Override
        public JSValue call(JSObject context, Vector<JSValue> args, boolean as_constr) {
            if (args.size() > 0 && args.get(0).getType().equals("Function")) {
                return ((Promise)context)._catch((Function)args.get(0));
            }
            return context;
        }
    }

    private PromiseProto() {
        items.put("then", new thenFunction());
        items.put("catch", new catchFunction());
    }

    public static PromiseProto getInstance() {
        if (instance == null) {
            instance = new PromiseProto();
        }
        return instance;
    }

    @Override
    public void set(JSString str, JSValue value) {
        set(str.getValue(), value);
    }

    @Override
    public void set(String str, JSValue value) {
        if (str.equals("constructor")) {
            super.set(str, value);
        }
    }

    @Override
    public String toString() {
        String result = "";
        Set keys = items.keySet();
        Iterator it = keys.iterator();
        while (it.hasNext()) {
            if (result.length() > 0) result += ", ";
            String str = (String)it.next();
            result += str + ": " + items.get(str).toString();
        }
        return "{" + result + "}";
    }

    @Override
    public String getType() {
        return type;
    }

    private String type = "Object";
    private static PromiseProto instance = null;
}

Не забудем добавить в конструктор Promise одну строчку в самом начале, чтобы всё работало:

    public Promise(Function f) {
        items.put("__proto__", PromiseProto.getInstance());
        ...
    }

И поменяем немного наш тест:

    JSParser jp = new JSParser("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str } " +
    "function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" } " +
    "function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str } " +
    "function err(str) { \"An error has occured: \" + str } " +
    "function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }; " +
    "(new Promise(f)).then(cbk1).then(cbk2).then(cbk3, err)");
        System.out.println();
        System.out.println("function cbk1(str) { \"Promise 1 fulfilled: \" + str; return str }");
        System.out.println("function cbk2(str) { setTimeout(str => { \"Promise 2 fulfilled: \" + str }, 1000); throw \"ERROR\" }");
        System.out.println("function cbk3(str) { \"Promise 3 fulfilled: \" + str; return str }");
        System.out.println("function err(str) { \"An error has occured: \" + str }");
        System.out.println("function f(resolve, reject) { setTimeout(function() { resolve(\"OK\") }, 300) }");
        System.out.println("(new Promise(f)).then(cbk1).then(cbk2).then(cbk3, err)");
        System.out.println();
        Expression exp = Expression.create(jp.getHead());
        ((jsparser.Function)Expression.getVar("f", exp)).setSilent(true);
        ((jsparser.Function)Expression.getVar("cbk2", exp)).setSilent(true);
        exp.eval();
        jsparser.Function f = (jsparser.Function)Expression.getVar("f", exp);
        f.setSilent(true);

Вывод не должен измениться.

На этом всё! Всё отлично работает, можно писать дополнительные юнит-тесты и искать возможные ошибки.

Как приспособить этот механизм для Java? Очень просто. Создаём класс, аналогичный нашему Function, который что-то делает в методе operate. И оборачиваем уже его в нашу обёртку. В любом случае, на этот счёт есть много замечательных паттернов, с которыми можно поиграться.

Надеюсь, данная статья была кому-то полезна. Исходники движка я обязательно выложу, как только доведу их до ума и добавлю недостающий функционал. Удачного дня!

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


  1. mayorovp
    05.03.2018 11:00
    +1

    Что-то я не понимаю...


    Вот у нас внутри PromiseHandleWrapper написано следующее:


        JSValue result = func.call(context, args, false);
        promise.setResult(result);

    Какое отношение результат вызова функции, переданной в then, имеет к значению, хранящемуся в обещании? Разве тут не должно быть наоборот — значение из обещания должно быть передано в onFulfilled единственным параметром?


    Опять же, реализация метода then какая-то странная. Вы вообще в курсе, что в Javascript у одного и того же обещания можно вызвать метод then два раза, и это не будет цепочкой?


    const p = Promise.resolve(5);
    p.then(x => (console.log(x), 10)); // выведет 5
    p.then(x => (console.log(x), 10)); // выведет опять 5
    p.then(x => (console.log(x), 10)).then(x => (console.log(x), 10)) // выведет сначала 5, а потом 10

    Кстати, куда делось требование 2.2.4 стандарта Promises/A+ "onFulfilled or onRejected must not be called until the execution context stack contains only platform code", оно же пункт 25.4.1.8 стандарта ECMAScript 2015:


    The abstract operation TriggerPromiseReactions takes a collection of PromiseReactionRecords and enqueues a new Job for each record. Each such Job processes the [[Handler]] of the PromiseReactionRecord, and if the [[Handler]] is a function calls it passing the given argument. [...]


    1. popov654 Автор
      05.03.2018 15:48

      Какое отношение результат вызова функции, переданной в then, имеет к значению, хранящемуся в обещании? Разве тут не должно быть наоборот — значение из обещания должно быть передано в onFulfilled единственным параметром?

      Нет, не должно. Вы всё несколько путаете. Значение из onFulfilled должно пойти в следующий onFulfilled. А для этого оно передаётся в текущее (не следующее!) обещание. Это как промежуточное звено. Если Вы про самое первое обещание — там не нужно задействовать поле result, поскольку внутри «рабочей» (как я её условно назвал) функции, переданной в конструктор, resolve и так будет вызван с любыми нужными аргументами напрямую (а это и есть onFulfilled).

      Кстати, куда делось требование 2.2.4 стандарта Promises/A+ «onFulfilled or onRejected must not be called until the execution context stack contains only platform code», оно же пункт 25.4.1.8 стандарта ECMAScript 2015

      Признаться, полностью стандарт я не изучил :) А Вы не расскажете, почему существует такое требование, и что будет, если его проигнорировать? Про очередь работ (jobs) я мельком читал, но это ведь детали реализации, как именно мы всё это сделаем. Я хочу сказать, что это можно делегировать на отдельный уровень абстракции: создать планировщик потоков, который будет управлять background задачами, в том числе и этими задачами для промисов, а заодно и воркерами, и другими вещами. Пока что ничего этого мой движок не умеет — так к чему заморачиваться. Ведь результат исполнения кода сходится? Детали реализации могут варьироваться, главное, что поведение предсказуемо.


      1. mayorovp
        05.03.2018 16:26
        +1

        Тут проблема в гонках. Текущий стандарт гарантирует, что вот такой код выполнится корректно независимо от состояния обещаний:


        class Foo {
            constructor() {
                this.a = 5;
                this.b = bar().then(x => this.baz());
                this.c = 10;
            }
        }

        А если вы вызываете onFulfilled синхронно — то продолжение может выполниться раньше чем объект полностью проинициализируется. Причем такой баг может спать в коде очень долго!


        Детали реализации могут варьироваться, главное, что поведение предсказуемо.

        Но отличается от предписанного стандартом.


        1. popov654 Автор
          05.03.2018 16:42
          -1

          Спасибо, теперь более-менее понятно)

          Хотя всё-таки не очень понимаю, какой практический смысл ставить обещания в конструктор. Ну да ладно, не мне судить, программист волен писать любой код, который считает нужным.

          Давайте просто зайдём с другой стороны: у нас есть функция. Каждая функция в JavaScript (независимо от редакции стандарта) — это объект. Функция может быть вызвана как конструктор. При таком вызове, в числе прочего, у неё принудительно устанавливается this равным объекту, который она вернёт. Сам этот объект создаётся интерпретатором до её запуска (и отдаётся сразу после).

          Я просто пытаюсь понять, где здесь гонка: если программист написал в конструкторе обещание, которое исполнится раньше, чем в коде конструктора ниже будет присвоено некое поле объекта — у него в коде обработчика обещания выкинет ошибку Reference Error, и он сам себе злобный Буратино :) Что до объекта this — так он доступен в конструкторе, а значит, доступен и в стрелочной функции в Вашем примере (и будет доступен в обычной, если использовать bind).


          1. mayorovp
            05.03.2018 16:46

            Проблема в том, что обещание зависит от внешнего кода. И если 99,999% случаев оно выполняется с задержкой, а в 0,001% случаев — сразу же, то этот случай программист в отладчике не увидит. И будет рассказывать сказки про кривой браузер.

            Поэтому требование асинхронности во всех случаях ввели в стандарт. А раз оно попало в стандарт — программист уже имеет право на него полагаться.


            1. popov654 Автор
              05.03.2018 18:02

              А что мы понимаем под «требованием асинхронности»? Я всегда понимал асинхронность как работу параллельно, в отдельном потоке. Я так полагаю, здесь речь идёт о том, чтобы завершить весь текущий JS код, и только потом запускать очередь промисов? Но это же весьма печально: промисы отработают гораздо позже, чем могли бы (они ведь вообще на внешний код могут быть не завязаны, например, просто получаем что-то с сервера, парсим, выводим на страницу).


              1. mayorovp
                05.03.2018 18:48

                Нет, асинхронность — это выполнение отдельно от основного (синхронного) кода, и не важно в каком потоке.


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

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


                1. popov654 Автор
                  05.03.2018 19:01

                  Почему не предназначен? Если мы говорим про синхронный код — это например все вычисления, которые стартуют на onload веб-страницы (не знаю уж, в каком потоке их выполняют современные движки). И там может быть довольно приличное количество кода. В том числе довольно тяжёлого (например, код, получающий значения ширины и высоты изображений при их загрузке, чтобы что-то корректно рассчитать, пример из реального проекта). Да, код, повешенный на обработчики — уже асинхронный, но по факту всё это всё равно существенно нагружает браузер и он может подлагивать. Если есть возможность что-то исполнить раньше, чтобы потом у пользователя ничего не лагало, например, при прокрутке — разве не стоит этим воспользоваться?


                  1. mayorovp
                    05.03.2018 19:07

                    Для того чтобы что-то исполнить раньше — надо что-то исполнить позже! Почему вы отдаете приоритет одному коду перед другим — и считаете что это уменьшит лаги?


                    Чтобы код не лагал — он должен успевать выполняться за некоторое время. И это время не зависит от того в каком порядке будет выполняться код.


                    1. popov654 Автор
                      05.03.2018 19:11

                      Да, согласен. Наверное, в той ситуации дело было вообще не в JS коде. Скорее, в неудачном планировании браузером порядка выполнения операций (слишком много всего в одном потоке делалось, либо разные потоки плохо между собой взаимодействовали с точки зрения внутреннего планировщика). Тем более, дело было на Opera 11.64, на Chrome (особенно версий 40+) практически не тормозило ничего на том сайте, про который я вспоминаю.


            1. popov654 Автор
              06.03.2018 11:27

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

              Кстати, мне сегодня в голову пришла очень интересная идея. Мне кажется, главная причина требования исполнения всех промисов после того, как отработает весь синхронный код — это не дать возможность писать вот такой вот сомнительный код. Проблема в другом, я сегодня при модификации кода как раз на неё наткнулся :)

              Смотрите, допустим мы создали промис, вызвали метод then. Он добавляет в вектор потомков новый промис (у нас же дерево). И после этого, если у нас состояние уже fulfilled (например, мы вызывали Promise.resolve()) — мы должны сразу его запустить. И в этом случае, даже если мы выполним добавление в вектор до запуска — у нас потом может идти вызов then через чейнинг уже для того нового промиса. И проблема в том, что он отработает раньше, чем туда что-то будет добавлено. И если в его обработчике onFulfilled произойдёт ошибка, то она не попадёт в обработчик onRejected следующего промиса, потому что следующего у него ещё просто нет (вектор потомков пуст). И она всплывёт наверх.

              Иными словам, необходимо дать доработать цепочке вызовов then до конца. А это синхронный код. Если этого не сделать — будут не очень приятные последствия.

              Вообще, эту проблему можно обойти, вставив в код интерпретатора специальный костыль для выявления этого сценария, но стандарт решает её изящнее и радикальнее.


              1. mayorovp
                06.03.2018 11:38
                +1

                Нет, вот тут вы ошибаетесь. Или путаете свою кривую реализацию со стандартом. Промисы специально так сделаны, чтобы не было никакой разницы что произошло раньше — резолв промиса или подписка на него методом then!

                Ошибка в обработчике никогда не выплывает за пределы обещания.


                1. popov654 Автор
                  06.03.2018 12:03

                  Так я ровно это же и написал выше. С чем Вы спорите?
                  Или я не прав, что then — это часть синхронного кода, который исполняется в первую очередь?

                  Да, resolve имеет право произойти раньше, но поведение должно быть предсказуемым. А если бы этого требования в стандарте не было, то поведение могло бы динамически меняться в зависимости от времени исполнения обработчика промиса (я специально привёл пример с ошибкой внутри обработчика для большей наглядности).


                1. popov654 Автор
                  06.03.2018 12:06

                  То есть — не выплывает? Ещё как выплывает, если нет обработчика onRejected дальше в цепи. Об этом же ясно в стандарте сказано. Да и в консоли это легко проверить


                  1. faiwer
                    06.03.2018 12:38
                    +1


                    Что мы видим? Мы видим 'after', что свидетельствует о том, что строка с resolve().then выполнилась без ошибки. Видим, что наш throw 1 не был никем пойман (uncaught).


                    Ошибка в обработчике никогда не выплывает за пределы обещания

                    ЧИТД. Всплыла? Нет. Та же самая картинка если мы швыряем ошибку прямо в new Promise(() => { /* тут */ }.


              1. faiwer
                06.03.2018 11:53
                +1

                Мне кажется, главная причина требования исполнения всех промисов после того, как отработает весь синхронный код

                Нет. Главная причина очевидна: унификация. Либо всё синхронно, либо всё асинхронно. Естественно выбрали второе (javascript же). Смешанный вариант чрезвычайно багонеустойчив. Уж поверьте, node-js разработчики наелись таких багов просто до отвалу.


                Это когда код ломается в тех самых редких случаях, когда он решил выполниться синхронно (какой-нибудь флаг, наличие кеша, да что угодно), а код принимающий на такое рассчитан не был. Состояние гонки возможно даже в однопоточном коде. node-разработчики это хорошо знают. Причём в разных вариантах.


                1. popov654 Автор
                  06.03.2018 12:05

                  А можно примеры какие-то привести? Ну вот чтобы на пальцах (я Node.JS не знаю). Просто то, что Вы написали выше с конструктором — это вообще не гонка, это просто глупость со стороны JS программиста.


                  1. faiwer
                    06.03.2018 12:30
                    +1

                    А можно примеры какие-то привести?

                    const a = {};
                    api(result => { a.inner.result = result; };
                    a.inner = anySyncCode();

                    Если api выполнится синхронно всё упадёт. Если асинхронно — не упадёт. И если это зависит от каких-нибудь хитрых неочевидных моментов (да ещё и закопано где-нибудь в глубине stack-trace-а), то выстрельнуть оно может совершенно неожиданно. Может, к примеру, приложение на обе лопатки положить. И анализ stack-trace-а вам особо не поможет, ибо он будет коротким и бесполезным. Здравствуйте часы дебага асинхронного nodejs кода. Бррр.


                    В данном случае (в примере) очевидно, что можно переписать так, чтобы не падало в любой ситуации. В реальном же коде всё бывает сильно запутанно.


                    это просто глупость со стороны JS программиста

                    Я думаю, что мы тут все не особо умные и совершаем ошибки. А ещё ошибки совершают те, чей код мы используем. Особенно если это какой-нибудь вложенный во вложенный во вложенный npm-пакет. Я бы вообще, на вашем месте, да и на своём тоже, воздержался от таких высказываний. Не боги горшки обжигают. Тут порой такую дичь напишешь, особенно если в спешке.


                    1. popov654 Автор
                      06.03.2018 12:40

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

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

                      И я нигде не пытался сказать, что лучше смешанный вариант, как писали выше — что то так работает, то этак.

                      Просто мне видится, что проблема с then-ами, которые не успели назначить обработчики ошибок до того, как ошибка случилась — стали основной причиной, почему в стандарт включили это требование. Можно правда было облегчить его до «не начинать исполнение промисов до завершения обработки цепочки then». Но в итоге решили отложить исполнение всех промисов совсем, пока весь код не отработает. В принципе — наверное, это плюс, then от существующих promise-объектов могут вызываться и ниже в текущей функции, и даже где-нибудь потом, за её пределами. Опять же, это подтверждает верность моей мысли: мы не начинаем исполнять промисы не потому, что нам как авторам стандарта или движка так приспичило, а потому, что мы хотим убедиться, что всё дерево сформировано и на момент начала исполнения не подвергается изменениям.


                      1. faiwer
                        06.03.2018 12:53

                        если бы знал, что в ряде случаев та строчка может выполниться синхронно

                        Не знали бы. Это бы для вас стало открытием. После 3-х часов дебага.


                        не начинать исполнение промисов до завершения обработки цепочки then

                        Вы похоже до сих пор не поняли что такое Promise-ы :) Нет такой стадии "завершение обработки". И быть не может. Точнее может: когда сборщик мусора виртуальной машины выкинет их из памяти. А до тех пор можно повешать свой .then в любую часть цепочку (лишь ссылка туда была). Опять же, это не цепочки. Это деревья. Другая структура данных.


                        в итоге решили отложить исполнение всех промисов совсем, пока весь код не отработает

                        что всё дерево сформировано

                        Это невозможно. Нет такой стадии "сформировано". Никто кроме разработчика не знает когда "весь код отработает". Цепочка начинает отрабатывать просто в рамках event loop. А .then на неё могут вешаться когда угодно и как угодно. Может асинхронно. then может быть повешан после выполнения всего, может в процессе, может до.


                        1. popov654 Автор
                          06.03.2018 13:07

                          Опять же, это не цепочки. Это деревья. Другая структура данных.

                          Да я это уже понял. Я цепочку вызовов через точку имел в виду, единую строку кода как выражение)

                          Нет такой стадии «завершение обработки». И быть не может.

                          Опять же, я писал про завершение интерпретации строки кода с цепочкой вызовов then. такая стадия — не только может быть, но и есть, и на ней можно даже что-то сделать (если мы не говорим про JIT компиляцию, у меня всё-таки интерпретатор обычный).

                          Цепочка начинает отрабатывать просто в рамках event loop

                          Поскольку в моём движке пока нет event loop — то это будет просто отдельная стадия, которая вызовется, когда весь скрипт исполнен :)

                          А .then на неё могут вешаться когда угодно и как угодно. Может асинхронно. then может быть повешан после выполнения всего, может в процессе, может до.

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

                          then может быть повешан после выполнения всего, может в процессе, может до.

                          Так в том-то и дело: он может быть вызван до, но исполнение начинать ещё нельзя. То есть мы делим нашу интерпретацию на две стадии: основной поток исполнения, и потом исполнение промисов. Если я правильно понял вообще тот пункт из стандарта в том виде, в котором его привели… Есть кстати ещё таймеры, которые в отдельном потоке исполняются, и обработчики событий (в других движках). Но эти потоки работают параллельно с основным. А вот поток исполнения очереди промисов — как я понял, ждёт завершения работы основного потока. Поправьте, если я не прав.


                          1. faiwer
                            06.03.2018 13:13

                            но если это норма — тем лучше.

                            Да. Это норма. Промисы хороши тем, что нет резона суетиться по поводу его статуса. Он если данные есть уже готовые отработает в следующем тике. Если нет, то когда появятся. В итоге мы полностью отвязаны от внутренней кухни того, что там в этом промисе происходит. Оно просто работает. Как ему там взбрендится. А мы полагаемся на задокументированное поведение и не боимся, что оно в какой-то момент подкинет сюрприз.


                            В nodeJS мы не может взять и застопорить весь поток (точнее можем, но за это больно бьют, здесь так нельзя). И ситуация "вчера это могло работать синхронно" легко превращается в "ох, у меня тут теперь сплошные await-ы". Если изначально предполагать такую возможность и всё покрыть promise-like-кодом, то всё будет работать как часы несмотря на внутреннюю реализацию.


                          1. mayorovp
                            06.03.2018 13:17

                            Javascript — пока что принципиально однопоточный. Второй поток при поддержке рантайма создать можно — но только в отдельном реалме (т.е. в нем будет свой глобальный объект, свои системные объекты и т.п.)


                            1. popov654 Автор
                              06.03.2018 14:14

                              Если применить супер-умный оптимизатор с распараллеливанием тех функций, которые не завязаны друг на друга — то теоретически, должно быть можно. Безопасность же при работе с данными можно обеспечить на уровне мониторов и synchronized блоков, если мы про Java, или использовать всякие семафоры и мьютексы, если мы пишем на C/C++.

                              Кроме того, Вы ведь сейчас говорите про нативные потоки? А я про логические. Попробуйте в отладчике поставить точку остановки в какой-то функции, и ещё одну в функции, повешенной на таймаут, скажем, в 1000-1500 мс. В итоге Вас прямо в процессе отладки перекинет совсем в другое место, там будет другая функция и другой стек вызовов. Вызов таких отложенных функций, как и вызов хендлеров событий, движок производит время от времени, в некоторые слайсы времени, и это определяет его внутренний планировщик. Можно называть это event-loop, а я для простоты называю это отдельными потоками.

                              На самом деле, в браузере всё ещё веселее — там «основного» потока нет. Потому что то, что происходит при загрузке страницы — это код, вызванный из обработчика события onload. Это в моём движке есть некий «основной» поток, исполняющий код, переданный на вход :)


                              1. mayorovp
                                06.03.2018 14:18

                                Попробуйте в отладчике поставить точку остановки в какой-то функции, и ещё одну в функции, повешенной на таймаут, скажем, в 1000-1500 мс. В итоге Вас прямо в процессе отладки перекинет совсем в другое место, там будет другая функция и другой стек вызовов.

                                Вот как раз этого в Javascript не бывает. Новый Job не может начать выполняться пока старый не закончился.


    1. popov654 Автор
      05.03.2018 15:56

      Опять же, реализация метода then какая-то странная. Вы вообще в курсе, что в Javascript у одного и того же обещания можно вызвать метод then два раза, и это не будет цепочкой?

      Ваш пример нисколько не противоречит статье. Смотрите, Вы создали промис (без основной функции и вообще без new, через Promise.resolve). Он у вас сразу перешёл в fulfilled
      состояние. Вы вызвали then — ваша функция сразу исполнилась, а состояние промиса не поменялось. Вы снова вызвали then — но состояние у нас всё ещё fulfilled, и поток исполнения опять пойдёт в первый if. Вот если бы у нас в этом случае создался новый промис в цепочке -тогда было бы плохо, так как у него сразу же вызвался бы метод then, но состояние так и зависло бы в pending.

      А Ваш последний пример — это и есть чейнинг. Вы передали результат из первого onFulfilled во второй. Что и требуется по стандарту.


      1. mayorovp
        05.03.2018 16:31

        А Ваш последний пример — это и есть чейнинг. Вы передали результат из первого onFulfilled во второй. Что и требуется по стандарту.

        Нет! Вы же возвращаете this — а значит, никакого чайнинга не получится, четвертый вызов then пойдет по тому же самому пути что и первые три.


        Более того, у вас все 4 вызова выведут на консоль undefined — потому что значение, которое было передано в resolve, нигде не используется...


        1. popov654 Автор
          05.03.2018 16:51
          -1

          А, всё, всё, понял.

          Окей, в целом согласен. Претензия значит была не к тому, что у меня чейнинг не к месту, а наоборот, к тому, что он не работает, где должен работать :) Но:

          1) В примере выше Вы передаёте моментально исполняющиеся функции. При передаче первой же функции, которая вернёт Promise сама, чейнинг начнёт работать. В целом если наши функции исполняются моментально, а состояние у нас fulfilled — то какая разница, делать чейнинг или не делать?
          2) Да, и это довольно печально. С другой стороны, так вышло ровно потому, что в момент вызова resolve у нас был стандартный хендлер, а ему пофиг на переданные аргументы. Он поменял состояние объекта и всё. Потом мы вызвали then, установили новый хендлер, но аргумент уже потерян. Не знаю, как часто в жизни используется такой код, делающий моментальный resolve, но стандарт здесь явно нарушен, да.


          1. mayorovp
            05.03.2018 17:11

            Замените Promise.resolve(5) на new Promise(resolve => setTimeout(resolve, 0, 5)) — тоже будет проблема.


            Не знаю, как часто в жизни используется такой код, делающий моментальный resolve, но стандарт здесь явно нарушен, да.

            Очень часто. Это один из паттернов работы с обещаниями.


            Кроме того, не забывайте, у вас есть вот такой код:


             ((Promise)result).then(next.onFulfilled, next.onRejected);

            В этот момент result запросто может быть уже выполнено, и у вас будет та же проблема.


            1. popov654 Автор
              05.03.2018 17:51

              Замените Promise.resolve(5) на new Promise(resolve => setTimeout(resolve, 0, 5)) — тоже будет проблема.

              Вы про то, что задержка нулевая? А, ну в этом плане да. И правда баг очень глупый. Но хотя бы обработчик будет выполнен))


            1. popov654 Автор
              05.03.2018 17:52

              А смысл в таком паттерне? Вроде обещания же как раз для того и созданы, чтобы что-то запустить, и вызвать resolve по завершении операции, если я верно понял идею.


              1. mayorovp
                05.03.2018 18:51

                Смысл в том, чтобы протестировать другую ветку кода. Вместо 0 можно записать любое другое число — от этого ничего не должно поменяться, кроме времени выполнения теста.


                Должно быть 5, 5, 5, 10 с точностью до порядка. А получится фигня какая-то.


                1. popov654 Автор
                  05.03.2018 19:02

                  Вы про Java код из моего примера или про JS код? Я спрашивал, когда в реальной жизни может понадобиться создать промис, и сразу перевести его в resolved состояние)


                  1. mayorovp
                    05.03.2018 19:11

                    А, вы про это. Я говорил про ваш код. Он выполнит продолжения с неправильными аргументами.


                    Что же до Promise.resolve(...) — он часто используется для того чтобы отловить синхронную ошибку и обработать ее так же как асинхронную:


                    // Неправильно
                    foo()
                        .then(bar)
                        .catch(baz); // Поймает ошибку в bar - но не поймает в foo!
                    
                    // Правильно
                    Promise.resolve()
                        .then(foo)
                        .then(bar)
                        .catch(baz); // Поймает все ошибки


                    1. popov654 Автор
                      05.03.2018 19:22

                      Почему первый вариант не поймает ошибку? По стандарту если встречена ошибка, движок идёт до первого onRejected обработчика в цепочке. Кстати, только что проверил — отлично всё ловит.

                      new Promise(function() { throw "Error"; })
                            .catch(function(e) { console.log(e) })
                      


                      1. faiwer
                        05.03.2018 19:45
                        +1

                        Я думаю, что имелось ввиду, что если foo внутри себя имеет где-нибудь return promise (от которого потом можно сделать .then()), но упадёт ещё в процессе, то никакой catch вызван не будет. Почему не будет? А почему должен? Ведь вызов foo() это просто вызов какой-то функции. Она может вообще к promise-ам никакого отношения не иметь. А может иметь. А может упасть в процессе выполнения.


                        А вот положенные внутрь .then() методы выполняются внутри try catch и их ошибки уже отлавливаются.


                        1. popov654 Автор
                          05.03.2018 20:18

                          А, господи, опять туплю. Ну да, тогда всё верно. Но кто заставляет писать foo().then(), когда можно (и нужно) писать new Promise(foo).then()?


                          1. mayorovp
                            05.03.2018 20:26

                            Нельзя вызвать foo() как new Promise(foo) если foo — это обычная асинхронная функция которая возвращает обещание.


                            А вот как Promise.resolve().then(foo) ее вызвать можно!


                            1. popov654 Автор
                              05.03.2018 20:46

                              А какие функции мы считаем асинхронными? Просто официально задекларированный и рекомендованный способ создания промиса (во всяком случае, в упомянутой статье Кантора) — через new Promise(func)


                              1. faiwer
                                05.03.2018 21:10

                                Для new Promise(fn) сигнатура: fn(resolve, reject). В то время как в .then(fn) можно передать любую вообще функцию. И сигнатура у неё уже будет fn(resultOfPrev), никаких resolve и reject. Точнее там даже .then(successFn, failureFn). И если наша fn не может быть выполнена синхронно, то она должна вернуть новый promise, который она должна создать уже самостоятельно и вернуть его в return. Если эта наша fn является async fn, то этим займётся сам интерпретатор языка. Помимо прочего, если нам не нужно делать никаких асинхронных запросов, то мы можем сразу в этой fn вернуть нужный результат. Например:


                                .then(prev =>
                                {
                                  if(some(prev))
                                    return 42;
                                  else return new Promise((resolve, reject) => { /* ... */ });
                                })

                                Следующая функция в цепочке-древе получит либо 42 либо результат от new Promise. Такая вот автоматика. Удобно.


                                1. popov654 Автор
                                  05.03.2018 21:34

                                  И если наша fn не может быть выполнена синхронно, то она должна вернуть новый promise, который она должна создать уже самостоятельно и вернуть его в return

                                  Ну да, я про это знаю) Всё правильно.

                                  Если эта наша fn является async fn, то этим займётся сам интерпретатор языка.

                                  А вот про эту возможность не знал, надо будет почитать, что это и как оно работает.


                              1. mayorovp
                                05.03.2018 21:12
                                +1

                                Асинхронная функция в широком смысле — это любая функция которая совершает асинхронную операцию.


                                В более узком смысле асинхронная функция — это функция которая возвращает обещание.


                                Конструктор new Promise предназначен для создания базовых асинхронных функций или для перехода от кода на колбеках к коду на обещаниях — т.н. "промисификации".


                                Составные же асинхронные функции так не делаются. Вместо этого используется один из двух подходов:


                                1. создание цепочек методами .then и .catch;
                                2. использование механизма async/await.


                                1. popov654 Автор
                                  05.03.2018 21:36

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


                                  1. faiwer
                                    05.03.2018 21:44
                                    +1

                                    Ну вот кстати про этот механизм в той же статье было сказано, что он потерял свою актуальность

                                    Какой? async-await oO? Скорее вытеснил ручную работу с promise на далёкие окраины.


                                    1. popov654 Автор
                                      05.03.2018 22:26

                                      Но он ведь появился, вроде как, намного раньше. Я упоминания о нём ещё в статьях за январь 2014-ого видел. Разве не?

                                      Насчёт цитаты — я перепутал, там сравнение с генераторами было, а не с промисами :)


                                      1. mayorovp
                                        05.03.2018 22:31

                                        async-await основан на обещаниях, он никак не мог появиться раньше!

                                        Кстати, обещания в библиотеках существуют как минимум с 2010го года.


                                        1. popov654 Автор
                                          05.03.2018 22:46

                                          Ну, мы же не про библиотеки говорим, а про ES6, который в браузерах только к концу 2015-ого появился толком, если не в начале 2016-ого (по Хрому если смотреть). Правда, отдельные фичи работали и раньше в виде экспериментов :)


                              1. faiwer
                                05.03.2018 21:12

                                Возможно вам поможет эта старая статья.


                                1. popov654 Автор
                                  05.03.2018 22:47

                                  Спасибо, отличный текст) А у меня вот вопрос возник. В тексте есть следующая цитата:

                                  Мы встретили здесь новый метод — Promise.resolve. Он создает промис, который будет выполнен в любом случае, вне зависимости от значений, ему переданных. Если вы передадите ему нечто промисообразное (имеющее метод then), будет создан новый промис, который выполнится или будет отклонен, так же, как и начальный промис.

                                  Означает ли это, что движок должен непрерывно мониторить состояние такого объекта — ведь неизвестно, когда оно может смениться, если оно на момент передачи pending?


                                  1. mayorovp
                                    06.03.2018 05:46

                                    Нет. Там просто вызывается метод then и все.


                                    1. popov654 Автор
                                      06.03.2018 10:01

                                      А конечный стейт мы как узнаем? Промисообразный объект имеет метод then, но не имеет никакого API для того, чтобы узнать его состояние. Что-то с этой выдержкой явно не так, имхо.


                                      1. mayorovp
                                        06.03.2018 11:39

                                        А зачем нам вдруг чужой стейт-то? Зачем он вообще нужен?


                                1. popov654 Автор
                                  05.03.2018 23:09

                                  И ещё:

                                  Когда мы прерываем выполнение промиса, spawn ждет выполнения промиса и возвращает окончательное значение. Если промис отклонен, точка выхода (yield) выбрасывает исключение, которое мы можем поймать в блоке try/catch

                                  Возможно, кривой перевод, но я здесь практически не понял ничего в этом абзаце. yield вернёт промис, потому что за ним стоит вызов функции, возвращающей промис. Каким образом он кинет нам исключение? Имеется в виду то исключение, что возникло в функции промиса getJSON и не было поймано там же? Если так, то да, но имхо если мы хотим подробный вывод ошибки в консоль, логичнее такое ловить на месте. Хотя стек вызова мы и так получим)


                          1. faiwer
                            05.03.2018 20:29

                            Мне кажется, что 99.9% пишут как раз foo().then(). Проще и нагляднее, а try-catch уровнем выше какой-нибудь итак лежит. Но да, когда-нибудь кому-нибудь это сильно аукнется (и я буду в первых рядах :D). Кстати, если foo сделан через async, то проблемы нет. Сейчас async это скорее правило, нежели исключение (имхо).


                            Проблема обычно именно с тем чтобы отловить ошибки именно в отложенных сценариях, а не в синхронных. Например какое-нибудь не promise-api какой-нибудь библиотеки с callback-ом. Это когда нотация для callback-а такая: fn(error, result). И вот если ошибка не была в той библиотеке отловлена и помещена в этот error, то ошибка просто падает и никем не отлавливается. А отлавливаются только те ошибки, которые автором библиотеки были отловлены целенаправленно. Но он же (автор), наверное, не идеален…


                            А вот стандартно написанный код поверх async-await от таких проблем обычно спасает. Ты что-то протупил, непредусмотрел, но т.к. try-catch "из коробки", а проброс ошибок встроен в promise-систему, то трагедии не будет.


    1. popov654 Автор
      05.03.2018 16:07

      Я кстати наконец понял, чем Вам не понравился подход со значением. Да, можно было бы передать значение не в текущий промис, а в следующий. А забирать его перед вызовом JS функции-обработчика внутри обёртки. Я согласен, что с точки зрения логики так изящнее. Но с точки зрения реализации — на мой взгляд, сложнее. Просто сейчас решение о вызове следующего промиса и передаче ему значения принимает метод setResult. Это несколько не свойственный ему функционал, и это плохо. С другой стороны, обёртка избавлена от необходимости явным образом получать какие-то аргументы (тем более в случае первого промиса в цепи она получает эти аргументы естественным образом автоматически, и городить там две логики, либо как-то разделять обёртки-обработчики на обработчики «головного» и «не головного» помиса, тоже плохо).

      Но самое главное, чем мне кажется текущий подход лучше — это тем, что следующего промиса в цепочке может просто не быть. А формальное значение всегда пригодится для интерпретатора, чтобы куда-нибудь его вывести (так же, как сейчас для блока кода return value — это ни что иное как return value последнего исполненного выражения в нём). То есть результатом работы промиса (его значением) будем считать результат работы сработавшего хэндлера, как-то так.


    1. popov654 Автор
      05.03.2018 16:22

      Кстати, мы оба оказались не правы. В первой части Вашего примера тоже чейнинг должен работать:

      Скриншот




      1. mayorovp
        05.03.2018 16:32

        Почему это неправы оказали мы оба? Вы правда думаете что я свой пример в консоль Хрома не копировал? :-)


        1. popov654 Автор
          05.03.2018 16:43

          Вы вообще в курсе, что в Javascript у одного и того же обещания можно вызвать метод then два раза, и это не будет цепочкой?

          Но подождите, если Вы его копировали, Вы же должны были видеть, что создаётся новый промис. Что это по-вашему, если не цепочка?


          1. mayorovp
            05.03.2018 16:47

            Это две цепочки. Более строго, ожидающие обещания образуют не цепочки, а деревья.


            1. popov654 Автор
              05.03.2018 16:52

              Эм, серьёзно? Зачем?..
              С точки зрения кода — это ведь линейная цепь вызовов then и catch.


              1. mayorovp
                05.03.2018 17:13
                +1

                Где вы видите тут линейную цепь вызовов?


                p.then(... /* 1 */);
                p.then(... /* 2 */).then(... /* 3 */);
                p.then(... /* 4 */);

                Порядок вызовов важен! Значение p должно попасть в обработчики 1, 2 и 4, значение которое вернул обработчик 2 — должно попасть в обработчик 3.


                1. popov654 Автор
                  05.03.2018 17:47

                  А, вон оно что… Окей, теперь понял, как правильно. Спасибо.

                  Но как по мне — это излишнее усложнение… Можно было сделать принудительное приведение к линейному списку, как у меня. Тем более, в статье Ильи Кантора, которую я читал (она правда совсем для новичков), этот момент не был освещён. Надеюсь, хоть автор про него знал :)


                  1. faiwer
                    05.03.2018 18:45
                    +1

                    Но как по мне — это излишнее усложнение… Можно было сделать принудительное приведение к линейному списку, как у меня

                    Ну и кому они были бы нужны в таком виде? Поверьте и то, что .then создаёт деревья, а не продолжение списка, и то, что оно всегда отрабатывает асинхронно ? очень правильные архитектурные решения. Вы это достаточно быстро поймёте если будете писать много promise-js кода.


                  1. mayorovp
                    05.03.2018 18:59

                    Ваше "упрощение" нарушает абстракцию.


                    Смысл обещаний — в том, что они ведут себя как значения: их можно передавать из метода в метод, сохранять где-нибудь, или забывать. Важно, что чтобы ни происходило с обещанием — на него это уже никак не повлияет, если есть обещание — всегда можно вызвать у него метод then и асинхронно получить лежащее в нем значение, независимо от того сколько раз этот метод уже вызывался.


                  1. mayorovp
                    05.03.2018 19:17

                    Кстати, по поводу статей Ильи Кантора и подобных… Они хороши для тех кто использует обещания, а не для тех кто их делает.


                    Первую же свою реализацию обещаний проще всего писать непосредственно по спецификации: http://www.ecma-international.org/ecma-262/6.0/#sec-promise-constructor


                    Там уже описаны все структуры данных и все алгоритмы над ними, надо лишь аккуратно перенести это все в код.


  1. mayorovp
    05.03.2018 11:02
    +3

    Что же до приспособления этого механизма для Java — в Java давно уже есть CompletableFuture


    1. gkislin
      06.03.2018 00:22

      Честно, тоже ожидал это в статье встретить. Ждем комментария автора.


    1. asm0dey
      07.03.2018 09:41
      +2

      Только ради этого комментария я зашёл в статью.
      Могу ещё добавить что до 8й джавы была существовала Google Guava, в которой есть ListenableFuture со схожей семантикой.


  1. Enverest
    05.03.2018 15:57
    +1

    На сколько мне известно, то что в Джаваскрипте упрощённо называют промисами — в остальных языках программирования разделяют на Promise и Future. В Джаве есть реализация Future — это CompletableFuture.