Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.
Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.
Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.
console.log('1')
console.log('2')
console.log('3')
console.log('1')
setTimeout(function afterTwoSeconds() {
console.log('2')
}, 2000)
console.log('3')
setTimeout
. Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds
.XMLHttpRequest
(XHR), но вы вполне можете использовать тут jQuery ($.ajax
), или более современный стандартный подход, основанный на использовании функции fetch
. И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:// аргумент url может быть чем-то вроде 'https://api.github.com/users/daspinola/repos'
function request(url) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Код обработки успешного завершения запроса
} else {
// Обрабатываем ответ с сообщением об ошибке
}
}
}
xhr.ontimeout = function () {
// Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию
}
xhr.open('get', url, true)
xhr.send();
}
// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis".
doThis(andThenThis)
// Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию
function andThenThis() {
console.log('and then this')
}
// Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант
function doThis(callback) {
console.log('this first')
// Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, '()', иначе ничего не получится
callback()
}
request
:function request(url, callback) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('Timeout')
}
xhr.open('get', url, true)
xhr.send();
}
callback
, поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
request(userGet, function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, function handleReposList(err, repos) {
if (err) throw err
//Здесь обработаем список репозиториев
})
})
})
handleUsersList
;SON.parse
, преобразовываем его, для удобства, в объект;repos_url
— это URL для наших следующих запросов, и получили мы его из первого запроса.handleReposList
. Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.try {
request(userGet, handleUsersList)
} catch (e) {
console.error('Request boom! ', e)
}
function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, handleReposList)
})
}
function handleReposList(err, repos) {
if (err) throw err
// Здесь обрабатываем список репозиториев
console.log('My very few repos', repos)
}
forEach
, здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.const myPromise = new Promise(function(resolve, reject) {
// Здесь будет код
if (codeIsFine) {
resolve('fine')
} else {
reject('error')
}
})
myPromise
.then(function whenOk(response) {
console.log(response)
return response
})
.catch(function notOk(err) {
console.error(err)
})
resolve
и reject
;Promise
. Если код будет выполнен успешно, вызывают метод resolve
, если нет — reject
;resolve
, будет исполнен метод .then
для объекта Promise
, аналогично, если будет вызван reject
, будет исполнен метод .catch
.resolve
и reject
принимают только один параметр, в результате, например, при выполнении команды вида resolve('yey', 'works')
, коллбэку .then
будет передано лишь 'yey'
;.then
, в конце соответствующих коллбэков следует всегда использовать return
, иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;reject
, если следующим в цепочке идёт .then
, он будет выполнен (вы можете считать .then
выражением, которое выполняется в любом случае);.then
в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение .catch
;resolve
или reject
, а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve
, и неуспешному, с вызовом reject
, завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.function request(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send();
})
}
request
, возвращено будет примерно следующее.request
, перепишем остальной код.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const myPromise = request(userGet)
console.log('will be pending when logged', myPromise)
myPromise
.then(function handleUsersList(users) {
console.log('when resolve is found it comes here with the response, in this case users ', users)
const list = JSON.parse(users).items
return Promise.all(list.map(function(user) {
return request(user.repos_url)
}))
})
.then(function handleReposList(repos) {
console.log('All users repos in an array', repos)
})
.catch(function handleErrors(error) {
console.log('when a reject is executed it will come here ignoring the then statement ', error)
})
.then
при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение .then
мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении .catch
.const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код
userRequest
.then(handleUsersList)
.then(repoRequest)
.then(handleReposList)
.catch(handleErrors)
function handleUsersList(users) {
return JSON.parse(users).items
}
function repoRequest(users) {
return Promise.all(users.map(function(user) {
return request(user.repos_url)
}))
}
function handleReposList(repos) {
console.log('All users repos in an array', repos)
}
function handleErrors(error) {
console.error('Something went wrong ', error)
}
.then
раскрывает смысл вызова userRequest
. С кодом легко работать, его легко читать.function
. С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:function* foo() {
yield 1
const args = yield 2
console.log(args)
}
var fooIterator = foo()
console.log(fooIterator.next().value) // выведет 1
console.log(fooIterator.next().value) // выведет 2
fooIterator.next('aParam') // приведёт к вызову console.log внутри генератора и к выводу 'aParam'
return
, используют выражение yield
, которое останавливает выполнение функции до следующего вызова .next
итератора. Это похоже на выражение .then
в промисах, которое выполняется при разрешении промиса.request
:function request(url) {
return function(callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('timeout')
}
xhr.open('get', url, true)
xhr.send()
}
}
url
, но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.function* list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = yield request(userGet)
yield
for (let i = 0; i<=users.length; i++) {
yield request(users[i].repos_url)
}
}
request
, которая она принимает url
и возвращает функцию, которая ожидает коллбэк);users
, для отправки в следующий .next
;users
и ожидаем, для каждого из них, .next
, возвращая, для каждого, соответствующий коллбэк.try {
const iterator = list()
iterator.next().value(function handleUsersList(err, users) {
if (err) throw err
const list = JSON.parse(users).items
// Отправляем список пользователей итератору
iterator.next(list)
list.forEach(function(user) {
iterator.next().value(function userRepos(error, repos) {
if (error) throw repos
// Здесь обрабатываем информацию о репозиториях каждого пользователя
console.log(user, JSON.parse(repos))
})
})
})
} catch (e) {
console.error(e)
}
async
, какую функцию предполагается выполнять асинхронно, и, используя await
, сообщить системе о том, какая часть кода должна ждать разрешения соответствующего промиса.sumTwentyAfterTwoSeconds(10)
.then(result => console.log('after 2 seconds', result))
async function sumTwentyAfterTwoSeconds(value) {
const remainder = afterTwoSeconds(20)
return value + await remainder
}
function afterTwoSeconds(value) {
return new Promise(resolve => {
setTimeout(() => { resolve(value) }, 2000);
});
}
sumTwentyAfterTwoSeconds
;afterTwoSeconds
, который может завершиться вызовом resolve
или reject
;.then
, где завершается операция, отмеченная ключевым словом await
, в данном случае — это всего одна операция.request
к использовании в конструкции async/await
:function request(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send()
})
}
async
, в которой используем ключевое слово await
:async function list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = await request(userGet)
const usersList = JSON.parse(users).items
usersList.forEach(async function (user) {
const repos = await request(user.repos_url)
handleRepoList(user, repos)
})
}
function handleRepoList(user, repos) {
const userRepos = JSON.parse(repos)
// Обрабатываем тут репозитории для каждого пользователя
console.log(user, userRepos)
}
list
, которая обработает запрос. Ещё конструкция async/await
нам понадобится в цикле forEach
, чтобы сформировать список репозиториев. Вызвать всё это очень просто:list()
.catch(e => console.error(e))
async/await
можно почитать здесь.async/await
, как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например — babel.async/await
. Если вы хотите как следует разобраться с тем, о чём мы говорили — поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.$.ajax
и fetch
. Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик — буду благодарен, если расскажете об этом мне.К сожалению, не доступен сервер mySQL