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

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

Судя по ридми на github в библиотеке отсутствует обработка ошибок


Following operators are not yet supported:
  • const
  • let
  • for… of
  • try… catch
    а значит вся её ценность равна нулю.

Тот же костыльPromise отлично с этим справляется.


Кстати, так вами любимый async/await тоже использует Promise


When async function is called, it returns a promise. When the async function returns a value, the promise will be resolved with the returned value. When the async function throws an exception or some value, the promise will be rejected with the thrown value.

Async function can contain await expression, that pauses the execution of the async function and waits for the passed promise's resolution, and resumes the async function's execution and returns the resolved value.

подробнее можно например тут посмотреть https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

try...catch недоступно только в теле самой вызываемой функции, которая запускается через SynJS.run (myTestFunction1 в статье). В любых других функциях, в том числе вызываемых из myTestFunction1 try...catch доступен.
Кстати, так вами любимый async/await тоже использует Promise
да, но async/await стал возможен не благодаря Promises, а благодаря генераторам, которые нативно позволяют останавливать/возобновлять выполнение контекста в движке. Чтобы получить Promises достаточно подключить небольшой полифил, а вот чтобы получить остановку/возобновление контекста исполнения в ES2015 в Babel фактически пришлось создать State machine, парсить и эмулировать исполнение операторов функции примерно так же, как это делает SynJS.
НЛО прилетело и опубликовало эту надпись здесь

На ноде уже есть async/await.

НЛО прилетело и опубликовало эту надпись здесь
IE ближайшие пару лет никуда не денется, особенно из западных компаний, у которых обычно есть policies не обновлять ПО до самых последних версий.
Интересно, как любое упоминание IE моментально набирает минусы. Но вот я сейчас работаю с 2-мя западными компаниями, которые сидят на Windows 7 и IE 9, и обновлений на Edge даже и не планируют пока. Они совместимость со своим старьем всегда ставят в условия ТЗ. Не отказывать же им…
Отказывать, внезапно.
http://bluebirdjs.com/ зацените штуку, работает начиная с ие7 весит меньше чем ваша библиотека, полностью совместима с promise, проверена временем, и не имеет недостатков которые у вас пока еще есть.

Как я вас понимаю!

Функция, вызываемая через SynJS.run, ничего не будет знать о своем окружении, так как она не вызывается JS-движком напрямую:
var i=123;
function myTestFunction1() {
    console.log(i); <--- i будет undefined
}
SynJS.run(myTestFunction1,obj, function () {
    console.log('done all');
});

К ней надо относится так, как если бы она была определена где-то в другом модуле, и передавать необходимые переменные через параметры, obj (this внутри функции), или global.
SynJS.run(myFetches,null,modules,urls,function () {
        console.log('done');
    });

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


SynJS.run(myFetches,null,modules,urls,function () {
        SynJS.run(myAfterFetches,null,modules,??result?? /*где бы его получить*/,function () {
        console.log('done');
    });

    });

или есть техника как избежать SynjsHell, простите за каламбур

Если в myFetches есть return, то его результат будет параметром колбека:
function myFetches(modules, urls) {
    ...
    return 123;
}
SynJS.run(myFetches,null,modules,urls,function (res) {
        console.log(res); <-- напечатает 123
    });

Можно вызывать вложенные SynJS.run, в этой части все как в обычном JavaScript. Ограничения касаются, в основном, функции, которая исполняется через SynJS.run,
В 3-м примере показано как SynJS.run вызывается рекурсивно чтобы обойти дерево.
В 3-м примере показано как SynJS.run вызывается рекурсивно чтобы обойти дерево.

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


Можно вызывать вложенные SynJS.run, в этой части все как в обычном JavaScript

ну то есть от callback hell мы никуда не ушли?


SynJS.run(myFetches,null,modules,urls,function (res) {
        // обработка ошибки 1?
        SynJS.run(filterTree, null,modules,res,function (res) {
            // обработка ошибки 2?
           SynJS.run(doSomethingWithFilteredTree, null,modules,res,function (res) {
               // обработка ошибки 3?
               console.log(res); 
           });
        });
    });

@amaksr так?

ну то есть от callback hell мы никуда не ушли?

Мы ушли от callback-hell только внутри функции, вызываемой через SynJS.run. Все остальные функции подчиняются тем же законам JavaScript, что и раньше. Точно так же в случае async/await мы должны объявить функцию через async, если мы собираемся в ней ждать коллбеки (ну и плюс еще сделать оболочки с Promises для функций с колбеками, которые мы собираемся вызывать).

Вообще этот момент мне более всего непонятен: почему нельзя было ввести в JavaScript оператор, который бы приостанавливал исполнение контекста без блокировки других контекстов, лет так 10 назад? Тогда никто и не знал бы сейчас про callback hell. Почему только недавно такая возможность появилась, но и то в виде генераторов? Выглядит так, что кто-то сильно ошибся с дизайном когда-то давно, поэтому мы сейчас и имеем все эти костыли.
Добавить можно, но так как в предложенном тесте всего лишь одна асинхронная операция, а в функциях практически нет логики, то смысла это особого не имеет, и код только раздуется.

Что предложите добавить, чтобы это обрело смысл?

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

var res1 = query("select 1");
SynJS.wait();
var res2 = query("select 2");
SynJS.wait();
var res = res1 + res2;

можно было бы сократить до

var a = query("select 1") + query("select 2");

Наверное попробую это реализовать…

Не пойму, почему немного не потерпеть пока async/await пойдет в масссы а пока пересидеть на babel?

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

Я не фронтендщик, возможно чего-то не понимаю, но разве IE 2 может в ajax? Мы же тут про async говорим… Опять же, имеет ли такое значение скорость javascript-а, когда у нас тут асинхронный запрос на сервер?

Мне кажется, имелось в виду "не менее двух предпоследних версий", а не археология

Да, точно. Мне аж больно за коллег стало.

Про async/await чистая ложь
Во-первых, transform-async-to-generator просто заменяет все await на yield и оборачивает функцию в вызов функции co (github.com/tj/co, можно свою реализацию подставить). Также есть asynctogen, с которым нет смысла тащить babel, если других фич не используете
Во-вторых, у вас включен regenerator, и поэтому код страшный

Как без включения regenerator будет работать конвертация async/await?

regenerator преобразует генераторы в стейт-машину которая будет работать и на старых версиях V8, которые не поддерживают их.


А async/await может спокойно без него работать, если упомянутые выше генераторы поддерживаются движком (благо они уже более распространены)

Я именно про случай преобразования для движков, не поддерживающих async/await
НЛО прилетело и опубликовало эту надпись здесь
Не совсем понимаю вас. Смотрим на сводную таблицу, к примеру, здесь:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

Если хром будет не 55, а 54 версии, он, судя по всему, не будет поддерживать async. Как поддержка генераторов ему поможет?
НЛО прилетело и опубликовало эту надпись здесь
Мы оба недопоняли друг друга :) Спасибо за пояснения.

Еще про генераторы забыли. Они поддерживаться уже давно.

Генераторы поддерживаются не везде. Они необходимы чтобы нативно приостанавливать выполнение функции и реализовать ожидание чего-то (возможность приостанавливаться и ждать как раз и позволиляет избавится от колбеков), но в IE например их нет. Поэтому Babel фактически создает свою State machine, парсит код функции и исполняет ее операторы сам, без вызова этой функции напрямую.
НЛО прилетело и опубликовало эту надпись здесь
Я тестировал вот этой програмкой:
спойлер
global.SynJS = require("synjs");

function publishLevel(modules) {

  var levels=[];
  var start = new Date().getTime();
  for(var i=0; i<100000; i++) {
    var user = modules.getUser(i);
    var can_create = modules.canCreate(user);
    if(!can_create)
    var level = modules.saveLevel(user, null);
    levels.push(level);
  }

  return new Date().getTime()-start;
}

function getUser(user_id) {
        return {
        id: user_id,
        nickname: 'tlhunter'
      };
}

function canCreate(user) {
  return user.id === 12;
}

function saveLevel(user, data) {
  return {
      id: 100,
      owner: user.nickname,
      data: data
    };
}
var modules = {
                getUser: getUser,
                canCreate: canCreate,
                saveLevel: saveLevel
};

SynJS.run(publishLevel,null,modules,function(ret){
        console.log('ret=',ret);
})




В SynJS она завершилась за 87мс. Эквивалентный код с одним async, 3-мя await-ами, промисами (но без setTimeout-ов и других длительных функций) после Babel-я работал около 3 с.

Нативные генераторы не проверял.

Зачем в этой абсолютно синхронной программе вообще использовать SynJS?

Все медленные функции были убраны специально для оценки быстродействия самого SynJS

В результате SynJS не делает ровным счётом ничего.

Ну да. А сгенерированный Babel-ем код тоже не делает ничего, но только медленно.

Нет, он много чего делает. Пусть и впустую. Зачем вы вставляете async и await если функции синхронные?

"Нативные" промисы очень медленные. Надо брать Bluebird

Ко всему прочему хотел бы докинуть что у вас же вышли старые добрые fiber'ы и они уже давно есть в NodeJS как одно из решений но от них отказываются. Как-то генераторы надежнее выглядят и сводятся к тому же.

Но это же не решение. Суть callback hell — в том, что с ним сложно обрабатывать ошибки читаемым способом. Этот велосипед не улучшает вообще ничего.
Что написали велосипед — хорошо, возможно что то поняли. Но не надо это использовать нигде, хотя бы потому, что
оно парсит код. От этого мало того, что отваливается вся оптимизация, которые делает браузер, но и уж точно вы не учли все возможные виды написания кода, который уж точно не уместится в 34kb.
Просто используйте Promise, и не нужно мучить ни себя не других. Если кто то внезапно столкнется с проблемами, которые вызывает ваш велосипед — цена переписывания кода с синхронного с костылями на асинхронный будет слишком высока, проще сразу писать нормально.

Да и полифил promise не минифицированный занимает всего 8kb, а уменьшенный 3kb.
Распарсенные операторы в SynJS компилируются в функции через eval (насколько я знаю Node тоже загружает файлы через eval). Поэтому все оптимизации, происходящие в eval никуда не отвалятся. К тому же делается это только 1 раз при первом вызове функции.

Promise не поможет мне написать понятный код, в котором перемешаны функции с колбеками, циклы, условия и рекурсия. Написать то конечно можно, но достаточно взглянуть на StackOverflow — там каждый день идут десятки вопросов как реализовать тот или иной алгоритм с Promises, и предлагаемые решения не выглядят интуитивно понятно.
Заказчику разве не всё равно, какой дрелью ему отверстие в стене будут делать?
Ну хоть что бы себя пожалеть, не рефакторить лишний раз, да и все равно думаю просветление придёт рано или поздно к автору и он от этого откажется, а поддерживать потом что то придется с этим. Самое то страшное, вот встретили вы багу где то, например в парсере, починили, а кто даст гарантию что где то что то не отвалилось?
НЛО прилетело и опубликовало эту надпись здесь

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

Выглядит так, будто бы оно просто распарсивает функцию и делает из неё генератор.

Почему они с тем же успехом не могли распарсивать функцию и впиливать поддержку await/async? Если это так хочется делать в рантайме, а не транспилировать.

Парсер классный, аж волосы зашевелились )

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

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

А так да, велосипед тот ещё, во — первых рантайм, а во — вторых потеря контекста.

Какое уродливое не-решение никаких проблем. Не знаю зачем вы его придумали, и для чего это может пригодиться. Возьмите уж тогда хотя бы promise-hell, всяко менее вырвиглазнее, да и без runtime парсинга js-а.
Но вот одну интересную мысль из топика я таки выцепил. Вы упомянули о том, что babel-polyfil весит под 100 KiB. Я с возмущением и словами "да не может быть, какая ерунда" полез смотреть и опешил. 97 KiB. Гхм. Грусть-печаль меня охватила.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Сравните с этим:


let action = new Atom( 'action' , ()=> {
    let responses = []
    for( let i = 0 ; i < 4 ; ++i ) responses.push( asyncRequest( i ) )
    responses.forEach( display )
} )
action.value()
НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

Что же мешает его добавить?

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Про атомы. Тут про них подробнее. Впрочем, CanJS тоже поддерживает "computed".


Да, ()=> — это та же лямбда. То же что и function(){ ... }, только сохраняет this.


Аккуратно писать на колбэках крайне сложно.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Я же написал или.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

TortoiseGit, например.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Для вопросов есть специальный ресурс: https://toster.ru
А так, в Chrome Dev Tools можно смотреть обработчики повешенные на выделенный элемент и всех его предков.

НЛО прилетело и опубликовало эту надпись здесь

Там через скоупы можно добраться до нужной функции.

НЛО прилетело и опубликовало эту надпись здесь

Ну, простого решения тут, к сожалению, нет. Создатели библиотек зачастую забивают болт на том, как код на них потом отлаживать.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Это решение для очень простой проблемы, так как все итерации однотипные, и не связаны никакой логикой. Такую проблему можно решать легко и через рекурсию, и через Promises, и через async.js. Все становится гораздо сложнее если вам надо делать циклы или условия в каждой итерации, и по их результатам вызывать другие асинхронные функции и ждать колбеков от них
НЛО прилетело и опубликовало эту надпись здесь
Коллеги, подскажите ненастоящему JS-нику, а способ решения проблемы с callback hell с помощью отказа от инлайнинга функций — годный/в стиле JS?

Бонусом к этому способу идет соблюдение SRP, нормальное юнит-тестирование и все такое.
НЛО прилетело и опубликовало эту надпись здесь
На данный момент я использую SynJS для написания браузерных тестов

Т.е. там, где даже мегабайты «лишнего» кода некритичны, да ещё и не продакшен это.


Ну и даже не заглядывая в исходники ставлю на полную неработоспособность при использовании любого обфускатора.


P.S. А ведь всё могло быть по другому
Будущее, судя по всему, за async/await, но пока это будущее не наступило, и многие движки эту возможность не поддерживают.

Кроме того, Babel тянет за собой около 100 кб минифицированного кода «babel-polyfill», а сконвертированный код работает медленно

Посмотрев на все это, я решил написать свой велосипед — SynJS свою урезанную версию реализации async/await на голой Эсприме...

Инструмент новый, пока обкатывается в тестах, вроде показывает себя хорошо, а значит будет и в продакшене.

Насчет обфускатора вы скорее всего правы, но иной раз код с Promises, логикой с рекурсией, циклами и колбеками выглядит так, что никакого обфускатора и не надо.
Ой, ну не надо, продуманная декомпозиция решат всё.

Забыл добавить: Но, обычно, причина callback/promise hell, банальная лень.

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

Мы запретили на одном из крупнейших проектов брать анонимные функции вообще (трудно утечки с ними контролировать) и не было никакого колбек-хела, ибо приходилось правильно декомпозировать

Блин, хабр, что ты сделал.


Насчет обфускатора вы скорее всего правы, но иной раз код с Promises, логикой с рекурсией, циклами и колбеками выглядит так, что никакого обфускатора и не надо.

Ой, ну не надо, продуманная декомпозиция решат всё.


Вот вы пишите про какие-то там задачи, в которых Callback/Promise страшны, но в статье их не приводите, а показываете какие-то элементарные вещи.


Потом идет пример на async/await, дальше пугаем babel-polyfill (который содержит поддержку генераторов, итераторов и много чего), говорим что спасение есть, это SynJS и… не приводим примера той же задачи на неё, а показываем какую-то муть myTestFunction1 и c setTimeout.


P.S. Обфускатор (он же и минификатор), сейчас используют все по умолчанию.

Серьезно? Вот это лучше промисов?
Буду этим джунов пугать!

Почему в каждой статье про «callback hell» всегда приводят пример самого тупого подхода к написанию кода?
fetch(“list_of_urls”, function(array_of_urls){
    for(var i=0;  array_of_urls.length; i++) {
        fetch(array_of_urls[i], function(profile){
            fetch(profile.imageUrl, function(image){
                ...
            });
        });
    }
});


Почему в качестве решения люди предлагают использовать сторонние библиотеки, и даже хотят вводить async/await, которые по факту не решают проблему, а лишь маскируют ее?

Разве не легче просто писать хороший код, который будет сам за себя говорить, что он делает?
fetch(“list_of_urls”, _loadAllFetchedUrls);

function _loadAllFetchedUrls(array_of_urls) {
    for(var i=0;  array_of_urls.length; i++) {
        fetch(array_of_urls[i], _loadProfileImage);
    }
}

function _loadProfileImage(profile){
    fetch(profile.imageUrl, _showProfileImage);
}


В данном случае все функции будут поддаваться оптимизации со стороны js движка, не будет генерироваться лишний мусор, сам код становится читабельным — имя функции сразу говорит, что будет сделано после загрузки.
Есть недостаток в том, что всю цепочку не видно, но это еще ни разу не было критичной проблемой, при этом любая более-менее хорошая IDE покажет всю цепочку вызовов до конкретного callback'а.

По-моему такой подход решает проблему «callback hell» без каких-либо сторонних библиотек и без нововведений на уровне языка (async/await).

Я, конечно, могу ошибаться и если это так, буду рад услышать в чем именно я ошибся.
По-моему такой подход решает проблему «callback hell» без каких-либо сторонних библиотек и без нововведений на уровне языка (async/await).

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

Пример из живого кода
    async openChild(id)
    {
        try
        {
            const cached = id in this.state.map;
            let widget = cached && this.state.map[id];

            if(!cached)
            {
                this.updateHabData(this.activeId, { habLoading: true });
                widget = await this.load(id);
                this.updateHabData(this.activeId, { habLoading: false });
            }

            await this.updateHabData(id,
                {
                    id,
                    title: widget.title,
                    widget,
                    loading: false,
                    habLoading: false,
                    failed: false
                });

            this.setActiveId(widget.id);
            this.navigator.pushPage(this.createRoute(widget));

            if(cached)
            {
                const widget = await this.load(id); // обновляем
                this.updateHabData(id, { widget });
            }
        }
        catch(err)
        {
            this.updateHabData(this.activeId, { habLoading: false });
            this.notifyError(err, this.l('failLoad'));
        }
    }

Его можно переписать в promise-стиле. Хотя он при этом значительно потеряет в наглядности. А если переписать его на множество разрозненных методов с кучей callback-ов без замыканий, то потребуется куча документации, длинные идиотские названия методов и пр., чтобы в этом вообще не сломать ногу.


А если этот openChild сам по себе является частью нетривиальной цепочки? Вам придётся плодить много-много сущностей на любой чих, а чтобы многочисленные их методы не путались с друг другом, придётся их ещё изолировать в разные объекты/классы. И так как без контекста такой код будет предельно неочевидным, придётся строчить много-много комментариев, примеров и прочего. А когда выяснится, что половину нужно переделать, т.к. условия изменились… упс. Трагедия.

Спасибо за пример кода, это, наверно, первый адекватный пример, который я увидел, где использование async/await действительно упрощает понимание кода.
Обычно приводят тривиальные и глупые примеры, которые даже под разряд callback hell не подходят.
Там где с await будет последовательно код идти, там где без — асинхронно бесконтрольно:
this.updateHabData(this.activeId, { habLoading: true });
widget = await this.load(id);

— порядок выполнения случайный.

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

Код с душком, будем честными (проверка на cached вызывает недоумение, как и в целом работа с updateHabData?!).

А в чём собственно проблема? :) Расстановка await перед всеми вызовами updateHabData по сути ничего не поменяет. Это так, экономия на спичках (и муках отладки регенератора в случае чего), наверное избыточная, но никакой трагедии уж точно. Не думал что она кого-то смутит.


А что не так с проверкой на cached? Она тут нужна ввиду того, что возможны два сценария:


  1. Данные берутся из кеша и отображаются моментально. В этом случае показываем их сразу, но втихую догружаем обновление
  2. Данных в кеше нет, и мы вынуждены показывать спиннер, дожидаясь загрузки.

Отдельного упоминания заслуживает onsen-ий navigator.pushPage, которые занимается разного рода анимациями перелистывания экранов (ради чего и приходится городить столько кода).

НЛО прилетело и опубликовало эту надпись здесь

Честно говоря не понял вас :(. Что вы имеете ввиду? И про какие события идёт речь?

Расстановка await в произвольно порядке говорит только о том, что над кодом особо не думали и так сойдёт.


С cached тоже самое, метод openChild умеет работать с кешем, загружать данные, показывать спиннеры, что-то обновлять (но при этом сам кеш не кладёт), наверно и кофё варить ;] Всё перемешалось в этом методе.

Повторяю, await расставлены не в произвольном порядке, а в таком, что await-ятся только те операции, дождаться окончания которых критично (тут нет никаких race conditions). Повторюсь, внутри .setState от react-компоненты. Часто вы в своём коде передаёте туда callback? Думаю крайне редко, однако такие ситуации могут быть полезными. В данном случае одна такая в коде показана, т.к. state обязательно должен обновиться до того, как будет вызван onsen.navigator.pushState (иначе он упадёт не найдя нужных для render-а данных).


Всё перемешалось в этом методе.

Соглашусь только с этим. Но, увы, некоторые UI задачи требуют заморочек. А cache обновляется внутри load-а.

await выполняет не только функцию "приостановить в ожидании успешного выполнения асинхронной функции", но и "кинуть исключение в случае безуспешного выполнения асинхронной функции". В случае ошибки в updateHabData вы не сможете её никак обработать. Собственно это — основная проблема асинхронного кода — нужно очень внимательно следить за обработкой ошибок при каждом асинхронном вызове и очень легко ошибку проигнорировать, оставив приложение в поломанном состоянии.

А вот с этим соглашусь. Серьёзный аргумент. Что с await, что с promise-ми, да даже callback-ми очень просто пустить некоторые ошибки на самотёк. В nodejs для этого даже костыль в виде uncaughtException воткнули.

Я просто расставил await только в тех местах, где критично дождаться окончания его выполнения (там внутри react-ий setState, который может быть обработан асинхронно). Где не критично пустил его на самотёк.

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

Публикации

Истории