Примите асинхронную природу JavaScript!
Все перечисленные ниже вернутся немедленно, но в них есть место для кода, который вы хотите запустить после того, как что-то произошло.
Методы, которые я здесь описал, предназначены для разных случаев использования и примерно упорядочены по сложности.
Различаются следующие вещи:
- В ожидании выполнения какого-либо условия
- Ожидание завершения набора методов (в любом порядке) перед вызовом одного обратного вызова
- Запуск ряда асинхронных методов с общим состоянием в определенном порядке перед вызовом обратного вызова
Подождите
Ожидание, чтобы увидеть, является ли какое-либо условие истинным, полезно, когда нет доступного обратного вызова, чтобы сообщить вам, когда что-то закончилось.
Это довольно базовая реализация, предполагающая, что условие в какой-то момент станет истинным. С помощью нескольких настроек его можно расширить, чтобы сделать его еще более полезным (например, путем установки ограничения вызовов). (Я написал это только вчера!)
function waitFor(predicate, successCallback) {
setTimeout(function () {
var result = predicate();
if (result !== undefined)
successCallback(result);
else
waitFor(predicate, successCallback);
}, 100);
}
телефонный код:
beforeEach(function (done) {
selectListField('A field');
waitFor(function () {
var availableOptions = stores.scrapeStore(optionStore);
if (availableOptions.length !== 0)
return availableOptions;
}, done);
});
Здесь я вызываю что-то, что загружает extjs 'store' и ждет, пока в хранилище что-то будет, прежде чем продолжить (beforeEach - это фреймворк для тестирования жасмина).
Подождите, пока завершится несколько вещей
Еще одна вещь, которую мне нужно было сделать, - запустить один обратный вызов после завершения загрузки различных методов. Вы можете сделать это так:
createWaitRunner = function (completionCallback) {
var callback = completionCallback;
var completionRecord = [];
var elements = 0;
function maybeFinish() {
var done = completionRecord.every(function (element) {
return element === true
});
if (done)
callback();
}
return {
getNotifier: function (func) {
func = func || function (){};
var index = elements++;
completionRecord[index] = false;
return function () {
func.applyTo(arguments);
completionRecord[index] = true;
maybeFinish();
}
}
}
};
телефонный код:
var waiter = createWaitRunner(done);
filterList.bindStore = waiter.getNotifier();
includeGrid.reconfigure = waiter.getNotifier(function (store) {
includeStore = store;
});
excludeGrid.reconfigure = waiter.getNotifier(function (store) {
excludeStore = store;
});
Вы либо просто ждете уведомлений, либо можете также обернуть другие функции, которые используют значения, переданные функции. После вызова всех методов будет запущен done
.
Запуск асинхронных методов в порядке
Я использовал другой подход, когда у меня была серия асинхронных методов для вызова подряд (снова в тестах). Это чем-то похоже на то, что вы можете получить в библиотеке Async - серия делает примерно то же самое, и я сначала немного прочитал эту библиотеку, чтобы посмотреть, получилось ли то, что я хотел. Я думаю, что у меня есть более приятный API для работы с тестами (+ это было интересно реализовать!).
//provides a context for running asyncronous methods syncronously
//the context just provides a way of sharing bits of state
//use run to execute the methods. These should be methods that take a callback and optionally the context as arguments
//note the callback is provided first so you have the option of just partially applying your function to the arguments you want
//instead of having to wrap even simple functions in another function
//when adding steps you can supply either just a function or a variable name and a function
//if you supply a variable name then the output of the function (which should be passed into the callback) will be written to the context
createSynchronisedRunner = function (doneFunction) {
var context = {};
var currentPosition = 0;
var steps = [];
//this is the loop. it is triggered again when each method finishes
var runNext = function () {
var step = steps[currentPosition];
step.func.call(null,
function (output) {
step.outputHandler(output);
currentPosition++;
if (currentPosition === steps.length)
return;
runNext();
}, context);
};
var api = {};
api.addStep = function (firstArg, secondArg) {
var assignOutput;
var func;
//overloads
if (secondArg === undefined) {
assignOutput = function () {
};
func = firstArg;
}
else {
var propertyName = firstArg;
assignOutput = function (output) {
context[propertyName] = output;
};
func = secondArg;
}
steps.push({
func: func,
outputHandler: assignOutput
});
};
api.run = function (completedAllCallback) {
completedAllCallback = completedAllCallback || function(){};
var lastStep = steps[steps.length - 1];
var currentHandler = lastStep.outputHandler;
lastStep.outputHandler = function (output) {
currentHandler(output);
completedAllCallback(context);
doneFunction();
};
runNext();
};
//this is to support more flexible use where you use a done function in a different scope to initialisation
//eg the done of a test but create in a beforeEach
api.setDoneCallback = function (done) {
doneFunction = done;
};
return api;
};
телефонный код:
beforeAll(function (done) {
var runner = createSynchronisedRunner(done);
runner.addStep('attachmentInformation', testEventService.getAttachmentCalled.partiallyApplyTo('cat eating lots of memory.jpg'));
runner.addStep('attachment', getAttachment.partiallyApplyTo("cat eating lots of memory.jpg"));
runner.addStep('noAttachment', getAttachment.partiallyApplyTo("somethingElse.jpg"));
runner.run(function (context) {
attachment = context.attachment;
noAttachment = context.noAttachment;
});
});
PartiallyApplyTo - это в основном переименованная версия реализации Curry Дуга Крокфорда. Многое из того, с чем я работаю, принимает в качестве последнего аргумента обратный вызов, поэтому простые вызовы можно выполнять, как это, вместо того, чтобы обернуть все с помощью дополнительной функции.
Надеюсь, некоторые идеи там могут быть полезны людям.