async await в JavaScript

May 11, 2018

async/await (асинхронные функции) - основной тренд 2017 года в JavaScript. Асинхронные (async) функции - созданы на основе обещаний и генераторов.

const AsyncFunction = (async () => {}).constructor;
console.log(AsyncFunction); // [Function: AsyncFunction]

const afn = async () => {};
console.log(afn instanceof AsyncFunction); // true

console.log(afn.__proto__.constructor);
console.log(afn.__proto__.__proto__.constructor);
console.log(afn.__proto__.__proto__.__proto__.constructor);

[Function: AsyncFunction]
[Function: Function]
[Function: Object]

async

Добавляя ключевое слово async мы делаем функцию (практически любую, нельзя только для constructor) асинхронной. Асинхронная (async) функция возвращает Promise, значит для получения значения можно воспользоваться методом then:

async function getUser(id) {
    return { id: 1 };
}
getUser(1).then(function(user) {
    console.log('get user on then: ', user); // { id: 1 }
})

Если из асинхронной функции мы вернем непосредственно Promise (return Promise.resolve({id: 1})), мы также получим объект {id: 1}. Асинхронная функция возвращает Promise по умолчанию. То есть, если асинхронная функция непосредственно возвращает обещание, то она не оборачивает его в другое обещание:

async function getUser(id) {
    return { id: 1 };
}

getUser(1).then(user => console.log(user)); // {id: 1}

// equivalent:

async function getUser(id) {
    return Promise.resolve({id: 1});
}

getUser(1).then(user => console.log(user)); // {id: 1}

await

Перед вызовом асинхронной функции нам нужно указать ключевое слово await. Ключевое слово await дожидается выполнения Promise, получает значение и возвращает значение. Ключевое слово await можно использовать только внутри async функции (async).

async function getUser(id) {
    return { id: 1 };
}

async function main() {
    let user = await getUser(1);
    console.log(user);
}

main(); // { id: 1 }

Ключевое слово await необязательно ставить перед вызовом асинхронной (async) функции; это может быть любая функция, которая возвращает Promise.

function getUserOnPromise(id) {
    return new Promise(function(res, rej) {
        res({ id: 1 });
    })
}

async function mainOnPromise() {
    let user = await getUserOnPromise(1);
    console.log('userOnPromise: ', user);
};

mainOnPromise(); // { id: 1 }

Также ключевое слово await необязательно ставить, чтобы дождаться значения от async функции - вы можете использовать обычный синтаксис Promise:

async function inc(a) {
    return a + 1;
}

inc(10).then(function(a) {
    console.log(a);
}); // 11

for await

for await умеет считывать из файлового потока (for await подъодит для любых объектов, которые реализуют asyncIterable контракт):

(async () => {
    const stream = fs.createReadStream('./8-for-await.js', 'utf8');
    for await (const chunk of stream) {
    console.log(chunk); // chunk типа buffer
  }
})();

thenable

Мы можем использовать await даже, когда мы работаем с неасинхронной функцией, например, объект от класса Thenable.

class Thenable {
  constructor() {
    this.thenHandler = null;
    this.next = null;
  }

  then(fn) {
    this.fn = fn;
    const next = new Thenable();
    this.next = next;
    return next;
  }

  resolve(value) {
    const fn = this.fn;
    if (fn) {
      const next = fn(value);
      // если не await'ом ждут значения, а через .then
      if (next) {
        next.then(value => {
          this.next.resolve(value);
        });
      }
    }
  }
}

// Usage

const readFile = filename => {
  const thenable = new Thenable();
  fs.readFile(filename, 'utf8', (err, data) => {
    if (err) throw err;
    thenable.resolve(data);
  });
  // при вызове вернется неразрешенный thenable
  return thenable;
};

(async () => {
    const file1 = await readFile('9-thenable.js');
    console.dir({ length: file1.length });
})();

Итератор

const range = {
    start: 1,
    end: 10,
    [Symbol.asyncIterator]() {
        let value = this.start;
        return {
            next: () => new Promise(resolve => {
                setTimeout(() => {
                    resolve({
                        value,
                        done: value++ === this.end + 1
                    });
                }, 0);
            })
        };
    }
};

console.dir({ range });

(async () => {
    for await (const number of range) {
        console.log(number);
    }
})();

Обрабатываем ошибки

Как обработать ошибки, если перед вызовом функции стоит await? try/catch

async function  getUser(id) {
    try {
        let response = await window['fetch'](` https://jsonplaceholder.typicode.cdddom/users/${id}`);  // typicode.cdddom   - error в URL
        let data = await response.json(); // так как метод json возвращает Promise
        return data;
    } catch (error) {
        throw new Error('Вот засада');
    }

}

async function  main() {
    // хотя можно ограничиться только try/catch из этой функции
    try {
        let user =  await getUser(1);
        console.log('user: ', user);
    } catch (error) {
        console.log('error: ', error);
    }
}

main();

/*--------------------------------------------------*/
const fs = require('fs');
const { readFile } = fs.promises;

(async () => {

    try {
        const file1 = await readFile('1-prototype.js');
        const file2 = await readFile('2-sync')
            .catch(err => { // данная ошибка не дойдет до try
                console.log('Promise...catch');
                console.error(err);
                // вернем правильный файл
                return readFile('2-sync.js');
            });
        // file3 при наличии ошибки в отличие от file2 попадет в try/catch
        const file3 = await readFile('3-async');
        console.dir([file1.length, file2.length, file3.length]);
  } catch (err) {
        console.log('try...catch');
        console.error(err);
  }

})();

Поработаем с реальными примерами:

async function inc(a) {
    return a + 1;
}

const sum = async function(a, b) {
    return a + b;
};

const max = async (a, b) => (a > b ? a : b);

const avg = async (a, b) => {
    const s = await sum(a, b);
    return s / 2;
};

const obj = {
    name: 'Marcus Aurelius',
    async split(sep = ' ') {
    return this.name.split(sep);
  }
};

class Person {
    constructor(name) {
    this.name = name;
  }

  static async of(name) {
    return await new Person(name);
  }

  async split(sep = ' ') {
    return this.name.split(sep);
  }
}

const person = new Person('Marcus Aurelius');

(async () => {

    console.log('await inc(5) =', await inc(5));
    console.log('await sum(1, 3) =', await sum(1, 3));
    console.log('await max(8, 6) =', await max(8, 6));
    console.log('await avg(8, 6) =', await avg(8, 6));
    console.log('await obj.split() =', await obj.split());
    console.log('await person.split() =', await person.split());

})();

////////////////////////////////////////////////

async function main() {
    let user = await getUser(1);
    console.log('user: ', user);
}

// (without keyword async)
function  getUser(id) {
    return window['fetch'](`https://jsonplaceholder.typicode.com/users/${id}`)  //  https://jsonplaceholder.typicode.com - этот URL предоставляет fake json-данные
    .then(function (response) {
        return response.json();  // https://developer.mozilla.org/ru/docs/Web/API/Body/json
    })
}
main();


// OR (with keyword async):

async function  getUser(id) {
    let response = await window['fetch'](`https://jsonplaceholder.typicode.com/users/${id}`);
    let data = await response.json(); // так как метод json возвращает Promise
    return data;
}

async function  main() {
    let user =  await getUser(1);
    console.log('user: ', user);
}
main();

sleep

const sleep = msec => new Promise(resolve => {
    setTimeout(resolve, msec);
});

(async () => {

    console.log('Start sleep: ' + new Date().toISOString());
    console.log('  Sleep about 3 sec');
    // данная функция не блокирует цикл событий
    await sleep(3000);
    console.log('After sleep: ' + new Date().toISOString());

})();

Эмулируем асинхронные вызовы

const pause = () => new Promise(resolve =>
  setTimeout(resolve, Math.floor(Math.random() * 1000))
);

// Asynchronous functions

const readConfig = async name => {
  await pause();
  console.log('(1) config loaded');
  return { name };
};

const doQuery = async statement => {
  await pause();
  console.log('(2) SQL query executed: ' + statement);
  return [{ name: 'Kiev' }, { name: 'Roma' }];
};

const httpGet = async url => {
    await pause();
    console.log('(3) Page retrieved: ' + url);
    return '<html>Some archaic web here</html>';
};

const readFile = async path => {
    await pause();
    console.log('(4) Readme file loaded: ' + path);
    return 'file content';
};

// Usage

(async () => {

    const config = await readConfig('myConfig');
    const res = await doQuery('select * from cities');
    const json = await httpGet('http://kpi.ua');
    const file = await readFile('README.md');
    console.log('Done');
    console.dir({ config, res, json, file });

})();

jsfiddle

http://jsfiddle.net/dnzl/1y0qqqak/embed/js/dark/

Promise.all и await

await Promise.all([ asyncFunc1(), asyncFunc2])
Добавить комментарий