Как стать автором
Обновить

Комментарии 25

Отличная статья, спасибо! Из нового узнал только про requestIdleCallback, но читалось легко (только «обещание» вместо «промиса» немного резало глаз).

Я бы ещё добавил про то, когда начинает исполняться код из промиса. В JS это совсем не так, как, скажем, в Python. В JS функция, переданная промису, начинает исполняться немедленно, без попадания в список таксов или микротасков. В отличие от кода в then, который выполнится в микротаксе даже в том случае, если код внутри промиса зарезолвится сразу, синхронно.

function foo() {
  console.log('before promise');
  const p = new Promise((resolve, reject) => {
    console.log('inside promise');
    resolve(3);
  });
  p.then((value) => console.log('then', value));
  console.log('after promise');
}
foo();
// before promise
// inside promise
// after promise
// then 3

Присоединяюсь к вышесказанному про терминологию. Силами Веб-стандартов собран классный словарик терминов фронтенда: https://github.com/web-standards-ru/dictionary/blob/main/dictionary.md#promises. Промисы они и есть промисы. Статья отличная!

Мне кажется, для полного понимания не хватает освещения некоторых тем: какие способы обновления UI не зависят от загрузки основного потока и будут работать даже при запуске бесконечного цикла while(true) { }, transition, animation, video?
Или, например, какой цикл событий в WebWorker и зачем там requestAnimationFrame и почему нет requestIdleCallback?

Есть статья про это?

Не знаю, не видел. Мне кажется при бесконечном цикле не будет работать css или video, но можно обновлять UI через OffscreenCanvas из воркера. Для него видимо и нужен requestAnimationFrame, но как он там работает точно не могу сказать. И про requestIdleCallback не знаю.

какие способы обновления UI не зависят от загрузки основного потока и будут работать даже при запуске бесконечного цикла while(true) { }

Ситуация постепенно меняется, и отличается от браузера к браузеру. Наверное поэтому мало кто это освещает.

Вот тестовые страницы от Jake Archibald с бесконечным циклом и бесконечными микротасками:

Помню, как в Chrome раньше анимированная гифка замерзала из-за while (true);. Сейчас в Chrome гифка не замерзает. В Firefox гифка замерзает как и раньше, но на while-true-composite.html анимация не останавливается.

Интересно, но непонятно. Гифки и анимации обрабатываются в отдельном потоке? Но если в анимацию добавить изменение размера, то перестает работать, что логично, ведь в while можно поставить условие зависящее от layout.

Это скорее всего вкусовщина, но в современном коде async / await наше все, т.к. цепочки вызовов promise.then недалеко ушли от callback ада. Тут и обработка ошибок проще, и код короче и понятней.

callback hell существует только в головах тех юных JavaScript-еров, которые пока еще не познакомились с композицией функций или не поняли к чему она может привести.

С того момента, как JS получил возможность оперировать функциями как объектами первого класса (то есть с момента своего рождения), ответственность за появление в коде call back hell лежит исключительно на совести программиста, но не языка.

Под спойлером простой пример того, о чем сказано выше:

Однажды заявленные два типа: _typeThing и _typeDo выполняют все необходимые задачи для решения любых вопросов как с асинхронным так и с синхронным кодом, без необходимости использования async await.

Более того, этот пример кода абсолютно работоспособен как на JS спецификации 1997 года, так и современной. И будет ровно таким же рабочим и в 2050 году.

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

(()=>{
  /* *******************************************
   * Define types
   * *******************************************/
	var _typeThing = (
		( theThing ) => (
			( doThen ) => (
				( theThing ?. then )
					&& theThing.then( doThen )
					|| doThen( theThing )
			)
		)
	);
	
	var _typeDo = (
		( doThing ) => (
			( _theThing ) => (
				_typeThing( _theThing( doThing ) )
			)
		)
	);


  
  /* *******************************************
   * Define helpers
   * *******************************************/
	var doLog = (
			( ...theParamList ) => (
				( console.log( ...theParamList ) )
				, theParamList[ theParamList.length - 1 ]
			)
	);

	var doJsonParse = (
		( theJsonStr ) => ( JSON.parse( theJsonStr ) )
	);
	
	var doDelayedJsonParse = (
		( theJsonStr ) => (
			new Promise( 
				(doRes, doRej) =>(
					setTimeout( ()=>doRes(JSON.parse( theJsonStr ) ),2000 )
				)
			)
		)
	);

	var doGetTitle = (
		( theObj ) => ( theObj["title"] )
	)

	var _typeDoTitleLog = (
		( theLogTitle ) => _typeDo( ( theThing )=>doLog(theLogTitle, theThing)  )
	);
	
	var _doJsonParse = _typeDo( doJsonParse );
	var _doDelayedJsonParse = _typeDo( doDelayedJsonParse );
	var _doGetTitle = _typeDo( doGetTitle );


  
  /* *******************************************
   * init data
   * *******************************************/
	var _theJsonStr =  _typeThing(`{"title":"Simple json title"}`);
	var _theFetchedJsonStr = _typeThing(  
		fetch('https://dummyjson.com/products/1')
			.then(res => res.text())
	);


  
  /* *******************************************
   * Main thread
   * *******************************************/
	var _theDelayedTitle = _doGetTitle(_doDelayedJsonParse( _theJsonStr ));
	var _theTitle = _doGetTitle(_doJsonParse( _theFetchedJsonStr ));
	var _theFetchedTitleDelayedJson =  _doGetTitle(_doDelayedJsonParse( _theFetchedJsonStr ));
	
	_typeDoTitleLog("_theDelayedTitle: ")( _theDelayedTitle );
	_typeDoTitleLog("_theTitle: ")( _theTitle );
	_typeDoTitleLog("_theFetchedTitleDelayedJson")(_theFetchedTitleDelayedJson );
})()

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



Ну и чем этот ваш спойлер лучше сырых промизов?

На мой взгляд, всем подобным материалам не хватает обязательного обозначения того, на какую часть официальной спецификации (и какой именно) опирается автор декларируя те или иные части своего материала. Отдельно важно обозначать тот материал, который представляет из себя авторские вставки или интерпретации известного ему материала.


В силу того, что в опубликованном материале ничего подобного нет, становится совершенно невозможно разобраться - где данные которые отвечают современной (какой-либо) спецификации, а где рыбу заворачивали.

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

Описываемый цикл событий, относится к спецификации HTML5 Event Loops. Описание процесса выполнения кода, относится к спецификации ECMA части Execution Context

 

таким образом получается эдакий стек вызовов.

Таким образом получается не эдакий стек вызовов, а Execution context stack описанный в официально спецификации ECMA

 

Если же мы грузим скрипты асинхронно через async или defer, то велика вероятность, что до загрузки JavaScript браузер успеет отрисовать интерфейс пользователя.

Атрибут defer оказывает влияние только на classic script, при этом выполнение этих скриптов произойдет после окончания парсинга страницы и строго в той последовательности в которой они объявлены (хотя загрузка их может происходить в любом порядке). Очевидно, что в этот момент никаких гарантий отрисованного интерфейса пользователя быть не может. 

Атрибут defer не оказывает никакого влияния на загружаемые модули -module script.

Classic script и module script с атрибутом async ведут себя одинаково и будут выполнены ровно в тот момент, когда они будут загружены - вне привязки к порядку объявления или событиям формирования контента.

При этом парсинг, что для скриптов с defer, что для скриптов с async будет выполнен в отдельном треде.

 

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

Правильнее сказать, что это будет зависит строго от host implementation

 

Стек вызовов будет выполнять встроенные скрипты даже тогда, когда мы запретим в браузере JavaScript. Для примера можно открыть пустую страницу about:blank без JavaScript, выполнить несколько кликов и увидеть, что стек вызовов выполнил код, отвечающий за обработку событий.

Открыв about:blank , Вы не запретили выполнение JavaScript. Вы инициализировали Realm, к которому ни один script не был подключен. Соответственно host среда работает с JavaScript ровно точно так же, как если бы хотя бы один скрипт был загружен. Чтобы отследить поведение с отключенным JS вам нужно нажать ctrl (cmd для apple) +p, ввести команду >disable javascript нажать enter. После чего проводить свои эксперименты. Включить - соответственно ctrl(cmd) +p >enable javascript.

 

 

Задачи, тики и Web API

В этом разделе вы смешали в кучу Jobs - как часть спецификации Ecma, tasks - как часть Event loop стандарта HTML5 и тиков - как часть спецификации Node

Все это совершенно разные - лишь косвенно пересекающиеся области. Никаких Тиков или Task-ов из NodeJs или Tasko-ов из HTML5 в JavaScript НЕТ.

Поэтому ваше утверждение: "Задача — это JavaScript-код, который выполняется в стеке вызовов" - cправедливо для спецификации ECMA и только в случае если Вы имеете ввиду Job.
Ваше утверждение: "Тик — выполнение задачи в стеке вызовов" справедливо только для выполнения кода в среде NodeJs.

 

Потому ваши заявления про

Методы Web API работают либо синхронно, либо асинхронно: первые выполнятся в текущем тике, а вторые в одном из следующих тиков.

Являются чушью поскольку в стандарте HTML5 для HOSTинга JS runtime как и для описания EventLoop никаких тиков не существует.

В результате ваши описания тиков это - то Job в рамках спецификации Ecma, то Task в рамках спецификации HTML5, то на самом деле tick но только в рамках описания работы NodeJs. Как следствие, Ваше описание имеет больше всего общего с описанием цикла событий в NodeJS. Чуть хуже в браузере. И вообще ничего общего с непосредственной средой выполнения JavaScript.

    

Очередь задач

Вследствие того, что как спецификация Ecma, так и спецификация HTML5 вместе с NodeJs имеют свою собственную интерпретацию того что такое Задача - вы обязаны указать о чем именно Вы говорите. Особенно с учетом того, что задачи спецификации HTML5 и NodeJs это производные от спецификации ECMA.

Понимая эту концепцию, можно разобрать одну особенность таймеров в JavaScript, которые тоже являются асинхронным API.

Нельзя. Потому, что никаких таймеров в JavaScript нет. SetTimeout - это API предоставляемое в соответствии со спецификацией HTML5, и это API никакого отношения к JavaScript не имеет. В JavaScript вообще не может существовать никаких таймеров или событий, кроме того, что описан в рамках концепции Jobs. Любые реализации EventLoop это host implemented API, которые в зависимости от HOST могут быть совершенно разными.

 

Но у веб-воркера есть ограничения. Например, внутри нельзя работать с DOM, однако вычислительные задачи будут работать.

То, что Вы назвали ограничением - является следствием спецификации Memory Model, которая прямо связано с работой JS кода в много-поточной среде.

 

RequestIdelCallBack - эта функция имеет ряд ограничений, из-за которых её удобно использовать только для небольших неприоритетных задач без взаимодействия с DOM, например для отправки аналитических данных. Более подробно о requestIdleCallback можно почитать в материале «Using requestIdleCallback» Пола Льюиса.

Никаких ограничений API RequestIdelCallBack не имеет. Ваша ссылка ведет на mdn где написаны рекомендации по эффективному использованию этого API, а статья Пола только подтверждает это. Совершенно нет никакой разницы где вы будете блокировать свой main thread - в основной ветке кода, или при вызове RequestIdelCallBack. С той лишь разницей, что может быть столкнувшись с RequestIdelCallBack придете наконец к очереди задач и эффективному распределению ресурсов для выполнения ее элементов.

 

Ад обратных вызовов (callback hell) — самая частая проблема, которую вспоминают, когда говорят про недостатки функций обратного вызова.

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

Простые наглядные примеры этого комментарием Выше.

 

Не выпускайте Залго

Эта проблема того же порядка что и call back hell, кода программист еще не научился дифференцировать системный код с прикладным.
В результате путается между логикой, которая формирует системный порядок выполнения его кода, и бизнес логикой, которая не должна от него зависеть.

Promise

Обещания создаются через конструктор Promise, который обязательно вызывается через new.

Ничего подобного. Допускаются все возможные для JS варианты вызова конструктора: new, super, reflect и т.д.

Reflect.construct(
  Promise
  , [ ( (doRes, doRej)=> doRes(10) ) ] 
)

 

Внутри обещания вызывается асинхронная операция с функцией обратного вызова.

Ничего подобного. Внутри конструктора вызывается executor синхронно. Причем в случае возврата из Executor-а значения при помощи throw автоматически приводит к вызову Reject связанных методов.

 

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

Никаких пробросов и прочих фантазий на эту тему не произойдет. Каждый последующий then catch или finally будет создавать новый Promise с флагом, о том, что Reject ситуация не обработана, что приведет к генерации сообщений в рамках host implementation. Chrome даст сообщение на каждую новую цепочку.

var theRejectedPromise = Promise.reject("Err");
var theRejectedPromise2 = theRejectedPromise.then( ()=>{});
var theRejectedPromise3 = theRejectedPromise2.then( ()=>{} );

setTimeout( 
  ()=> {
	 theRejectedPromise3
       .catch( (theRes)=> { console.log("catch", theRes) } )
}, 2000)

 

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

Вы пишите чушь. И пишите ее по двум причинам:
1 - Вы никогда не читали спецификацию
2 - В своем примере Вы забыли указать return перед Promise.reject

 Promise.resolve()
    .then(() => {
       Promise.reject('O_o')
    })
    .catch(() => { })

В результате чего вернули undefined, но не Rejected Promise. Как следствие -catch у Вас и не отработал.

 

У обещаний есть две неявные особенности. Во-первых, методы then и catch всегда возвращают новое обещание.

Все методы: then catch finally всегда возвращают новый Promise. Вне зависимости от наличия или отсутствия обработчиков. Вне зависимости от наличия return или throw.

 

Каждый вызов then или catch создаёт новое обещание, значение которого либо undefined, либо явно выставляется через return.

Не до конца верно. Значение нового Promise в случае handler catch, можно изменить посредством throw.

 Promise.reject("Err").catch( ()=>{ throw "new Err"} );
 // Uncaught (in promise) new Err

 

Другая особенность обещаний связана с обработкой ошибок. Функции обратных вызовов, которые передаются в методы обещаний, оборачиваются в try/catch.

Опять нет. Особенность Promise в том, что они всегда возвращают Promise, вне зависимости от того, каким образом завершился Executor. Разница только в том, что если Executor завершился через throw, а не через return ( вы же знаете что throw делает обычный возврат из функции?) то Promise будет отклонен (Rejected) со значением из throw

 

Thenable-объекты [...] Скорее всего, это полифилы обещаний до ES6.

Нет. Дело все в том, что в JS нет ни единой возможности точно установить - объект который перед нами действительно тот, который был создан при помощи конструктора Promise или нет. По этой причине любой thenable object ( объект который имеет метод then) дефакто приравнивается к Promise и принимает полноправное участие во всех алгоритмах связанных с разрешением Promis -ов. От методов Promise.all до асинхронных итераторов.

 

У обещаний есть шесть полезных статических методов. Два из них мы уже разобрали — это Promise.resolve и Promise.reject. Давайте посмотрим на другие четыре.

Все они принимают либо thenable Object либо Primitive value, которое автоматически оборачивается в Promise.resolve

 

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

Нет не массив значений. Они принимают любой itarable object.

Вместо ИГОГО

Характер материала сильно меняется, как только мы, нашим словам, пытаемся найти подтверждение в официальной спецификации. Это дает возможность как систематизировать то, что отвечает реальности (например стандарт EventLoop из спецификации HTML5), так и узнать для себя много нового (например как в рамках спецификации организована работа с промисами)

Никаких пробросов и прочих фантазий на эту тему не произойдет. Каждый последующий then catch или finally будет создавать [...]

Причём тут пробросы и фантазии?


Утверждается простой факт, что в цепочке A.then(B).then(C).catch(D).then(E) при возникновении ошибки в B вызов C будет пропущен, D вызван, после чего управление перейдёт к E в обычном режиме.


Что там происходит внутри или в случае параллельных цепочек — это уже детали.


Вы пишите чушь. И пишите ее по двум причинам: [...] В своем примере Вы забыли указать return перед Promise.reject

Нет, это вы читать не умеете. Вы же сами привели цитату — "если опустить return и создать обещание, установленное на отказ, то последующий catch не сможет его обработать". Автор намеренно его опустил и показал что будет в таком случае.


Все методы: then catch finally всегда возвращают новый Promise. Вне зависимости от наличия или отсутствия обработчиков. Вне зависимости от наличия return или throw.

А автор что написал?


вы же знаете что throw делает обычный возврат из функции

Ну уж нет, throw делает throw completion, а обычный возврат — это return completion.


Нет. Дело все в том, что в JS нет ни единой возможности точно установить — объект который перед нами действительно тот, который был создан при помощи конструктора Promise или нет. [...]

А вот тут уже вы чушь пишете. Для движка установить как объект был создан вообще не проблема, к примеру с массивами он как-то справляется (см. array exotic object и метод Array.isArray).


Протокол Thenable был добавлен для совместимости со сторонними реализациями промисов, он пришёл в стандарт языка из стандарта Promises/A+.

Молоток Мурыч! Дави их спецификациями, дави! А то скоро совсем только нейросетью думать будут. Спасибо за ссылки на нужные разделы.

Навскидку не нашел, тут написано где нибудь явно, что обработчики событий запускаются синхронно? Как то долго пришлось в гитхабе доказывать, что это так и есть.

Я имел ввиду неочевидный для многих момент, что код который триггерит события, выполнит листенеры немедленно и синхронно

Hidden text
const element = document.querySelector('.pingMe');
let syncTest = 'async';
element.addEventListener('pingEvent', (e) => {
  console.log("I'm sync!", e.detail.a);
  syncTest = 'sync';
  e.preventDefault();
});
setTimeout(()=>{
	const event = new CustomEvent('pingEvent', {cancelable: true, detail: {a: 1}});
  console.log('before dispatch', syncTest, event.defaultPrevented);
	element.dispatchEvent(event);
  console.log('after dispatch', syncTest, event.defaultPrevented);
}, 1000);

А, вот вы про что… Тут только спецификацию HTML смотреть. Вот вам нужный раздел: 2.7. Interface EventTarget


Для полного понимания что происходит можно уйти вглубь в раздел 2.9. Dispatching events и не уивдеть никаких намёков на асинхронность.


Ну а чтобы просто убедиться что всё синхронно, достаточно увидеть в разделе ненормативного описания что метод dispatchEvent "returns true if either event’s cancelable attribute value is false or its preventDefault() method was not invoked; otherwise false". Разумеется. вернуть булево значение метод может только при синхронном выполнении.

Да, я понимаю как это все работает :) этож вроде статья про подводные камни при работе с асинхронными процедурами в js. Вот и спросил есть там ремарка про это или нет, . вскользь посмотре Я допер просто от понимания, что preventDefault() по другому и не сделать.

Пункт 9.5 спецификации языка Jobs and Host Operations to Enqueue Jobs явно требует от хоста следующего:


  • Only one Job may be actively undergoing evaluation at any point in time.
  • Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts.

Отличная статья, но в ней не описан один момент, связанный с синхронными XHR. Я знаю, что они obsleted, но иногда без них нельзя обойтись. Так вот, в процессе ожидания данных от xhr, браузер вполне может выполнять другой код. Например, если в это время придет сообщение через вебсокет, то обработчик будет вызван, не дожидаясь, пока отработает задача, висящая в ожидании xhr. По крайней мере, в FF. Когда пишешь на JS, внутренне полагаешься на то, что код строго однопоточный, но, как миниумум функции, где есть синхронные xhr, надо рассчитывать на реентерабельность.

Когда-то давно я видел даже библиотечку, которая реализовывала кооперативную многозадачность через внутренние xhr :)

Я конечно жутко извиняюсь, но статья называется "Полное понимание асинхронности в браузере". Зачем добавлять "в браузере", когда статья про асихронность в ДжаваСкрипте? Еще и есть раздел про Нод.жс.

А что касается браузера - то автор смешно пишет про очередь событий в браузере и сайте, ничего говоря про окна, табы, и ифреймы. Мы точно прочитали статью о программировании, где точность важна, как бы? Или это перевод?

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

А да, есть ещё смешное поведение, если один инстанс сайта открыл local storage (просто записав туда значение) и ушел в длинный синхронный цикл, то второй инстанс (соседний, независимый таб) при обращении к localStorage повиснет ожидая lock сторэджа.

Спасибо автору за статью, было интересно и познавательно, некоторые вещи узнал впервые, некоторые освежил в памяти!

З.ы. ну почему на Хабре даже под полезными статьями всегда куча г@&на в комментариях??? 😄

Как правило, мониторы обновляют картинку с частотой 60 кадров в секунду

Пишу комментарий, смотря в монитор, на котором https://event-loop-tests.glitch.me/raf-frequency.html показывает 120 Гц. Раньше был монитор на 144 Гц. На макбуке - 60 Гц при питании от сети и 30 Гц при питании от аккумулятора. На Samsung A53 - 120 Гц. На других телефонах может быть и 60 и 90 Гц.

Давайте уже отвыкать от мысли, что 60 Гц и 16.(6)мс - это универсальные константы.

Я C#-backender, которому приходится быть fullstacker'ом, и эта статья для меня - то, что нужно. Респкт автору. Отличное понятное изложение, грамотный язык. А это редкость...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий