The promise then() function in javascript

Что такое async/await и promise?

Прежде чем ответить на поставленный вопрос, нам необходимо узнать немного теории.

Асинхронность меняет сложившуюся парадигму последовательного кода. Последовательность — когда только одна конкретная операция происходит в данный момент времени. Если функция зависит от результата выполнения другой функции, то она должна дождаться пока прошлая функция не завершит свою работу. Для пользователя это значит состояние вечного «ждуна».

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

«Летняя ИТ-школа КРОК»

Старт 28 июня, 2 недели, Москва, Беcплатно

tproger.ru

События и курсы на tproger.ru

На самом деле с точки зрения машинного кода, async/await и промисы это абсолютно то же самое. Но мы то с вами люди, и нам важен синтаксис. И разница в синтаксисе настолько существенна, что разделила разработчиков на два лагеря. Любители колбэков выбрали Promise, а не любители цепочек выбрали async/await.

Async/await — синтаксис работающий с промисами, придуман как альтернатива синтаксису промисов. Используя async, можно полностью избежать использования цепочек промисов с помощью await. Async создает Promise. А await ждет выполнения промиса.

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

Давайте посмотрим практические маленькие примеры синтаксиса.

Пример 1:

Пример 2:

Пример 3:

Так как является надстройкой над промисами, то мы можем смешивать код, например так:

или так

Последовательное выполнение Промисов

Теперь, после знакомства с теорией, можно приступать к созданию приложений. Рассмотрим работу на примере создание веб-паука:

Код, который представлен ниже, не будет работать. Это просто пример использование.

const utilities = require('./utilities');
const request = utilities.promisify(require('request'));
const mkdirp = utilities.promisify(require('mkdirp'));
const fs = require('fs');
const readFile = utilities.promisify(fs.readFile);
const writeFile = utilities.promisify(fs.writeFile);

function download(url, filename) {
  console.log(`Downloading ${url}`);
  let body;
  return request(url)
    .then(response => {
      body = response.body;
      return mkdirp(path.dirname(filename));
    })
    .then(() => writeFile(filename, body))
    .then(() => {
      console.log(`Downloaded and saved: ${url}`);
      return body;
    });
}

Промисы в ES5, ES6/2015, ES7/Next

ES 5 — поддерживают почти все браузеры. Демо код работает в ES5 среде (Все основные браузеры + NodeJS), если бы вы подключили библиотеку промисов Bluebird. Почему так? Потому что ES5 не поддерживает промисы из коробки. Другая знаменитая библиотека промисов это Q, от Криса Коваля.

ES6 / ES2015 — демо код сработает прямо из коробки, так как ES6 поддерживает промисы естественным путём. Более того, с ES6 функциями, мы можем ещё круче упростить код с помощью  и использовать const и .

Обратите внимание, что все  заменены на . Все  были упрощены на  

ES7 —  делают синтаксис визуально лучше. ES7 представил async и await синтаксис. Это делает асинхронный синтаксис визуально лучше и проще для понимания, без  и  . Перепишем свой пример с ES7 синтаксисом.

1. Всегда, когда вам нужно возвратить промис в функцию, вы ставите спереди  к этой функции. Для примера, 

2. Всякий раз, когда вам надо вызвать промис, вам надо вставить . Для примера,  и 

3. Используйте   , чтобы словить ошибку промиса, отклоненную промисом.

Нормальная функция против асинхронной.

Давайте посмотрим на эти два примера, каждый пример делает сложение двух чисел, один пример с нормальной функцией, другой с удаленной.

Нормальная функция для сложения чисел.

Асинхронная функция для сложения двух чисел:

Если вы сложите числа нормальной функцией, то вы сразу же получите результат. Тем не менее, если в вашем случае нужен удаленный запрос для получения результата, то вам нужно подождать, тут вы не сможете получить результат мгновенно.

Или таким способом вы вообще не можете знать — получите ли вы результат, потому что сервер может просто упасть, тормознуть с ответом и т. п. Вам не нужно, чтобы весь процесс был заблокирован, пока вы ждете результат.

Вызов API, скачивание файлов, чтение файлов — всё это те обычные async операции, которые вы можете выполнять.

Compatibility with other libraries #

The JavaScript promises API will treat anything with a method as promise-like (or in promise-speak sigh), so if you use a library that returns a Q promise, that’s fine, it’ll play nice with the new JavaScript promises.

Although, as I mentioned, jQuery’s Deferreds are a bit … unhelpful. Thankfully you can cast them to standard promises, which is worth doing as soon as possible:

Here, jQuery’s returns a Deferred. Since it has a method, can turn it into a JavaScript promise. However, sometimes deferreds pass multiple arguments to their callbacks, for example:

Whereas JS promises ignore all but the first:

Thankfully this is usually what you want, or at least gives you access to what you want. Also, be aware that jQuery doesn’t follow the convention of passing Error objects into rejections.

Example: loadScript

Let’s use this feature with the promisified , defined in the , to load scripts one by one, in sequence:

This code can be made bit shorter with arrow functions:

Here each call returns a promise, and the next runs when it resolves. Then it initiates the loading of the next script. So scripts are loaded one after another.

We can add more asynchronous actions to the chain. Please note that the code is still “flat” — it grows down, not to the right. There are no signs of the “pyramid of doom”.

Technically, we could add directly to each , like this:

This code does the same: loads 3 scripts in sequence. But it “grows to the right”. So we have the same problem as with callbacks.

People who start to use promises sometimes don’t know about chaining, so they write it this way. Generally, chaining is preferred.

Sometimes it’s ok to write directly, because the nested function has access to the outer scope. In the example above the most nested callback has access to all variables , , . But that’s an exception rather than a rule.

Thenables

To be precise, a handler may return not exactly a promise, but a so-called “thenable” object – an arbitrary object that has a method . It will be treated the same way as a promise.

The idea is that 3rd-party libraries may implement “promise-compatible” objects of their own. They can have an extended set of methods, but also be compatible with native promises, because they implement .

Here’s an example of a thenable object:

JavaScript checks the object returned by the handler in line : if it has a callable method named , then it calls that method providing native functions , as arguments (similar to an executor) and waits until one of them is called. In the example above is called after 1 second . Then the result is passed further down the chain.

This feature allows us to integrate custom objects with promise chains without having to inherit from .

Consuming a Promise: then, catch, finally

1) The method

The method is used to schedule a callback to be executed when the promise is successfully resolved.

The method takes two callback functions:

The callback is called if the promise is fulfilled. The callback is called when the promise is rejected.

The following function returns a object:

And the following calls the function and invokes the method:

It is possible to schedule a callback to handle the fulfilled or rejected case only. The following runs the fulfilled case:

And the following schedules a callback to handle the rejected case:

Note that you must pass to the method as the first argument.

2) The method

If you want to schedule a callback to be executed when the promise is rejected, you can use the method of the object:

Internally, the method invokes the method.

3) The method

Sometimes, you want to execute the same piece of code whether the promise is fulfilled or rejected. For example:

As you can see, the function call is duplicated in both and methods.

To remove this duplicate and execute the whether the promise is fulfilled or rejected, you use the method, like this:

Цепочки промисов

«Чейнинг» (chaining), то есть возможность строить асинхронные цепочки из промисов – пожалуй, основная причина, из-за которой существуют и активно используются промисы.

Например, мы хотим по очереди:

  1. Загрузить данные посетителя с сервера (асинхронно).
  2. Затем отправить запрос о нём на github (асинхронно).
  3. Когда это будет готово, вывести его github-аватар на экран (асинхронно).
  4. …И сделать код расширяемым, чтобы цепочку можно было легко продолжить.

Вот код для этого, использующий функцию , описанную выше:

Самое главное в этом коде – последовательность вызовов:

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

Если очередной вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.

В коде выше:

  1. Функция в первом возвращает «обычное» значение . Это значит, что возвратит промис в состоянии «выполнен» с в качестве результата. Он станет аргументом в следующем .
  2. Функция во втором возвращает промис (результат нового вызова ). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий с его результатом.
  3. Третий ничего не возвращает.

Схематично его работу можно изобразить так:

Значком «песочные часы» помечены периоды ожидания, которых всего два: в исходном и в подвызове далее по цепочке.

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

То есть, логика довольно проста:

  • В каждом мы получаем текущий результат работы.
  • Можно его обработать синхронно и вернуть результат (например, применить ). Или же, если нужна асинхронная обработка – инициировать её и вернуть промис.

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

Это общее правило: если внутри стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис.

В данном случае промис должен перейти в состояние «выполнен» после срабатывания .

Строку для этого нужно переписать так:

Теперь, если к цепочке добавить ещё , то он будет вызван после окончания .

Create a promise

You can create a promise from any asynchronous code by wrapping it in an executor function as follow :

var myPromise = new Promise(function (resolve, reject, notify) {
    setTimeout(resolve, 1000, 'Hello world !');
});

The example code above will simply create a promise that will be resolved at least 1000 ms in the future with the value .

Upon wrapped code execution, the executor function will be injected 3 callbacks that you can use to resolve/reject the created promise or just notify watchers of its progress. Each of these callbacks accepts one argument that will be in turn passed to callbacks attached via , see the section.

NB1 : Note that as a promise can be whether resolved or rejected only once, multiple calls to / won’t have any effect, the first to be called will determine the promise final state. On the other hand can be called as much as you want, see the section.

NB2 : Throwing something in the executor function will have the same effect as calling and passing it the thrown value as parameter.

Плюсы и минусы в теории

Async/awaitПлюсы

  • Удобство и простота чтения
  • Возможность использования последовательного стиля программирования

Минусы

  • Легко наткнуться на избыточное ожидание последовательного кода. Для истинной параллельности нужно модифицировать код.
  • Неочевидность возвращаемых значений try…catch.

PromiseПлюсы

  • Использует традиционный подход колбэков.
  • Данные с ошибками и данные с успешным результатом операции однозначно понимаемы.
  • Возможность использовать Promise.all без оглядки на синтаксис.
  • Оповещения Promise.resolve и Promise.reject доступны везде.
  • Наглядное использование метода Promise.finally.

Минусы

При неправильном использовании возможно создание слишком глубоких использований цепочек .then

Пример использования

Базовое использования метода

const promise = new Promise(function(resolve, reject) {
    setTimeout(resolve, 2000, "promise1"); // изменяем состояние объекта на fulfilled (успешное выполнение) через 2 секунды
});

const promise2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 1000, "promise2"); // изменяем состояние объекта на fulfilled (успешное выполнение) через 1 секунду
});

const promise3 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, "promise3"); // изменяем состояние объекта на fulfilled (успешное выполнение) через пол секунды
});

Promise.all([promise, promise2, promise3]) // ожидаем выполнение всех переданных в массиве обещаний / или отклонения какого-то обещания
                .then(val => console.log(val)); // обработчик для успешного выполнения

// 

В этом примере мы инициализировали три переменные, которые содержат объект Promise, который изменяет состояние объекта на fulfilled (успешное выполнение) через 2 секунды в первом случае, через одну секунду во втором и через пол секунды в третьем.

С использованием метода .all() объекта Promise мы принимаем массив, который содержит ссылку на три обещания, результат выполнения этого метода зависит от того с каким результатом завершится выполнение обещаний, в нашем случае он возвращает объект Promise, который имеет состояние fulfilled (успешное выполнение), так как все переданные объекты Promise в аргументе имеют состояние fulfilled (успешное выполнение).

С использованием метода then() мы добавили обработчик, вызываемый когда объект Promise имеет состояние fulfilled (успешное выполнение), и выводим в консоль полученное значение (массив полученных значений из всех обещаний).

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

const promise = new Promise(function(resolve, reject) {
    setTimeout(resolve, 2000, "promise1"); // изменяем состояние объекта на fulfilled (успешное выполнение) через 2 секунды
});

const promise2 = new Promise(function(resolve, reject) {
    setTimeout(reject, 1000, new Error("Обещание отклонено")); // изменяем состояние объекта на rejected (выполнение отклонено) через 1 секунду
});

const promise3 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, "promise3"); // изменяем состояние объекта на fulfilled (успешное выполнение) через пол секунды
});

Promise.all([promise, promise2, promise3]) // ожидаем выполнение всех переданных в массиве обещаний / или отклонения какого-то обещания
                .then(val => console.log(val), // обработчик для успешного выполнения
                           err => console.log(err.message)); // обработчик для случая, когда выполнение отклонено

// Обещание отклонено

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

Обратите внимание, что с помощью метода reject() мы изменяем значение объекта Promise на rejected (выполнение отклонено) через 1 секунду во второй переменной

С использованием метода .all() объекта Promise мы принимаем массив, который содержит ссылку на три обещания, результат выполнения этого метода зависит от того с каким результатом завершится выполнение обещаний, в нашем случае он возвращает объект Promise, который имеет состояние rejected (выполнение отклонено), это связано с тем, что один из переданных объектов изменил своё состояние на rejected (выполнение отклонено).

С использованием метода then() мы добавили обработчики, вызываемые когда объект Promise имеет состояние fulfilled (успешное выполнение), или rejected (выполнение отклонено). В нашем случае срабатывает обработчик для отклоненного выполнения, и выводит информацию об ошибке в консоль.

Нюансы использования метода

Promise.all()
                .then(val => console.log(val)); // обработчик для успешного выполнения

// 

Promise.all()
                .then(val => console.log(val)); // обработчик для успешного выполнения

// 

Promise.all([])
                .then(val => console.log(val)); // обработчик для успешного выполнения

// []            

В этом примере мы рассмотрели основные нюансы использования метода .all(), например, если объект содержит одно, или несколько значений, которые не являются обещаниями, то метод разрешится с этими значениями. Если переданный объект пуст, то возвращенное обещание будет сразу переведено в состояние fulfilled (успешное выполнение).

JavaScript Promise

Объект promise: его философия, техническое представление, возможные состояния

Уже не раз было мною замечено, что качественное обучение программированию должно состоять из 2 частей. Это — философское осмысление идеи, а уже затем её техническая реализация. То есть обычная человеческая логика, которой ученик руководствуется при принятии каких-либо решений, сильно облегчает понимание технической реализации этого решения. Поэтому начнем мы с того, что такое обещание в жизни, и как мы к нему относимся? А затем посмотрим: как примеры обещаний будут реализованы в коде. Рассмотрим следующие рисунки (рис. 1, 2, 3).

рис 1. ( `PromiseState` — как результат обещания )рис 2. ( `PromiseResult` — как информация, связанная с результатом выполненного или невыполненного обещания )рис 3. ( `PromiseFulfillReactions`, `PromiseRejectReactions` — как последствия, которые наступают после выполнения или невыполнения обещания )

Мы видим, что само понятие обещания стоит на 3-х китах. 1) Было ли выполнено обещание вообще? 2) Какую дополнительную информацию мы можем извлечь после выполнения или отказа обещания? 3) Какие последствия наступят в случае положительного или отрицательного исхода нашего обещания?

Технически же обещание — это обыкновенная сущность, выраженная через такой тип данных, как объект. У этой сущности есть имя / класс Promise. Объекты, рожденные от этого класса, имеют в цепочке своих прототипов Promise.prototype. И данная сущность должна быть как-то связана со всей той «информацией из жизни», которую мы рассмотрели выше. Спецификация ECMAScript эту информацию закладывает в промис еще на уровне, который ниже по абстракции, чем сам JavaScript. Например, на уровень С++. Соответственно в объекте промиса есть место и под статус, и под результат, и под последствия обещания. Взгляните на то, из чего состоит обещание по  (рис. 4).

рис 4. ( Внутренние поля promise объекта по версии ECMAScript спецификации )

Какими новыми красками заиграла фраза «обещать — не значит жениться» с точки зрения программиста? 1) `PromiseState`. Кто-то не женился. 2) `PromiseResult`. Потому что ему не хватило денег на свадьбу. 3) `PromiseRejectReactions`. Как следствие — у него появилось куча свободного времени, которое он потратил на саморазвитие 4) `PromiseFulfillReactions`. А зачем человеку план B, когда он уже выбрал план A?

Да, есть и пятое поле `PromiseIsHandled`

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

Если нет, то необработанный reject обещания интерпретируется движком JS как ошибка. Для нетерпеливых: если программист не повесил вторым аргументом через Promise.prototype.then() функцию-callback-обработчик reject-а состояния промиса — то возникшее состояние «rejected» promise объекта покажет вам красную ошибку в консоли разработчика.

Обратили внимание, что поля объекта promise заключены в «]»? Этим подчеркивается, что доступ к данной информации у программиста JS напрямую не имеется. Только через специальные средства / команды / API, как, например, команду Promise.prototype.then()

Если же у вас есть непреодолимое желание управлять «этой кухней» напрямую, то добро пожаловать в клуб разработчиков стандартов спецификации EcmaScript.

Небольшое замечание в конце данной подглавы. Если в жизни у нас могут быть обещания выполнены частично, то в EcmaScript — нет. То есть, если человек обещал отдать миллион, а отдал 950 тысяч, то в жизни, может, он и надежный партнер, но для для JavaScript такой должник будет занесен в черный список через `PromiseState` === «rejected». Promise объект меняет свое состояние однозначно и всего лишь один раз. Как это технически реализуется — немного позже.

Promise: Последовательные итерации

На данный момент, на примере приложения веб­-паука, мы рассмотрели объекты Promise и приемы их использования для создания простой элегантной реализации последовательного потока выполнения. Но этот код обеспечивает выполнение лишь известного заранее набора асинхронных операций. Поэтому, чтобы восполнить пробелы в исследовании последовательного выполнения, нам нужно разработать фрагмент, реализующий итерации с помощью объектов Promise. И снова прекрасным примером для демонстрации станет функция spiderLinks().

function spiderLinks(currentUrl, body, nesting) {
  let promise = Promise.resolve();
  if(nesting === 0) {
    return promise;
  }
  const links = utilities.getPageLinks(currentUrl, body);
  links.forEach(link => {
    promise = promise.then(() => spider(link, nesting – 1));
  });
  return promise;
}

Для асинхронного обхода всех ссылок на веб-­странице нужно динамически создать цепочку объектов Promise.

  1. Начнем с определения «пустого» объекта Promise, разрешаемого как undefned. Он будет служить началом цепочки.
  2. Затем в цикле присвоим переменной promise новый объект Promise, полученный вызовом метода then() предыдущего объекта Promise в цепочке. Это и есть шаблон асинхронных итераций с использованием объектов Promise.

В конце цикла переменная promise будет содержать объект Promise, который вернул последний вызов then() в цикле, поэтому он будет разрешен после разрешения всех объектов Promise в цепочке.

Chain promises

returns a new promise and therefore allows you to chain calls :

new Promise(function (resolve, reject, notify) {
    setTimeout(resolve, 1000, 'P1');
}).then(function (value) {
    return value.toLowerCase();
}).then(function (value) {
    console.log(value);
});

// Will console.log('p1')

You can also reject any promise in the chain by throwing an error :

new Promise(function (resolve, reject, notify) {
    setTimeout(resolve, 1000, 'P1');
}).then(function (value) {
    throw 'I am not in the mood';
}).then(function (value) {
    console.log(value);
}, function (error) {
    console.log('An error has happened', error);
});

// Will console.log('An error has happened', 'I am not in the mood')

You can also, and it’s a very common case, return another promise :

new Promise(function (resolve, reject, notify) {
    setTimeout(resolve, 1000, 2000);
}).then(function (value) {
    return new Promise(function (resolve, reject, notify) {
        setTimeout(resolve, value, 'Timers done !');
    });
}).then(function (value) {
    console.log(value);
});

// Will console.log('Timers done !') at least 3000 ms after

When a promise is resolved but you didn’t provided a to or is rejected but no was provided, then the new promise returned keeps the parent promise state :

new Promise(function (resolve, reject, notify) {
    setTimeout(reject, 1000, 'Timer failed');
}).then(function (value) {
    console.log('Timer done !');
}).then(null, function (error) {
    console.log('An error has happened', error);
});

// Will console.log('An error has happened', 'Timer failed')

Now imagine replacing all these calls by some AJAX ones ! If you’re an adept of jQuery’s you can for example :

new Promise(function (resolve, reject, notify) {
    $.ajax({
        url: 'www.example.com',
        success: function (data, textStatus, jqXHR) {
            resolve(data);
        },
        error: function (jqXHR, textStatus, errorThrown) {
            reject(errorThrown);
        }
    });
}).then(function (data) {
    // Do something with data received
}, function (error) {
    console.log('AJAX call failed :', error);
});

Применяем промисы

Теперь у нас есть промис, давайте применим его:

1. Мы вызываем функцию в . В этой функции, мы применим наш промис .

2. Нам надо сделать одно действие, чтобы промис был решен или отклонен, тут мы будем использовать  и .

3. В нашем примере, у нас в . Какое значение у ?  значение это точное значение в вашем промисе (значение при успехе). Следовательно, это будет .

4. У нас есть  в . Какое значение будет у ? Как вы могли предположить,  значение именно то, которое вы указали в промисе (значение при неудаче). Следовательно, в этом случае это будет .

Давайте запустим этот пример и увидим результат!

Цепочки промисов

Да, в промисах есть цепочки.

Давайте представим, что вы ребенок и обещали своему другу, что покажете ему новый телефон, когда вам его купят. Это будет ещё один промис.

В этом примере вы уже наверное поняли, что мы не вызывали . Так как, в принципе, это опционально.

Мы вообще можем сократить этот пример, используя .

А теперь давайте свяжем наши промисы. Вы — ребенок и можете запустить  промис только после промиса .

Вот так легко связывать промисы.

Promise.allSettled

Новая возможность

Эта возможность была добавлена в язык недавно.
В старых браузерах может понадобиться полифил.

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

Метод всегда ждёт завершения всех промисов. В массиве результатов будет

  • для успешных завершений,
  • для ошибок.

Например, мы хотели бы загрузить информацию о множестве пользователей. Даже если в каком-то запросе ошибка, нас всё равно интересуют остальные.

Используем для этого :

Массив в строке будет таким:

То есть, для каждого промиса у нас есть его статус и значение/ошибка.

Если браузер не поддерживает , для него легко сделать полифил:

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

Этот обработчик превращает успешный результат в , а ошибку в . Это как раз и есть формат результатов .

Затем мы можем использовать , чтобы получить результаты всех промисов, даже если при выполнении какого-то возникнет ошибка.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector