Класс Promise существует во многих современных движках JavaScript и может быть легко заполифиллен. Основной причиной использования промисов является синхронный стиль обработки ошибок в отличие от асинхронного коллбэк стиля.
Чтобы в полной мере оценить промисы, давайте рассмотрим простой пример, который доказывает сложность создания надежного асинхронного кода с помощью только коллбэков. Рассмотрим простой случай создания асинхронной версии загрузки JSON из файла. Синхронная версия этого может быть довольно простой:
1 2 3 4 5 6 7 8 91011121314151617181920212223
importfs=require('fs');functionloadJSONSync(filename:string){returnJSON.parse(fs.readFileSync(filename));}// валидный json файлconsole.log(loadJSONSync('good.json'));// несуществующий файл, поэтому fs.readFileSync завершается ошибкойtry{console.log(loadJSONSync('absent.json'));}catch(err){console.log('absent.json error',err.message);}// невалидный json файл т.е файл существует, но содержит невалидный JSON,// поэтому JSON.parse завершается ошибкойtry{console.log(loadJSONSync('invalid.json'));}catch(err){console.log('invalid.json error',err.message);}
Эта простая функция loadJSONSync имеет три варианта поведения: валидное возвращаемое значение, ошибка файловой системы или ошибка JSON.parse. Мы обрабатываем ошибки с помощью простого метода try / catch, как вы привыкли делать синхронное программирование на других языках. Теперь давайте сделаем хорошую асинхронную версию такой функции. Неплохая первоначальная версия с простой проверкой ошибки будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9101112
importfs=require('fs');// Неплохая первоначальная версия .... но неправильная. Мы объясним причины нижеfunctionloadJSON(filename:string,cb:(error:Error,data:any)=>void){fs.readFile(filename,function(err,data){if(err)cb(err);elsecb(null,JSON.parse(data));});}
Всё достаточно просто, функция принимает коллбэк, передавая ему любые ошибки файловой системы. Если нет ошибок файловой системы, он возвращает результат JSON.parse. При работе с асинхронными функциями, основанными на обратных вызовах, следует помнить следующее:
Никогда не вызывайте коллбэк дважды.
Никогда не бросайте ошибку.
Однако эта простая функция не подходит для второго пункта. Фактически, JSON.parse выдает ошибку, если ему передан неверный JSON, в итоге коллбэк никогда не вызывается и приложение вылетает. Это продемонстрировано в следующем примере:
1 2 3 4 5 6 7 8 910111213141516171819
importfs=require('fs');// Неплохая первоначальная версия .... но неправильнаяfunctionloadJSON(filename:string,cb:(error:Error,data:any)=>void){fs.readFile(filename,function(err,data){if(err)cb(err);elsecb(null,JSON.parse(data));});}// загрузка невалидного jsonloadJSON('invalid.json',function(err,data){// Этот код никогда не выполнитсяif(err)console.log('bad.json error',err.message);elseconsole.log(data);});
Было бы наивной попыткой исправить это обернуть JSON.parse в try...catch, как показано в следующем примере:
1 2 3 4 5 6 7 8 910111213141516171819202122232425
importfs=require('fs');// Версия получше .... но всё ещё неправильнаяfunctionloadJSON(filename:string,cb:(error:Error)=>void){fs.readFile(filename,function(err,data){if(err){cb(err);}else{try{cb(null,JSON.parse(data));}catch(err){cb(err);}}});}// загрузка невалидного jsonloadJSON('invalid.json',function(err,data){if(err)console.log('bad.json error',err.message);elseconsole.log(data);});
Тем не менее, в этом коде есть небольшая ошибка. Если коллбэк (cb), а не JSON.parse, выдает ошибку, так как мы завернули ее в try...catch, выполняется catch, и мы снова вызываем коллбэк, т. е. вызываем дважды! Это продемонстрировано в примере ниже:
Это потому что наша функция loadJSON неправильно завернула коллбэк в блок try. Здесь нужно запомнить простое правило.
Простое правило
Держите весь ваш синхронный код в try...catch, кроме случаев, когда вы вызываете коллбэк.
Следуя этому простому правилу, мы имеем полностью функциональную асинхронную версию loadJSON, как показано ниже:
1 2 3 4 5 6 7 8 9101112131415161718
importfs=require('fs');functionloadJSON(filename:string,cb:(error:Error)=>void){fs.readFile(filename,function(err,data){if(err)returncb(err);// Держим весь ваш синхронный код в try catchtry{varparsed=JSON.parse(data);}catch(err){returncb(err);}// кроме случаев, когда вы вызываете коллбэкreturncb(null,parsed);});}
Конечно, этому легко следовать как только вы уже проделали это несколько раз, но, тем не менее, это много шаблонного кода, который нужно писать просто для хорошей обработки ошибок. Теперь давайте рассмотрим более удачный способ борьбы с асинхронным JavaScript с использованием промисов.
Промис может быть в состоянии ожидание(pending), исполнено(fulfilled) или отклонено(rejected).
Давайте посмотрим на создание промиса. Достаточно просто вызвать new с Promise (конструктор промисов). Конструктор промисов передаст resolve и reject функции для определения состояния промиса:
123
constpromise=newPromise((resolve,reject)=>{// resolve / reject функции контролируют варианты завершения промиса});
На варианты завершения промиса можно подписаться с помощью .then (если исполнено) или .catch (если отклонено).
123456789
constpromise=newPromise((resolve,reject)=>{resolve(123);});promise.then((res)=>{console.log('I get called:',res===123);// I get called: true});promise.catch((err)=>{// Не будет вызвано});
1 2 3 4 5 6 7 8 910
constpromise=newPromise((resolve,reject)=>{reject(newError('Something awful happened'));});promise.then((res)=>{// Не будет вызвано});promise.catch((err)=>{console.log('I get called:',err.message);// I get called: 'Something awful happened'});
Быстрое создание исполненного промиса: Promise.resolve(result)
Быстрое создание отклоненного промиса: Promise.reject(error)
Способность создавать цепочки вызова промисов - это главное преимущество, которое предоставляют промисы. Как только у вас появляется промис, вы можете использовать функцию then для создания цепочки промисов.
Если вы возвращаете промис из любой функции в цепочке, .then вызывается только когда его значение resolved:
1 2 3 4 5 6 7 8 910111213141516
Promise.resolve(123).then((res)=>{console.log(res);// 123return456;}).then((res)=>{console.log(res);// 456returnPromise.resolve(123);// Обратите внимание, что мы возвращаем промис}).then((res)=>{console.log(res);// 123 : Обратите внимание, что этот `then`// вызывается со значением resolvedreturn123;});
Вы можете объединить обработку ошибок любой предыдущей части цепочки с помощью одного catch:
1 2 3 4 5 6 7 8 91011121314151617
// Создаём rejected промисPromise.reject(newError('something bad happened')).then((res)=>{console.log(res);// не вызываетсяreturn456;}).then((res)=>{console.log(res);// не вызываетсяreturn123;}).then((res)=>{console.log(res);// не вызываетсяreturn123;}).catch((err)=>{console.log(err.message);// случилось что-то плохое});
сatch фактически возвращает новый промис (фактически создавая новую цепочку промисов):
1 2 3 4 5 6 7 8 910111213
// Создаём rejected промисPromise.reject(newError('something bad happened')).then((res)=>{console.log(res);// не вызываетсяreturn456;}).catch((err)=>{console.log(err.message);// случилось что-то плохоеreturn123;}).then((res)=>{console.log(res);// 123});
Любые синхронные ошибки, добавленные в then (или catch), приводят к тому, что возвращаемый промис не исполняется:
1 2 3 4 5 6 7 8 910111213
Promise.resolve(123).then((res)=>{thrownewError('something bad happened');// генерируем синхронную ошибкуreturn456;}).then((res)=>{console.log(res);// не вызываетсяreturnPromise.resolve(789);}).catch((err)=>{console.log(err.message);// случилось что-то плохое});
Только соответствующий (ближайший) catch вызывается для данной ошибки (так как catch запускает новую цепочку промисов).
1 2 3 4 5 6 7 8 910111213141516171819
Promise.resolve(123).then((res)=>{thrownewError('something bad happened');// генерируем синхронную ошибкуreturn456;}).catch((err)=>{console.log('first catch: '+err.message);// случилось что-то плохоеreturn123;}).then((res)=>{console.log(res);// 123returnPromise.resolve(789);}).catch((err)=>{console.log('second catch: '+err.message);// не вызывается});
сatch вызывается только в случае ошибки в предыдущей цепочке:
1234567
Promise.resolve(123).then((res)=>{return456;}).catch((err)=>{console.log('HERE');// не вызывается});
Дело в том, что:
ошибки переходят к catch (и пропускают любые средние вызовы then) и
синхронные ошибки также отлавливаются любым catch,
эффективно предоставляя нам парадигму асинхронного программирования, которая позволяет обрабатывать ошибки лучше, чем необработанные коллбэки. Подробнее об этом ниже.
Отличительной особенностью TypeScript является то, что он понимает поток значений, проходящих через цепочку промисов:
12345678
Promise.resolve(123).then((res)=>{// res подразумевает тип `number`returntrue;}).then((res)=>{// res подразумевает тип `boolean`});
Конечно, он также понимает развертывание любых вызовов функций, которые могут вернуть промис:
1 2 3 4 5 6 7 8 9101112131415
functioniReturnPromiseAfter1Second():Promise<string>{returnnewPromise((resolve)=>{setTimeout(()=>resolve('Hello world!'),1000);});}Promise.resolve(123).then((res)=>{// res подразумевает тип `number`returniReturnPromiseAfter1Second();// мы возвращаем `Promise<string>`}).then((res)=>{// res подразумевает тип `string`console.log(res);// Hello world!});
Теперь давайте вернемся к нашему примеру loadJSON и перепишем асинхронную версию, которая использует промисы. Все, что нам нужно сделать, это прочитать содержимое файла как промис, затем парсим его как JSON, и все готово. Это показано в следующем примере:
123456
functionloadJSONAsync(filename:string):Promise<any>{returnreadFileAsync(filename)// Use the function we just wrote.then(function(res){returnJSON.parse(res);});}
Использование (обратите внимание, насколько оно похоже на оригинальную версию sync, представленную в начале этого раздела 🌹):
// валидный json файлloadJSONAsync('good.json').then(function(val){console.log(val);}).catch(function(err){console.log('good.json error',err.message);// не вызывается})// несуществующий json файл.then(function(){returnloadJSONAsync('absent.json');}).then(function(val){console.log(val);})// не вызывается.catch(function(err){console.log('absent.json error',err.message);})// невалидный json файл.then(function(){returnloadJSONAsync('invalid.json');}).then(function(val){console.log(val);})// не вызывается.catch(function(err){console.log('bad.json error',err.message);});
Эта функция проще, потому что объединение промисов произвело "loadFile(async) + JSON.parse (sync) => catch" объединение. Также коллбэк не был вызван нами, но вызван цепочкой промисов, поэтому у нас не было возможности совершить ошибку, заключив его в try...catch.
Мы видели, как просто выполнять серию последовательных асинхронных задач с промисами. Просто использованием цепочки вызовов then.
Однако вы, возможно, захотите выполнить серию асинхронных задач, а затем что-то сделать с результатами всех этих задач. Promise предоставляет статическую функцию Promise.all, которую вы можете использовать для ожидания выполнения n промисов. Вы предоставляете ему массив n промисов, и он дает вам массив n resolve значений. Ниже мы показываем последовательный вызов по цепочке, а также параллельно:
// асинхронная функция для имитации загрузки элемента с какого-либо сервераfunctionloadItem(id:number):Promise<{id:number}>{returnnewPromise((resolve)=>{console.log('loading item',id);setTimeout(()=>{// имитируем задержку сервераresolve({id:id});},1000);});}// последовательный вызов по цепочкеletitem1,item2;loadItem(1).then((res)=>{item1=res;returnloadItem(2);}).then((res)=>{item2=res;console.log('done');});// общее время будет около 2с.// параллельный вызовPromise.all([loadItem(1),loadItem(2)]).then((res)=>{[item1,item2]=res;console.log('done');});// общее время будет около 1с.
Иногда вы хотите запустить серию асинхронных задач, но вы получите все, что вам нужно, при условии, что будет решена любая из этих задач. Promise предоставляет статическую функцию Promise.race для этого сценария:
1 2 3 4 5 6 7 8 91011
vartask1=newPromise(function(resolve,reject){setTimeout(resolve,1000,'one');});vartask2=newPromise(function(resolve,reject){setTimeout(resolve,2000,'two');});Promise.race([task1,task2]).then(function(value){console.log(value);// "one"// исполнены обе, но task1 исполнился быстрее});
Обратите внимание, что в NodeJS есть супер-удобная функция, которая выполняет вот такую node style function => promise returning function магию для вас:
1234
/** Пример использования */importfs=require('fs');importutil=require('util');constreadFile=util.promisify(fs.readFile);