Всем доброго времени суток. Сегодня я хочу рассказать о том, как писал реализацию механизма промисов для своего JS движка. Как известно, не так давно вышел новый стандарт ECMA Script 6, и концепция промисов выглядит довольно интересно, а также уже очень много где применяется веб-разработчиками. Поэтому для любого современного JS движка это, безусловно, must-have вещь.
Внимание: в статье довольно много кода. Код не претендует на красоту и высокое качество, поскольку весь проект писался одним человеком и всё ещё находится в бете. Цель данного повествования — показать, как же всё работает под капотом. Кроме того, после небольшой адаптации данный код можно использовать для создания проектов чисто на Java, без оглядки на JavaScript.
Первое, с чего стоило начать написание кода — это с изучения того, как всё должно работать в итоге. Архитектура получившегося модуля во многом определялась по ходу процесса.
Promise — это специальный объект, который при создании находится в состоянии pending (пусть это будет константа равная 0).
Далее объект начинает исполнять функцию, которая была передана в его конструктор при создании. Если функция не была передана — следуя стандарту ES6, мы должны бросить исключение argument is not a function. Однако в нашей Java реализации можно ничего не кидать, и создать объект «как есть» (просто потом добавить дополнительную логику, я скажу об этом позже).
Итак, конструктор принимает функцию. В нашем движке это объект класса Function, реализующий метод call. Данный метод позволяет вызвать функцию, принимая на вход контекст исполнения, вектор с аргументами, и boolean параметр, определяющий режим вызова (вызов как конструктора или обычный режим).
Далее эта функция записывается в поле нашего объекта и потом может быть вызвана.
Заодно здесь же создадим константы для наших двух оставшихся состояний, и int поле, хранящее текущее состояние объекта.
Итак, согласно стандарту наша функция в процессе своего выполнения может вызвать одну из двух функций (которые передаются ей в качестве первых двух аргументов, поэтому по-хорошему мы должны задать их имена в сигнатуре функции). Обычно используют что-то вроде resolve и reject для простоты.
Это — обычные функции с точки зрения JavaScript, а значит, объекты Function с точки зрения нашего движка. Добавим поля и для них:
Эти функции могут быть вызваны в любой момент нашей основной рабочей функцией, а значит, должны находиться в её области видимости (scope). Кроме того, отработав, они должны менять состояние нашего объекта на fulfilled и rejected, соответственно. Наши функции ничего не знают про промисы (и знать не должны). Поэтому, нам нужно создать некую обёртку, которая будет про них знать, и сможет инициировать смену состояния.
Также нам нужен метод setState() для нашего объекта (с дополнительными проверками: например, мы не имеем права менять состояние, если оно уже fulfilled или rejected).
Займёмся конструктором нашего объекта:
Здесь, кажется, всё понятно. Если функция передана — мы обязаны вызвать её немедленно. Если нет — то ничего пока что не делаем (а наш объект сохраняет состояние pending).
Теперь про установку самих этих обработчиков (ведь в основной функции мы только объявляем их имена как формальные параметры). Для этого стандартом предусмотрены три варианта: Promise.then(resolve, reject), Promise.then(resolve) (эквивалентно Promise.then(resolve, null)), и Promise.catch(reject) (эквивалентно Promise.then(null, reject)).
Насчёт функции then: очевидно, что лучше всего реализовать подробно метод с двумя аргументами, а оставшиеся два сделать как «шорткаты» на него. Так и поступим:
В конце мы возвращаем ссылку на себя: это нужно для последующей реализации чейнинга промисов.
Что за блок у нас в начале метода, спросите вы? А дело в том, что наш обработчик мог исполниться ещё до того, как мы в первый раз вызвали then (такое бывает, и это совершенно нормально). В этом случае мы должны вызвать нужный обработчик из переданных в метод немедленно.
В месте многоточия потом будет ещё код, про него чуть позже.
Далее идёт установка наших обработчиков в нужные поля.
А вот далее самое интересное. Предположим, наша рабочая функция исполняется достаточно долго (запрос по сети, или просто setTimeout для учебного примера). В этом случае она по сути как бы исполнится, но создаст ряд объектов (таймер, сетевой XmlHttpRequest интерфейс и т.д.) которые исполнят некоторый код позднее. И эти объекты имеют доступ к scope нашей функции!
Поэтому сейчас ещё может быть не поздно добавить нужные переменные в её область видимости (а если поздно — то исполнится код в начале метода). Для этого мы создаём новый метод в классе Function:
Второй метод нам фактически не понадобится: он создан чисто ради полноты картины.
Теперь время реализовать шорткаты:
catch — зарезервированное слово в языке java, поэтому нам пришлось добавить знак подчёркивания.
Теперь опишем метод setState. В первом приближении он будет выглядеть так:
Отлично, теперь мы сможем менять состояние из наших обработчиков — точнее, из обёрток над ними. Займёмся обёртками:
Типов обёрток у нас два, но класс один. А за тип отвечает целочисленное поле to_state. Вроде, неплохо :)
Обёртка имеет ссылки как на свою функцию, так и на свой промис. Это очень важно.
С конструктором всё понятно, давайте посмотрим на метод call, переопределяющий метод класса Function. Для нашего JS интерпретатора — обёртки такие же функции, то есть объекты с тем же интерфейсом, которые можно вызывать, получать их значения, и так далее.
Сначала нам нужно пробросить в функцию объект Caller, полученный при вызове обёртки — это нужно как минимум для корректного всплытия исключений.
Далее мы вызываем нашу функцию и сохраняем в поле результат её исполнения. Заодно устанавливаем его в объект промиса, для чего создадим там ещё один метод setResult:
Про последнюю строчку пока говорить не будем: это нужно для чейнинга. В самом тривиальном случае там вернётся то же самое значение, которое мы только что получили и передали.
Важный момент: рабочая функция может вызвать resolve или reject до того, как мы вызовем метод then или catch (или мы можем не вызвать их вовсе). Чтобы при этом у нас не возникло исключения, прямо при создании промиса у нас создаются две «дефолтных» обёртки, у которых нет функций-обработчиков. При вызове они всего лишь поменяют состояние нашего промиса (и потом при вызове then это будет учтено).
Если коротко, чейнинг — это возможность писать вещи вида p.then(f1, f2).then(f3, f4).catch(f5).
Именно для этого наши методы then и _catch возвращают объект Promise.
Первое, что говорит нам стандарт — это то, что метод then при наличии существующего обработчика должен создать новый промис и добавить его в цепочку. Поскольку наши промисы должны быть равны между собой — пускай у нас не будет никакого головного промиса, хранящего линейный список, а каждый промис будет хранить только ссылку на следующий (изначально она равна null):
Вот и наш недостающий блок: если у нас уже есть следующий промис — передаём вызов ему и выходим (а он, если надо, передаст следующему, и так до конца). А если его нет — создаём и назначаем ему обработчики, которые получили в метод, после чего возвращаем уже его. Всё просто.
Теперь доработаем метод setState:
Во-первых, стандарт говорит о том, что мы обязаны передать обработчику следующего промиса результат работы предыдущего (в этом основной смысл чейнинга — назначить операцию, потом назначить вторую, и сделать так, чтобы вторая при старте приняла результат первой).
Во-вторых — ошибки обрабатываются особым образом. Если успешный результат передаётся по цепочке (видоизменяясь) до конца, то вот возникшая в коде обработчика ошибка — передаётся только на один шаг, до следующего onrejected, либо всплывает наверх, если достигнут конец цепочки.
В-третьих — функции могут вернуть новый промис. В этом случае мы обязаны подменить наш next, если он уже задан, на него (перебросив имеющиеся обработчики). Это, опять же, позволяет сочетать а цепочке обработчики моментального исполнения, и асинхронные — которые сами возвращают Promise.
Вышеприведённый код адресует все эти сценарии.
Пока что мы управляем всем со стороны Java кода. Тем не менее, всё уже работает: через полторы секунды мы увидим в консоли надпись «Promise fulfilled: OK». Кстати, наши функции resolve и reject, будучи вызванными из рабочей функции промиса, без чейнинга, могут принимать произвольное число аргументов. Весьма удобно. В этом примере мы передали строку «OK».
Ещё небольшое замечание: у промисов, созданных во время чейнинга, отсутствуют рабочие функции в принципе. У них сразу вызываются обработчики при смене состояния предыдущего промиса.
Пример посложнее:
Вызвав данный пример, мы получим следующий вывод:
Первые фигурные скобки — это объект промиса, который нам вернула наша цепочка вызовов then в результате чейнинга. В функции cbk1 мы вернули «OK» — и это значение было передано в cbk2, что мы и видим в последней строке. Внутри cbk2 мы бросаем ошибку со значением «ERROR» — поэтому cbk3 у нас не исполняется, зато исполняется err (как и должно быть при возникновении ошибки в обработчике предыдущего промиса в цепи). Но этот код исполняется моментально, а вот вывод cbk2 осуществляется через вспомогательную функцию, повешенную на таймер. Она имеет доступ к переменной str, как и должна, но её вывод идёт из-за этого ниже. Если исполнить данный пример в Chrome 49, мы получим ровно тот же вывод с одним исключением: переменная str не видна в анонимной функции, переданной в setTimeout. Это особенность поведения стрелочных функций в Хроме (а возможно, так нужно по стандарту, здесь я затрудняюсь сказать, в чём дело). Если поменять стрелочную функцию на обычную — вывод станет идентичным.
Но это ещё не всё. Наша конечная цель — чтобы новые возможности мог использовать JS код, исполняемый нашим интерпретатором. Впрочем, это уже дело техники.
Создаём конструктор:
И объект-прототип с набором нужных методов:
Не забудем добавить в конструктор Promise одну строчку в самом начале, чтобы всё работало:
И поменяем немного наш тест:
Вывод не должен измениться.
На этом всё! Всё отлично работает, можно писать дополнительные юнит-тесты и искать возможные ошибки.
Как приспособить этот механизм для Java? Очень просто. Создаём класс, аналогичный нашему Function, который что-то делает в методе operate. И оборачиваем уже его в нашу обёртку. В любом случае, на этот счёт есть много замечательных паттернов, с которыми можно поиграться.
Надеюсь, данная статья была кому-то полезна. Исходники движка я обязательно выложу, как только доведу их до ума и добавлю недостающий функционал. Удачного дня!
Внимание: в статье довольно много кода. Код не претендует на красоту и высокое качество, поскольку весь проект писался одним человеком и всё ещё находится в бете. Цель данного повествования — показать, как же всё работает под капотом. Кроме того, после небольшой адаптации данный код можно использовать для создания проектов чисто на 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. И оборачиваем уже его в нашу обёртку. В любом случае, на этот счёт есть много замечательных паттернов, с которыми можно поиграться.
Надеюсь, данная статья была кому-то полезна. Исходники движка я обязательно выложу, как только доведу их до ума и добавлю недостающий функционал. Удачного дня!
mayorovp
Что-то я не понимаю...
Вот у нас внутри PromiseHandleWrapper написано следующее:
Какое отношение результат вызова функции, переданной в then, имеет к значению, хранящемуся в обещании? Разве тут не должно быть наоборот — значение из обещания должно быть передано в onFulfilled единственным параметром?
Опять же, реализация метода then какая-то странная. Вы вообще в курсе, что в Javascript у одного и того же обещания можно вызвать метод then два раза, и это не будет цепочкой?
Кстати, куда делось требование 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:
popov654 Автор
Нет, не должно. Вы всё несколько путаете. Значение из onFulfilled должно пойти в следующий onFulfilled. А для этого оно передаётся в текущее (не следующее!) обещание. Это как промежуточное звено. Если Вы про самое первое обещание — там не нужно задействовать поле result, поскольку внутри «рабочей» (как я её условно назвал) функции, переданной в конструктор, resolve и так будет вызван с любыми нужными аргументами напрямую (а это и есть onFulfilled).
Признаться, полностью стандарт я не изучил :) А Вы не расскажете, почему существует такое требование, и что будет, если его проигнорировать? Про очередь работ (jobs) я мельком читал, но это ведь детали реализации, как именно мы всё это сделаем. Я хочу сказать, что это можно делегировать на отдельный уровень абстракции: создать планировщик потоков, который будет управлять background задачами, в том числе и этими задачами для промисов, а заодно и воркерами, и другими вещами. Пока что ничего этого мой движок не умеет — так к чему заморачиваться. Ведь результат исполнения кода сходится? Детали реализации могут варьироваться, главное, что поведение предсказуемо.
mayorovp
Тут проблема в гонках. Текущий стандарт гарантирует, что вот такой код выполнится корректно независимо от состояния обещаний:
А если вы вызываете onFulfilled синхронно — то продолжение может выполниться раньше чем объект полностью проинициализируется. Причем такой баг может спать в коде очень долго!
Но отличается от предписанного стандартом.
popov654 Автор
Спасибо, теперь более-менее понятно)
Хотя всё-таки не очень понимаю, какой практический смысл ставить обещания в конструктор. Ну да ладно, не мне судить, программист волен писать любой код, который считает нужным.
Давайте просто зайдём с другой стороны: у нас есть функция. Каждая функция в JavaScript (независимо от редакции стандарта) — это объект. Функция может быть вызвана как конструктор. При таком вызове, в числе прочего, у неё принудительно устанавливается this равным объекту, который она вернёт. Сам этот объект создаётся интерпретатором до её запуска (и отдаётся сразу после).
Я просто пытаюсь понять, где здесь гонка: если программист написал в конструкторе обещание, которое исполнится раньше, чем в коде конструктора ниже будет присвоено некое поле объекта — у него в коде обработчика обещания выкинет ошибку Reference Error, и он сам себе злобный Буратино :) Что до объекта this — так он доступен в конструкторе, а значит, доступен и в стрелочной функции в Вашем примере (и будет доступен в обычной, если использовать bind).
mayorovp
Проблема в том, что обещание зависит от внешнего кода. И если 99,999% случаев оно выполняется с задержкой, а в 0,001% случаев — сразу же, то этот случай программист в отладчике не увидит. И будет рассказывать сказки про кривой браузер.
Поэтому требование асинхронности во всех случаях ввели в стандарт. А раз оно попало в стандарт — программист уже имеет право на него полагаться.
popov654 Автор
А что мы понимаем под «требованием асинхронности»? Я всегда понимал асинхронность как работу параллельно, в отдельном потоке. Я так полагаю, здесь речь идёт о том, чтобы завершить весь текущий JS код, и только потом запускать очередь промисов? Но это же весьма печально: промисы отработают гораздо позже, чем могли бы (они ведь вообще на внешний код могут быть не завязаны, например, просто получаем что-то с сервера, парсим, выводим на страницу).
mayorovp
Нет, асинхронность — это выполнение отдельно от основного (синхронного) кода, и не важно в каком потоке.
"Гораздо позже" они отработают только в одном случае — если основной поток перегружен вычислительной работой, для чего он попросту не предназначен. Во всех остальных случаях действует правило — от перестановки слагаемых сумма не меняется.
popov654 Автор
Почему не предназначен? Если мы говорим про синхронный код — это например все вычисления, которые стартуют на onload веб-страницы (не знаю уж, в каком потоке их выполняют современные движки). И там может быть довольно приличное количество кода. В том числе довольно тяжёлого (например, код, получающий значения ширины и высоты изображений при их загрузке, чтобы что-то корректно рассчитать, пример из реального проекта). Да, код, повешенный на обработчики — уже асинхронный, но по факту всё это всё равно существенно нагружает браузер и он может подлагивать. Если есть возможность что-то исполнить раньше, чтобы потом у пользователя ничего не лагало, например, при прокрутке — разве не стоит этим воспользоваться?
mayorovp
Для того чтобы что-то исполнить раньше — надо что-то исполнить позже! Почему вы отдаете приоритет одному коду перед другим — и считаете что это уменьшит лаги?
Чтобы код не лагал — он должен успевать выполняться за некоторое время. И это время не зависит от того в каком порядке будет выполняться код.
popov654 Автор
Да, согласен. Наверное, в той ситуации дело было вообще не в JS коде. Скорее, в неудачном планировании браузером порядка выполнения операций (слишком много всего в одном потоке делалось, либо разные потоки плохо между собой взаимодействовали с точки зрения внутреннего планировщика). Тем более, дело было на Opera 11.64, на Chrome (особенно версий 40+) практически не тормозило ничего на том сайте, про который я вспоминаю.
popov654 Автор
Мне кажется, программист не должен был бы писать такой код, даже если бы это не включили в стандарт, т.к. это глупо и результат будет зависеть от времени исполнения промиса. Хуже не придумаешь вообще. Никто же не мешает поставить строчку, создающую промис, в конец конструктора?
Кстати, мне сегодня в голову пришла очень интересная идея. Мне кажется, главная причина требования исполнения всех промисов после того, как отработает весь синхронный код — это не дать возможность писать вот такой вот сомнительный код. Проблема в другом, я сегодня при модификации кода как раз на неё наткнулся :)
Смотрите, допустим мы создали промис, вызвали метод then. Он добавляет в вектор потомков новый промис (у нас же дерево). И после этого, если у нас состояние уже fulfilled (например, мы вызывали
Promise.resolve()
) — мы должны сразу его запустить. И в этом случае, даже если мы выполним добавление в вектор до запуска — у нас потом может идти вызов then через чейнинг уже для того нового промиса. И проблема в том, что он отработает раньше, чем туда что-то будет добавлено. И если в его обработчике onFulfilled произойдёт ошибка, то она не попадёт в обработчик onRejected следующего промиса, потому что следующего у него ещё просто нет (вектор потомков пуст). И она всплывёт наверх.Иными словам, необходимо дать доработать цепочке вызовов then до конца. А это синхронный код. Если этого не сделать — будут не очень приятные последствия.
Вообще, эту проблему можно обойти, вставив в код интерпретатора специальный костыль для выявления этого сценария, но стандарт решает её изящнее и радикальнее.
mayorovp
Нет, вот тут вы ошибаетесь. Или путаете свою кривую реализацию со стандартом. Промисы специально так сделаны, чтобы не было никакой разницы что произошло раньше — резолв промиса или подписка на него методом then!
Ошибка в обработчике никогда не выплывает за пределы обещания.
popov654 Автор
Так я ровно это же и написал выше. С чем Вы спорите?
Или я не прав, что then — это часть синхронного кода, который исполняется в первую очередь?
Да, resolve имеет право произойти раньше, но поведение должно быть предсказуемым. А если бы этого требования в стандарте не было, то поведение могло бы динамически меняться в зависимости от времени исполнения обработчика промиса (я специально привёл пример с ошибкой внутри обработчика для большей наглядности).
popov654 Автор
То есть — не выплывает? Ещё как выплывает, если нет обработчика onRejected дальше в цепи. Об этом же ясно в стандарте сказано. Да и в консоли это легко проверить
faiwer
Что мы видим? Мы видим 'after', что свидетельствует о том, что строка с
resolve().then
выполнилась без ошибки. Видим, что нашthrow 1
не был никем пойман (uncaught
).ЧИТД. Всплыла? Нет. Та же самая картинка если мы швыряем ошибку прямо в
new Promise(() => { /* тут */ }
.faiwer
Нет. Главная причина очевидна: унификация. Либо всё синхронно, либо всё асинхронно. Естественно выбрали второе (javascript же). Смешанный вариант чрезвычайно багонеустойчив. Уж поверьте, node-js разработчики наелись таких багов просто до отвалу.
Это когда код ломается в тех самых редких случаях, когда он решил выполниться синхронно (какой-нибудь флаг, наличие кеша, да что угодно), а код принимающий на такое рассчитан не был. Состояние гонки возможно даже в однопоточном коде. node-разработчики это хорошо знают. Причём в разных вариантах.
popov654 Автор
А можно примеры какие-то привести? Ну вот чтобы на пальцах (я Node.JS не знаю). Просто то, что Вы написали выше с конструктором — это вообще не гонка, это просто глупость со стороны JS программиста.
faiwer
Если
api
выполнится синхронно всё упадёт. Если асинхронно — не упадёт. И если это зависит от каких-нибудь хитрых неочевидных моментов (да ещё и закопано где-нибудь в глубине stack-trace-а), то выстрельнуть оно может совершенно неожиданно. Может, к примеру, приложение на обе лопатки положить. И анализ stack-trace-а вам особо не поможет, ибо он будет коротким и бесполезным. Здравствуйте часы дебага асинхронного nodejs кода. Бррр.В данном случае (в примере) очевидно, что можно переписать так, чтобы не падало в любой ситуации. В реальном же коде всё бывает сильно запутанно.
Я думаю, что мы тут все не особо умные и совершаем ошибки. А ещё ошибки совершают те, чей код мы используем. Особенно если это какой-нибудь вложенный во вложенный во вложенный npm-пакет. Я бы вообще, на вашем месте, да и на своём тоже, воздержался от таких высказываний. Не боги горшки обжигают. Тут порой такую дичь напишешь, особенно если в спешке.
popov654 Автор
Ну я не хотел никого обидеть тем высказыванием, это чисто про пример было. И я думаю, я бы такого не написал, если бы знал, что в ряде случаев та строчка может выполниться синхронно) Ну разве что, проглядел бы по невнимательности.
И вообще, я разве где-то говорил, что стандарт плохой? Я просто пытаюсь понять, почему он сделан именно так.
И я нигде не пытался сказать, что лучше смешанный вариант, как писали выше — что то так работает, то этак.
Просто мне видится, что проблема с then-ами, которые не успели назначить обработчики ошибок до того, как ошибка случилась — стали основной причиной, почему в стандарт включили это требование. Можно правда было облегчить его до «не начинать исполнение промисов до завершения обработки цепочки then». Но в итоге решили отложить исполнение всех промисов совсем, пока весь код не отработает. В принципе — наверное, это плюс, then от существующих promise-объектов могут вызываться и ниже в текущей функции, и даже где-нибудь потом, за её пределами. Опять же, это подтверждает верность моей мысли: мы не начинаем исполнять промисы не потому, что нам как авторам стандарта или движка так приспичило, а потому, что мы хотим убедиться, что всё дерево сформировано и на момент начала исполнения не подвергается изменениям.
faiwer
Не знали бы. Это бы для вас стало открытием. После 3-х часов дебага.
Вы похоже до сих пор не поняли что такое Promise-ы :) Нет такой стадии "завершение обработки". И быть не может. Точнее может: когда сборщик мусора виртуальной машины выкинет их из памяти. А до тех пор можно повешать свой
.then
в любую часть цепочку (лишь ссылка туда была). Опять же, это не цепочки. Это деревья. Другая структура данных.Это невозможно. Нет такой стадии "сформировано". Никто кроме разработчика не знает когда "весь код отработает". Цепочка начинает отрабатывать просто в рамках
event loop
. А.then
на неё могут вешаться когда угодно и как угодно. Может асинхронно.then
может быть повешан после выполнения всего, может в процессе, может до.popov654 Автор
Да я это уже понял. Я цепочку вызовов через точку имел в виду, единую строку кода как выражение)
Опять же, я писал про завершение интерпретации строки кода с цепочкой вызовов then. такая стадия — не только может быть, но и есть, и на ней можно даже что-то сделать (если мы не говорим про JIT компиляцию, у меня всё-таки интерпретатор обычный).
Поскольку в моём движке пока нет event loop — то это будет просто отдельная стадия, которая вызовется, когда весь скрипт исполнен :)
Спасибо за уточнение. В принципе, технически и правда никто не мешает использовать then уже в коде обработчиков наших промисов, или функций, который они вызовут, при наличии ссылок. Просто я думал, что такой вариант не очень желателен, но если это норма — тем лучше.
Так в том-то и дело: он может быть вызван до, но исполнение начинать ещё нельзя. То есть мы делим нашу интерпретацию на две стадии: основной поток исполнения, и потом исполнение промисов. Если я правильно понял вообще тот пункт из стандарта в том виде, в котором его привели… Есть кстати ещё таймеры, которые в отдельном потоке исполняются, и обработчики событий (в других движках). Но эти потоки работают параллельно с основным. А вот поток исполнения очереди промисов — как я понял, ждёт завершения работы основного потока. Поправьте, если я не прав.
faiwer
Да. Это норма. Промисы хороши тем, что нет резона суетиться по поводу его статуса. Он если данные есть уже готовые отработает в следующем тике. Если нет, то когда появятся. В итоге мы полностью отвязаны от внутренней кухни того, что там в этом промисе происходит. Оно просто работает. Как ему там взбрендится. А мы полагаемся на задокументированное поведение и не боимся, что оно в какой-то момент подкинет сюрприз.
В nodeJS мы не может взять и застопорить весь поток (точнее можем, но за это больно бьют, здесь так нельзя). И ситуация "вчера это могло работать синхронно" легко превращается в "ох, у меня тут теперь сплошные await-ы". Если изначально предполагать такую возможность и всё покрыть promise-like-кодом, то всё будет работать как часы несмотря на внутреннюю реализацию.
mayorovp
Javascript — пока что принципиально однопоточный. Второй поток при поддержке рантайма создать можно — но только в отдельном реалме (т.е. в нем будет свой глобальный объект, свои системные объекты и т.п.)
popov654 Автор
Если применить супер-умный оптимизатор с распараллеливанием тех функций, которые не завязаны друг на друга — то теоретически, должно быть можно. Безопасность же при работе с данными можно обеспечить на уровне мониторов и synchronized блоков, если мы про Java, или использовать всякие семафоры и мьютексы, если мы пишем на C/C++.
Кроме того, Вы ведь сейчас говорите про нативные потоки? А я про логические. Попробуйте в отладчике поставить точку остановки в какой-то функции, и ещё одну в функции, повешенной на таймаут, скажем, в 1000-1500 мс. В итоге Вас прямо в процессе отладки перекинет совсем в другое место, там будет другая функция и другой стек вызовов. Вызов таких отложенных функций, как и вызов хендлеров событий, движок производит время от времени, в некоторые слайсы времени, и это определяет его внутренний планировщик. Можно называть это event-loop, а я для простоты называю это отдельными потоками.
На самом деле, в браузере всё ещё веселее — там «основного» потока нет. Потому что то, что происходит при загрузке страницы — это код, вызванный из обработчика события onload. Это в моём движке есть некий «основной» поток, исполняющий код, переданный на вход :)
mayorovp
Вот как раз этого в Javascript не бывает. Новый Job не может начать выполняться пока старый не закончился.
popov654 Автор
Ваш пример нисколько не противоречит статье. Смотрите, Вы создали промис (без основной функции и вообще без new, через Promise.resolve). Он у вас сразу перешёл в fulfilled
состояние. Вы вызвали then — ваша функция сразу исполнилась, а состояние промиса не поменялось. Вы снова вызвали then — но состояние у нас всё ещё fulfilled, и поток исполнения опять пойдёт в первый if. Вот если бы у нас в этом случае создался новый промис в цепочке -тогда было бы плохо, так как у него сразу же вызвался бы метод then, но состояние так и зависло бы в pending.
А Ваш последний пример — это и есть чейнинг. Вы передали результат из первого onFulfilled во второй. Что и требуется по стандарту.
mayorovp
Нет! Вы же возвращаете this — а значит, никакого чайнинга не получится, четвертый вызов then пойдет по тому же самому пути что и первые три.
Более того, у вас все 4 вызова выведут на консоль undefined — потому что значение, которое было передано в resolve, нигде не используется...
popov654 Автор
А, всё, всё, понял.
Окей, в целом согласен. Претензия значит была не к тому, что у меня чейнинг не к месту, а наоборот, к тому, что он не работает, где должен работать :) Но:
1) В примере выше Вы передаёте моментально исполняющиеся функции. При передаче первой же функции, которая вернёт Promise сама, чейнинг начнёт работать. В целом если наши функции исполняются моментально, а состояние у нас fulfilled — то какая разница, делать чейнинг или не делать?
2) Да, и это довольно печально. С другой стороны, так вышло ровно потому, что в момент вызова resolve у нас был стандартный хендлер, а ему пофиг на переданные аргументы. Он поменял состояние объекта и всё. Потом мы вызвали then, установили новый хендлер, но аргумент уже потерян. Не знаю, как часто в жизни используется такой код, делающий моментальный resolve, но стандарт здесь явно нарушен, да.
mayorovp
Замените
Promise.resolve(5)
наnew Promise(resolve => setTimeout(resolve, 0, 5))
— тоже будет проблема.Очень часто. Это один из паттернов работы с обещаниями.
Кроме того, не забывайте, у вас есть вот такой код:
В этот момент result запросто может быть уже выполнено, и у вас будет та же проблема.
popov654 Автор
Вы про то, что задержка нулевая? А, ну в этом плане да. И правда баг очень глупый. Но хотя бы обработчик будет выполнен))
popov654 Автор
А смысл в таком паттерне? Вроде обещания же как раз для того и созданы, чтобы что-то запустить, и вызвать resolve по завершении операции, если я верно понял идею.
mayorovp
Смысл в том, чтобы протестировать другую ветку кода. Вместо 0 можно записать любое другое число — от этого ничего не должно поменяться, кроме времени выполнения теста.
Должно быть 5, 5, 5, 10 с точностью до порядка. А получится фигня какая-то.
popov654 Автор
Вы про Java код из моего примера или про JS код? Я спрашивал, когда в реальной жизни может понадобиться создать промис, и сразу перевести его в resolved состояние)
mayorovp
А, вы про это. Я говорил про ваш код. Он выполнит продолжения с неправильными аргументами.
Что же до
Promise.resolve(...)
— он часто используется для того чтобы отловить синхронную ошибку и обработать ее так же как асинхронную:popov654 Автор
Почему первый вариант не поймает ошибку? По стандарту если встречена ошибка, движок идёт до первого onRejected обработчика в цепочке. Кстати, только что проверил — отлично всё ловит.
faiwer
Я думаю, что имелось ввиду, что если
foo
внутри себя имеет где-нибудьreturn promise
(от которого потом можно сделать.then()
), но упадёт ещё в процессе, то никакойcatch
вызван не будет. Почему не будет? А почему должен? Ведь вызовfoo()
это просто вызов какой-то функции. Она может вообще к promise-ам никакого отношения не иметь. А может иметь. А может упасть в процессе выполнения.А вот положенные внутрь
.then()
методы выполняются внутриtry catch
и их ошибки уже отлавливаются.popov654 Автор
А, господи, опять туплю. Ну да, тогда всё верно. Но кто заставляет писать foo().then(), когда можно (и нужно) писать new Promise(foo).then()?
mayorovp
Нельзя вызвать
foo()
какnew Promise(foo)
если foo — это обычная асинхронная функция которая возвращает обещание.А вот как
Promise.resolve().then(foo)
ее вызвать можно!popov654 Автор
А какие функции мы считаем асинхронными? Просто официально задекларированный и рекомендованный способ создания промиса (во всяком случае, в упомянутой статье Кантора) — через new Promise(func)
faiwer
Для
new Promise(fn)
сигнатура:fn(resolve, reject)
. В то время как в.then(fn)
можно передать любую вообще функцию. И сигнатура у неё уже будетfn(resultOfPrev)
, никакихresolve
иreject
. Точнее там даже.then(successFn, failureFn)
. И если нашаfn
не может быть выполнена синхронно, то она должна вернуть новый promise, который она должна создать уже самостоятельно и вернуть его вreturn
. Если эта нашаfn
являетсяasync fn
, то этим займётся сам интерпретатор языка. Помимо прочего, если нам не нужно делать никаких асинхронных запросов, то мы можем сразу в этойfn
вернуть нужный результат. Например:Следующая функция в цепочке-древе получит либо 42 либо результат от
new Promise
. Такая вот автоматика. Удобно.popov654 Автор
Ну да, я про это знаю) Всё правильно.
А вот про эту возможность не знал, надо будет почитать, что это и как оно работает.
mayorovp
Асинхронная функция в широком смысле — это любая функция которая совершает асинхронную операцию.
В более узком смысле асинхронная функция — это функция которая возвращает обещание.
Конструктор new Promise предназначен для создания базовых асинхронных функций или для перехода от кода на колбеках к коду на обещаниях — т.н. "промисификации".
Составные же асинхронные функции так не делаются. Вместо этого используется один из двух подходов:
popov654 Автор
Ну вот кстати про этот механизм в той же статье было сказано, что он потерял свою актуальность. Хотя я может несколько неправильно понял всё. Но всё же — почитать надо, я про него совсем не знаю ничего.
faiwer
Какой? async-await oO? Скорее вытеснил ручную работу с promise на далёкие окраины.
popov654 Автор
Но он ведь появился, вроде как, намного раньше. Я упоминания о нём ещё в статьях за январь 2014-ого видел. Разве не?
Насчёт цитаты — я перепутал, там сравнение с генераторами было, а не с промисами :)
mayorovp
async-await основан на обещаниях, он никак не мог появиться раньше!
Кстати, обещания в библиотеках существуют как минимум с 2010го года.
popov654 Автор
Ну, мы же не про библиотеки говорим, а про ES6, который в браузерах только к концу 2015-ого появился толком, если не в начале 2016-ого (по Хрому если смотреть). Правда, отдельные фичи работали и раньше в виде экспериментов :)
faiwer
Возможно вам поможет эта старая статья.
popov654 Автор
Спасибо, отличный текст) А у меня вот вопрос возник. В тексте есть следующая цитата:
Означает ли это, что движок должен непрерывно мониторить состояние такого объекта — ведь неизвестно, когда оно может смениться, если оно на момент передачи pending?
mayorovp
Нет. Там просто вызывается метод then и все.
popov654 Автор
А конечный стейт мы как узнаем? Промисообразный объект имеет метод then, но не имеет никакого API для того, чтобы узнать его состояние. Что-то с этой выдержкой явно не так, имхо.
mayorovp
А зачем нам вдруг чужой стейт-то? Зачем он вообще нужен?
popov654 Автор
И ещё:
Возможно, кривой перевод, но я здесь практически не понял ничего в этом абзаце. yield вернёт промис, потому что за ним стоит вызов функции, возвращающей промис. Каким образом он кинет нам исключение? Имеется в виду то исключение, что возникло в функции промиса getJSON и не было поймано там же? Если так, то да, но имхо если мы хотим подробный вывод ошибки в консоль, логичнее такое ловить на месте. Хотя стек вызова мы и так получим)
faiwer
Мне кажется, что 99.9% пишут как раз
foo().then()
. Проще и нагляднее, а try-catch уровнем выше какой-нибудь итак лежит. Но да, когда-нибудь кому-нибудь это сильно аукнется (и я буду в первых рядах :D). Кстати, если foo сделан через async, то проблемы нет. Сейчас async это скорее правило, нежели исключение (имхо).Проблема обычно именно с тем чтобы отловить ошибки именно в отложенных сценариях, а не в синхронных. Например какое-нибудь не promise-api какой-нибудь библиотеки с callback-ом. Это когда нотация для callback-а такая:
fn(error, result)
. И вот если ошибка не была в той библиотеке отловлена и помещена в этотerror
, то ошибка просто падает и никем не отлавливается. А отлавливаются только те ошибки, которые автором библиотеки были отловлены целенаправленно. Но он же (автор), наверное, не идеален…А вот стандартно написанный код поверх async-await от таких проблем обычно спасает. Ты что-то протупил, непредусмотрел, но т.к. try-catch "из коробки", а проброс ошибок встроен в promise-систему, то трагедии не будет.
popov654 Автор
Я кстати наконец понял, чем Вам не понравился подход со значением. Да, можно было бы передать значение не в текущий промис, а в следующий. А забирать его перед вызовом JS функции-обработчика внутри обёртки. Я согласен, что с точки зрения логики так изящнее. Но с точки зрения реализации — на мой взгляд, сложнее. Просто сейчас решение о вызове следующего промиса и передаче ему значения принимает метод setResult. Это несколько не свойственный ему функционал, и это плохо. С другой стороны, обёртка избавлена от необходимости явным образом получать какие-то аргументы (тем более в случае первого промиса в цепи она получает эти аргументы естественным образом автоматически, и городить там две логики, либо как-то разделять обёртки-обработчики на обработчики «головного» и «не головного» помиса, тоже плохо).
Но самое главное, чем мне кажется текущий подход лучше — это тем, что следующего промиса в цепочке может просто не быть. А формальное значение всегда пригодится для интерпретатора, чтобы куда-нибудь его вывести (так же, как сейчас для блока кода return value — это ни что иное как return value последнего исполненного выражения в нём). То есть результатом работы промиса (его значением) будем считать результат работы сработавшего хэндлера, как-то так.
popov654 Автор
Кстати, мы оба оказались не правы. В первой части Вашего примера тоже чейнинг должен работать:
mayorovp
Почему это неправы оказали мы оба? Вы правда думаете что я свой пример в консоль Хрома не копировал? :-)
popov654 Автор
Но подождите, если Вы его копировали, Вы же должны были видеть, что создаётся новый промис. Что это по-вашему, если не цепочка?
mayorovp
Это две цепочки. Более строго, ожидающие обещания образуют не цепочки, а деревья.
popov654 Автор
Эм, серьёзно? Зачем?..
С точки зрения кода — это ведь линейная цепь вызовов then и catch.
mayorovp
Где вы видите тут линейную цепь вызовов?
Порядок вызовов важен! Значение p должно попасть в обработчики 1, 2 и 4, значение которое вернул обработчик 2 — должно попасть в обработчик 3.
popov654 Автор
А, вон оно что… Окей, теперь понял, как правильно. Спасибо.
Но как по мне — это излишнее усложнение… Можно было сделать принудительное приведение к линейному списку, как у меня. Тем более, в статье Ильи Кантора, которую я читал (она правда совсем для новичков), этот момент не был освещён. Надеюсь, хоть автор про него знал :)
faiwer
Ну и кому они были бы нужны в таком виде? Поверьте и то, что .then создаёт деревья, а не продолжение списка, и то, что оно всегда отрабатывает асинхронно ? очень правильные архитектурные решения. Вы это достаточно быстро поймёте если будете писать много promise-js кода.
mayorovp
Ваше "упрощение" нарушает абстракцию.
Смысл обещаний — в том, что они ведут себя как значения: их можно передавать из метода в метод, сохранять где-нибудь, или забывать. Важно, что чтобы ни происходило с обещанием — на него это уже никак не повлияет, если есть обещание — всегда можно вызвать у него метод then и асинхронно получить лежащее в нем значение, независимо от того сколько раз этот метод уже вызывался.
mayorovp
Кстати, по поводу статей Ильи Кантора и подобных… Они хороши для тех кто использует обещания, а не для тех кто их делает.
Первую же свою реализацию обещаний проще всего писать непосредственно по спецификации: http://www.ecma-international.org/ecma-262/6.0/#sec-promise-constructor
Там уже описаны все структуры данных и все алгоритмы над ними, надо лишь аккуратно перенести это все в код.
mayorovp
Что же до приспособления этого механизма для Java — в Java давно уже есть CompletableFuture
gkislin
Честно, тоже ожидал это в статье встретить. Ждем комментария автора.
asm0dey
Только ради этого комментария я зашёл в статью.
Могу ещё добавить что до 8й джавы была существовала Google Guava, в которой есть ListenableFuture со схожей семантикой.
Enverest
На сколько мне известно, то что в Джаваскрипте упрощённо называют промисами — в остальных языках программирования разделяют на Promise и Future. В Джаве есть реализация Future — это CompletableFuture.