Считается, что мир JavaScript бурно развивается: регулярно выходят новые стандарты языка, появляются новые синтаксические фишки, а разработчики моментально все это адаптируют и переписывают свои фреймворки, библиотеки и прочие проекты с тем, чтобы все это использовалось. Сейчас, например, если вы всё ещё пишете в коде var, а не const или let, то это уже вроде как моветон. А уж если функция описана не через стрелочный синтаксис, то вообще позор…
Однако, все эти const-ы, let-ы, class-ы и большинство других нововведений не более чем косметика, которая хоть и делает код красивее, но действительно острых проблем не решает.
Я думаю, что основная проблема JavaScript, которая уже давным давно созрела и перезрела, и которая должна была быть решена в первую очередь, это невозможность приостановить выполнение, и как следствие, необходимость все делать через callbacks.
Чем хороши callbacks?
На мой взгляд только тем, что дают нам событийность и асинхронность, что позволяет мгновенно реагировать на события, проделывать большую работу в одном процессе, экономить ресурсы и т.п.
Чем плохи callbacks?
Первое, с чем обычно сталкивается новичок, это тот факт, что с ростом сложности код быстро превращается в малопонятные многократно вложенные блоки — «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){ ... }); }); } });
Во-вторых, если функции с колбеками соединены друг с другом логикой, то эту логику приходится дробить и выносить в отдельные именованные функции или модули. Например, код выше выполнит цикл «for» и запустит множество fetch(array_of_urls[i]… мгновенно, и если array_of_urls слишком большой, то движок JavaScript зависнет и/или упадет с ошибкой.
С этим можно бороться путем переписывания цикла «for» в рекурсивную функцию с колбеком, но рекурсия может переполнить стек и также уронить движок. Кроме того, рекурсивные программы труднее для понимания.
Другие пути решения требуют использования дополнительных инструментов или библиотек:
- Promises – позволяет писать код колбеков внутри неких объектов. В результате это те же колбеки, но меньшей вложенности и соединенные друг с другом в цепочки:
firstMethod().then(secondMethod).then(thirdMethod);
На мой взгляд Promises это костыль, потому что
- цепочки вызывают функции только в одном заданном порядке,
- если порядок может менятся в соответсвии с какой-то логикой, по-прежнему приходится дробить логику в колбеках на отдельные функции,
- для кодирования логики между функциями по-прежнему приходится что-то изобретать, вместо того, чтобы просто пользоваться стандартными операторами if, for, while и т.п.
- логика с Promises выглядит малопонятно.
- async (библиотека) — позволяет объявить массив функций с колбеками, и исполнять их одну за другой, или одновременно. Недостатки те же, что и у Promises.
- async/await – новая возможность в JavaScript, основанная на generators, позволяет останавливать и возобновлять исполнение функции.
Будущее, судя по всему, за async/await, но пока это будущее не наступило, и многие движки эту возможность не поддерживают.
Чтобы иметь возможность исполнять код с async/await на актуальных на данный момент движках JavaScript 2015, были созданы транспиляторы — преобразователи кода из нового JavaScript в старый. Самый известный из них, Babel, позволяет конвертировать код Javascript 2017 с async/await в JavaScript 2015 и запускать его на практически всех используемых в данный момент движках.
Выглядит это примерно так:
Исходный код на JavaScript 2017:
async function notifyUserFriends(user_id) { var friends = await getUserFriends(user_id); for(var i=0; i<friends.length; i++) { friend = await getUser(friends[i].id); var sent = await sendEmail(freind.email,"subject","body"); } }
Конвертированный код на JavaScript 2015:
"use strict"; var notifyUserFriends = function () { var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(user_id) { var friends, i, sent; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return getUserFriends(user_id); case 2: friends = _context.sent; i = 0; case 4: if (!(i < friends.length)) { _context.next = 14; break; } _context.next = 7; return getUser(friends[i].id); case 7: friend = _context.sent; _context.next = 10; return sendEmail(freind.email, "subject", "body"); case 10: sent = _context.sent; case 11: i++; _context.next = 4; break; case 14: case "end": return _context.stop(); } } }, _callee, this); })); return function notifyUserFriends(_x) { return _ref.apply(this, arguments); }; }(); function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
Чтобы иметь возможность отлаживать такой код, необходимо настроить и задействовать многое из того, что перечислено в этой статье.
Всё это само по себе требует нетривиальных усилий. Кроме того, Babel тянет за собой около 100 кб минифицированного кода «babel-polyfill», а сконвертированный код работает медленно (на что косвенно намекают многочисленные конструкции case номер_строки в сгенерированном коде).
Посмотрев на все это, я решил написать свой велосипед — SynJS. Он позволяет писать и синхронно исполнять код с колбеками:
function myTestFunction1(paramA,paramB) { var res, i = 0; while (i < 5) { setTimeout(function () { res = 'i=' + i; SynJS.resume(_synjsContext); // < –- функция для сигнализации, что колбек закончен }, 1000); SynJS.wait(); // < – оператор, останавливающий исполнение console.log(res, new Date()); i++; } return "myTestFunction1 finished"; }
Исполнить функцию можно следующим образом:
SynJS.run(myTestFunction1,null, function (ret) { console.log('done all:', ret); });
Результат будет такой:
i=0 Wed Dec 21 2016 11:45:33 GMT-0700 (Mountain Standard Time) i=1 Wed Dec 21 2016 11:45:34 GMT-0700 (Mountain Standard Time) i=2 Wed Dec 21 2016 11:45:35 GMT-0700 (Mountain Standard Time) i=3 Wed Dec 21 2016 11:45:36 GMT-0700 (Mountain Standard Time) i=4 Wed Dec 21 2016 11:45:37 GMT-0700 (Mountain Standard Time)
По-сравнению с Babel он
- легче (35кб без минимизации),
- не имеет зависимостей,
- не требует компиляции,
- исполняется примерно в 40 раз быстрее (хотя это может быть не так критично при работе с медленными функциями).
SynJS берет указатель на функцию в качестве параметра, парсит эту функцию на отдельные операторы (парсит вложенные операторы рекурсивно, если необходимо), оборачивает их все в функции, и помещает эти функции в древовидную структуру, эквивалентную коду функции. Затем создается контекст исполнения, в котором хранится локальные переменные, параметры, текущее состояние стека, программные счётчики и другая информация, необходимая для остановки и продолжения выполнения. После этого операторы в древовидной структуре исполняются один за другим, используя контекст в качестве хранилища данных.
Функция может быть выполнена через SynJS следующим образом:
SynJS.run(funcPtr,obj, param1, param2 [, more params],callback)
Параметры:
— funcPtr: указатель на функцию, которую надо выполнит синхронно
— obj: объект, который будет доступен в функции через this
— param1, param2: параметры
— callback: функция, которая будет выполнена по завершении
Чтобы можно было дожидаться завершения колбека в SynJS существует оператор SynJS.wait(), который позволяет остановить исполнение функции, запущенной через SynJS.run(). Оператор может принимать 3 формы:
— SynJS.wait() — останавливает исполнение пока не будет вызван SynJS.resume()
— SynJS.wait(number_of_milliseconds) – приостанавливает исполнение на время number_of_milliseconds
— SynJS.wait(some_non_numeric_expr) – проверяет (!!some_non_numeric_expr), и останавливает исполнение в случае false.
С помощью SynJS.wait можно ожидать завершения одного или нескольких колбеков:
var cb1, cb2; setTimeout(function () { cb1 = true; SynJS.resume(_synjsContext); }, 1000); setTimeout(function () { cb2 = true; SynJS.resume(_synjsContext); }, 2000); SynJS.wait(cb1 && cb2);
Чтобы дать сигнал о завершении колбека в основной поток используется функция
SynJS.resume(context)
Обязательный параметр context содержит ссылку на контекст исполнения, который необходимо уведомить (так как каждый вызов SynJS.run создает и запускает отдельный контекст, в системе может существовать одновременно несколько запущенных контекстов).
При парсинге SynJS оборачивает каждый оператор оборачивается в функцию следующим образом:
function(_synjsContext) { ... код оператора ... }
Таким образом можно использовать параметр _synjsContext в коде колбека для сигнализации о завершении:
SynJS.resume(_synjsContext);
Обработка локальных переменных.
При парсинге тела функции SynJS определяет декларации локальных переменных по ключевому слову var, и создаёт для них хеш в контексте исполнения. При обёртывании в функцию код оператора модифицируется, и все ссылки на локальные переменные заменяются ссылками на хеш в контексте исполнения.
Например, если исходный оператор в теле функции выглядел так:
var i, res; ... setTimeout(function() { res = 'i='+i; SynJS.resume(_synjsContext); },1000);
то оператор, обернутый в функцию будет выглядеть так:
function(_synjsContext) { setTimeout(function() { _synjsContext.localVars.res = 'i='+_synjsContext.localVars.i; SynJS.resume(_synjsContext); },1000); }
Несколько примеров использования SynJS
1. Выбрать из БД массив родительских записей, для каждой из них получить список детей
2. По списку URL-ов, получать их один за другим, пока содержимое URL-а не будет удовлетворять условию
var SynJS = require('synjs'); var fetchUrl = require('fetch').fetchUrl; function fetch(context,url) { console.log('fetching started:', url); var result = {}; fetchUrl(url, function(error, meta, body){ result.done = true; result.body = body; result.finalUrl = meta.finalUrl; console.log('fetching finished:', url); SynJS.resume(context); } ); return result; } function myFetches(modules, urls) { for(var i=0; i<urls.length; i++) { var res = modules.fetch(_synjsContext, urls[i]); SynJS.wait(res.done); if(res.finalUrl.indexOf('github')>=0) { console.log('found correct one!', urls[i]); break; } } }; var modules = { SynJS: SynJS, fetch: fetch, }; const urls = [ 'http://www.google.com', 'http://www.yahoo.com', 'http://www.github.com', // This is the valid one 'http://www.wikipedia.com' ]; SynJS.run(myFetches,null,modules,urls,function () { console.log('done'); });
3. В базе данных, обойти всех детей, внуков и т.д. некоторого родителя
global.SynJS = global.SynJS || require('synjs'); var mysql = require('mysql'); var connection = mysql.createConnection({ host : 'localhost', user : 'tracker', password : 'tracker123', database : 'tracker' }); function mysqlQueryWrapper(modules,context,query, params){ var res={}; modules.connection.query(query,params,function(err, rows, fields){ if(err) throw err; res.rows = rows; res.done = true; SynJS.resume(context); }) return res; } function getChildsWrapper(modules, context, doc_id, children) { var res={}; SynJS.run(modules.getChilds,null,modules,doc_id, children, function (ret) { res.result = ret; res.done = true; SynJS.resume(context); }); return res; } function getChilds(modules, doc_id, children) { var ret={}; console.log('processing getChilds:',doc_id,SynJS.states); var docRec = modules.mysqlQueryWrapper(modules,_synjsContext,"select * from docs where id=?",[doc_id]); SynJS.wait(docRec.done); ret.curr = docRec.rows[0]; ret.childs = []; var docLinks = modules.mysqlQueryWrapper(modules,_synjsContext,"select * from doc_links where doc_id=?",[doc_id]); SynJS.wait(docLinks.done); for(var i=0; docLinks.rows && i < docLinks.rows.length; i++) { var currDocId = docLinks.rows[i].child_id; if(currDocId) { console.log('synjs run getChilds start'); var child = modules.getChildsWrapper(modules,_synjsContext,currDocId,children); SynJS.wait(child.done); children[child.result.curr.name] = child.result.curr.name; } } return ret; }; var modules = { SynJS: SynJS, mysqlQueryWrapper: mysqlQueryWrapper, connection: connection, getChilds: getChilds, getChildsWrapper: getChildsWrapper, }; var children={}; SynJS.run(getChilds,null,modules,12,children,function (ret) { connection.end(); console.log('done',children); });
На данный момент я использую SynJS для написания браузерных тестов, в которых требуется имитировать сложные пользовательские сценарии (кликнуть ”New”, заполнить форму, кликнуть ”Save”, подождать, проверить через API что записалось, и т. п.) — SynJS позволяет сократить код, и самое главное, повысить его понятность.
Надеюсь, кому-то он тоже окажется полезен до тех пор, пока не наступило светлое будущее с async/await.
Проект на гитхабе: github.com/amaksr/SynJS
NPM: www.npmjs.com/package/synjs
P.S. Чуть не забыл, в SynJS имеется оператор SynJS.goto(). А почему бы и нет?
ссылка на оригинал статьи https://habrahabr.ru/post/319094/
Добавить комментарий