"Поэтапное" выполнение функций в javascript
Это мой первый пост о stackru, поэтому, пожалуйста, не расстраивайте меня слишком сильно, если я сталкиваюсь с полным пренебрежением или если я не могу сделать себя совершенно ясно.:-)
Вот моя проблема: я пытаюсь написать функцию javascript, которая "связывает" две функции с другой, проверяя завершение первой, а затем выполняя вторую.
Очевидно, что простым решением этой проблемы будет написание мета-функции, которая вызывает обе функции в своей области видимости. Однако, если первая функция является асинхронной (в частности, вызов AJAX), а вторая функция требует данных результата первой, это просто не будет работать.
Моя идея для решения заключалась в том, чтобы дать первой функции "флаг", то есть заставить ее создать открытое свойство "this.trigger" (инициализируемое как "0", устанавливаемое в "1" после завершения) после вызова; это должно позволить другой функции проверить флаг на его значение ([0,1]). Если условие выполнено ("trigger == 1"), вторая функция должна быть вызвана.
Ниже приведен абстрактный пример кода, который я использовал для тестирования:
<script type="text/javascript" >
/**/function cllFnc(tgt) { //!! first function
this.trigger = 0 ;
var trigger = this.trigger ;
var _tgt = document.getElementById(tgt) ; //!! changes the color of the target div to signalize the function's execution
_tgt.style.background = '#66f' ;
alert('Calling! ...') ;
setTimeout(function() { //!! in place of an AJAX call, duration 5000ms
trigger = 1 ;
},5000) ;
}
/**/function rcvFnc(tgt) { //!! second function that should get called upon the first function's completion
var _tgt = document.getElementById(tgt) ; //!! changes color of the target div to signalize the function's execution
_tgt.style.background = '#f63' ;
alert('... Someone picked up!') ;
}
/**/function callCheck(obj) {
//alert(obj.trigger ) ; //!! correctly returns initial "0"
if(obj.trigger == 1) { //!! here's the problem: trigger never receives change from function on success and thus function two never fires
alert('trigger is one') ;
return true ;
} else if(obj.trigger == 0) {
return false ;
}
}
/**/function tieExc(fncA,fncB,prms) {
if(fncA == 'cllFnc') {
var objA = new cllFnc(prms) ;
alert(typeof objA + '\n' + objA.trigger) ; //!! returns expected values "object" and "0"
}
//room for more case definitions
var myItv = window.setInterval(function() {
document.getElementById(prms).innerHTML = new Date() ; //!! displays date in target div to signalize the interval increments
var myCallCheck = new callCheck(objA) ;
if( myCallCheck == true ) {
if(fncB == 'rcvFnc') {
var objB = new rcvFnc(prms) ;
}
//room for more case definitions
window.clearInterval(myItv) ;
} else if( myCallCheck == false ) {
return ;
}
},500) ;
}
</script>
HTML часть для тестирования:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd >
<html>
<head>
<script type="text/javascript" >
<!-- see above -->
</script>
<title>
Test page
</title>
</head>
<body>
<!-- !! testing area -->
<div id='target' style='float:left ; height:6em ; width:8em ; padding:0.1em 0 0 0; font-size:5em ; text-align:center ; font-weight:bold ; color:#eee ; background:#fff;border:0.1em solid #555 ; -webkit-border-radius:0.5em ;' >
Test Div
</div>
<div style="float:left;" >
<input type="button" value="tie calls" onmousedown="tieExc('cllFnc','rcvFnc','target') ;" />
</div>
<body>
</html>
Я почти уверен, что это некоторая проблема с областью JavaScript, так как я проверил, правильно ли установлен триггер на "1", и это так. Очень вероятно, что функция "checkCall()" не получает обновленный объект, а вместо этого только проверяет его старый экземпляр, который, очевидно, никогда не отмечает завершение, устанавливая "this.trigger" в "1". Если так, я не знаю, как решить эту проблему.
Во всяком случае, надеюсь, у кого-то есть идея или опыт с этим конкретным видом проблемы.
Спасибо за прочтение!
FK
5 ответов
Я с этим справился, и теперь, похоже, все работает отлично. Я опубликую свой код позже, после того как я разберусь с ним. А пока большое спасибо за помощь!
Обновить
Пробовал код в Webkit (Safari, Chrome), Mozilla и Opera. Кажется, работает просто отлично. Ждем любых ответов.
Обновление 2
Я изменил метод tieExc(), чтобы интегрировать синтаксис вызова функции Anurag. Теперь вы можете вызывать столько функций, сколько захотите, после проверки завершения, передавая их в качестве аргументов.
Если вы не склонны читать код, попробуйте: http://jsfiddle.net/UMuj3/ (кстати, JSFiddle - действительно отличный сайт!).
JS-код:
/**/function meta() {
var myMeta = this ;
/** **/this.cllFnc = function(tgt,lgt) { //!! first function
this.trigger = 0 ; //!! status flag, initially zero
var that = this ; //!! required to access parent scope from inside nested function
var _tgt = document.getElementById(tgt) ; //!! changes the color of the target div to signalize the function's execution
_tgt.style.background = '#66f' ;
alert('Calling! ...') ;
setTimeout(function() { //!! simulates longer AJAX call, duration 5000ms
that.trigger = 1 ; //!! status flag, one upon completion
},5000) ;
} ;
/** **/this.rcvFnc = function(tgt) { //!! second function that should get called upon the first function's completion
var _tgt = document.getElementById(tgt) ; //!! changes color of the target div to signalize the function's execution
_tgt.style.background = '#f63' ;
alert('... Someone picked up!') ;
} ;
/** **/this.callCheck = function(obj) {
return (obj.trigger == 1) ? true
: false
;
} ;
/** **/this.tieExc = function() {
var functions = arguments ;
var myItv = window.setInterval(function() {
document.getElementById('target').innerHTML = new Date() ; //!! displays date in target div to signalize the interval increments
var myCallCheck = myMeta.callCheck(functions[0]) ; //!! checks property "trigger"
if(myCallCheck == true) {
clearInterval(myItv) ;
for(var n=1; n < functions.length; n++) {
functions[n].call() ;
}
} else if(myCallCheck == false) {
return ;
}
},100) ;
} ;
}
HTML:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd >
<html>
<head>
<script type='text/javascript' >
<!-- see above -->
</script>
<title>
Javascript Phased Execution Test Page
</title>
</head>
<body>
<div id='target' style='float:left ; height:7.5em ; width:10em ; padding:0.5em 0 0 0; font-size:4em ; text-align:center ; font-weight:bold ; color:#eee ; background:#fff;border:0.1em solid #555 ; -webkit-border-radius:0.5em ;' >
Test Div
</div>
<div style="float:left;" >
<input type="button" value="tieCalls()" onmousedown="var myMeta = new meta() ; var myCll = new myMeta.cllFnc('target') ; new myMeta.tieExc(myCll, function() { myMeta.rcvFnc('target') ; }, function() { alert('this is fun stuff!') ; } ) ;" /><br />
</div>
<body>
</html>
Вы можете воспользоваться функцией JS, называемой закрытием. Объедините это с очень распространенным шаблоном JS, называемым "стиль передачи продолжения", и вы получите свое решение. (Ни одна из этих вещей не является оригинальной для JS, но интенсивно используется в JS).
// a function
function foo(some_input_for_foo, callback)
{
// do some stuff to get results
callback(results); // call our callback when finished
}
// same again
function bar(some_input_for_bar, callback)
{
// do some stuff to get results
callback(results); // call our callback when finished
}
"Стиль передачи продолжения" относится к обратному вызову. Вместо того, чтобы возвращать значение, каждая функция вызывает обратный вызов (продолжение) и дает ему результаты.
Затем вы можете легко связать их вместе:
foo(input1, function(results1) {
bar(results1, function(results2) {
alert(results2);
});
});
Вложенные анонимные функции могут "видеть" переменные из области видимости, в которой они живут. Поэтому нет необходимости использовать специальные свойства для передачи информации.
Обновить
Чтобы уточнить, во фрагменте кода вашего вопроса ясно, что вы думаете примерно так:
У меня длительная асинхронная операция, поэтому мне нужно знать, когда она заканчивается, чтобы начать следующую операцию. Поэтому мне нужно сделать это состояние видимым как свойство. Затем в другом месте я могу запустить цикл, неоднократно проверяя это свойство, чтобы увидеть, когда оно переходит в состояние "завершено", поэтому я знаю, когда продолжить.
(И тогда как усложняющий фактор, цикл должен использовать setInterval
начать бегать и clearInterval
выйти, разрешить запуск другого кода JS - но это, тем не менее, "цикл опроса").
Вам не нужно этого делать!
Вместо того, чтобы заставлять вашу первую функцию устанавливать свойство при завершении, заставьте его вызвать функцию.
Чтобы сделать это абсолютно понятным, давайте проведем рефакторинг вашего исходного кода:
function cllFnc(tgt) { //!! first function
this.trigger = 0 ;
var trigger = this.trigger ;
var _tgt = document.getElementById(tgt) ; //!! changes the color...
_tgt.style.background = '#66f' ;
alert('Calling! ...') ;
setTimeout(function() { //!! in place of an AJAX call, duration 5000ms
trigger = 1 ;
},5000) ;
}
[Обновление 2: кстати, там есть ошибка. Вы копируете текущее значение trigger
свойство в новую локальную переменную под названием trigger
, Затем в конце вы назначаете 1 этой локальной переменной. Никто другой не сможет увидеть это. Локальные переменные являются частными для функции. Но вам все равно ничего не нужно делать, так что продолжайте читать...]
Все, что нам нужно сделать, это сообщить этой функции, что вызывать, когда она будет выполнена, и избавиться от установки свойств:
function cllFnc(tgt, finishedFunction) { //!! first function
var _tgt = document.getElementById(tgt) ; //!! changes the color...
_tgt.style.background = '#66f' ;
alert('Calling! ...') ;
setTimeout(function() { //!! in place of an AJAX call, duration 5000ms
finishedFunction(); // <-------- call function instead of set property
},5000) ;
}
Теперь нет необходимости в вашей проверке вызовов или ваших специальных tieExc
помощник. Вы можете легко связать две функции вместе с очень небольшим количеством кода.
var mySpan = "#myspan";
cllFnc(mySpan, function() { rcvFnc(mySpan); });
Еще одним преимуществом этого является то, что мы можем передавать различные параметры для второй функции. При вашем подходе одинаковые параметры передаются обоим.
Например, первая функция может выполнить пару вызовов службы AJAX (для краткости используется jQuery):
function getCustomerBillAmount(name, callback) {
$.get("/ajax/getCustomerIdByName/" + name, function(id) {
$.get("/ajax/getCustomerBillAmountById/" + id), callback);
});
}
Вот, callback
принимает сумму счета клиента, и AJAX get
вызов передает полученное значение функции, которую мы передаем, поэтому callback
уже совместим и может напрямую выступать в качестве обратного вызова для второго вызова AJAX. Таким образом, это сам пример связывания двух асинхронных вызовов в последовательности и оборачивания их в то, что кажется (снаружи), в одну асинхронную функцию.
Затем мы можем связать это с другой операцией:
function displayBillAmount(amount) {
$("#billAmount").text(amount);
}
getCustomerBillAmount("Simpson, Homer J.", displayBillAmount);
Или мы могли бы (снова) использовать анонимную функцию:
getCustomerBillAmount("Simpson, Homer J.", function(amount) {
$("#billAmount").text(amount);
});
Таким образом, с помощью цепочки вызовов функций, подобных этой, каждый шаг может передавать информацию на следующий шаг, как только она становится доступной.
Заставив функции выполнять обратный вызов, когда они будут выполнены, вы освобождаетесь от любых ограничений на то, как каждая функция работает внутри. Он может делать вызовы AJAX, таймеры, что угодно. Пока обратный вызов "продолжения" передается вперед, может быть любое количество уровней асинхронной работы.
По сути, в асинхронной системе, если вы когда-нибудь пишете цикл для проверки переменной и выяснения, изменилось ли ее состояние, то что-то пошло не так. Вместо этого должен быть способ предоставить функцию, которая будет вызываться при изменении состояния.
Обновление 3
В других местах в комментариях вы упоминаете, что настоящая проблема заключается в кэшировании результатов, поэтому вся моя работа по объяснению этого была пустой тратой времени. Это то, что вы должны поставить в вопросе.
Обновление 4
Совсем недавно я написал небольшой пост в блоге на тему кэширования результатов асинхронных вызовов в JavaScript.
(конец обновления 4)
Другой способ поделиться результатами - предоставить возможность для одного обратного вызова для "трансляции" или "публикации" нескольким подписчикам:
function pubsub() {
var subscribers = [];
return {
subscribe: function(s) {
subscribers.push(s);
},
publish: function(arg1, arg2, arg3, arg4) {
for (var n = 0; n < subscribers.length; n++) {
subscribers[n](arg1, arg2, arg3, arg4);
}
}
};
}
Так:
finished = pubsub();
// subscribe as many times as you want:
finished.subscribe(function(msg) {
alert(msg);
});
finished.subscribe(function(msg) {
window.title = msg;
});
finished.subscribe(function(msg) {
sendMail("admin@mysite.com", "finished", msg);
});
Затем пусть некоторая медленная операция опубликует свои результаты:
lookupTaxRecords("Homer J. Simpson", finished.publish);
По окончании этого звонка он будет звонить всем трем абонентам.
Окончательным ответом на эту проблему "позвони мне, когда будешь готов" является обратный вызов. Обратный вызов - это, по сути, функция, которую вы назначаете свойству объекта (например, "onload"). Когда состояние объекта изменяется, вызывается функция. Например, эта функция делает ajax-запрос к указанному URL-адресу и выкрикивает, когда он завершен:
function ajax(url) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.onreadystatechange = function (aEvt) {
if(req.readyState == 4)
alert("Ready!")
}
req.send(null);
}
Конечно, это недостаточно гибко, потому что мы, вероятно, хотим разные действия для разных вызовов ajax. К счастью, javascript - это функциональный язык, поэтому мы можем просто передать требуемое действие в качестве параметра:
function ajax(url, action) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.onreadystatechange = function (aEvt) {
if(req.readyState == 4)
action(req.responseText);
}
req.send(null);
}
Эта вторая функция может использоваться следующим образом:
ajax("http://...", function(text) {
do something with ajax response
});
Согласно комментариям, вот пример, как использовать ajax в объекте
function someObj()
{
this.someVar = 1234;
this.ajaxCall = function(url) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
var me = this; // <-- "close" this
req.onreadystatechange = function () {
if(req.readyState == 4) {
// save data...
me.data = req.responseText;
// ...and/or process it right away
me.process(req.responseText);
}
}
req.send(null);
}
this.process = function(data) {
alert(this.someVar); // we didn't lost the context
alert(data); // and we've got data!
}
}
o = new someObj;
o.ajaxCall("http://....");
Идея состоит в том, чтобы "закрыть" (с псевдонимом) "this" в обработчике событий, чтобы его можно было передавать дальше.
Добро пожаловать на ТАК! Кстати, вы сталкиваетесь с полной глупостью, и ваш вопрос совершенно неясен:)
Это основывается на ответе @ Дэниела об использовании продолжений. Это простая функция, которая объединяет несколько методов. Очень похоже на то, как труба |
работает в Unix. В качестве аргументов он принимает набор функций, которые должны выполняться последовательно. Возвращаемое значение каждого вызова функции передается следующей функции в качестве параметра.
function Chain() {
var functions = arguments;
return function(seed) {
var result = seed;
for(var i = 0; i < functions.length; i++) {
result = functions[i](result);
}
return result;
}
}
Чтобы использовать его, создайте объект из Chained
передавая все функции в качестве параметров. Пример, который вы можете проверить на скрипке:
var chained = new Chain(
function(a) { return a + " wo"; },
function(a) { return a + "r"; },
function(a) { return a + "ld!"; }
);
alert(chained('hello')); // hello world!
Чтобы использовать его с запросом AJAX, передайте цепочечную функцию в качестве обратного вызова успеха XMLHttpRequest.
var callback = new Chain(
function(response) { /* do something with ajax response */ },
function(data) { /* do something with filtered ajax data */ }
);
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.onreadystatechange = function (aEvt) {
if(req.readyState == 4)
callback(req.responseText);
}
req.send(null);
Важно то, что каждая функция зависит от вывода предыдущей функции, поэтому вы должны возвращать какое-то значение на каждом этапе.
Это всего лишь предложение - возложение ответственности за проверку наличия данных на локальном уровне или необходимость выполнения HTTP-запроса увеличит сложность системы. Вместо этого у вас может быть непрозрачный менеджер запросов, очень похожий на metaFunction
у вас есть, и пусть он решает, будут ли данные обслуживаться локально или удаленно.
Вот образец Request
объект, который обрабатывает эту ситуацию без каких-либо других объектов или функций, не зная, откуда были получены данные:
var Request = {
cache: {},
get: function(url, callback) {
// serve from cache, if available
if(this.cache[url]) {
console.log('Cache');
callback(this.cache[url]);
return;
}
// make http request
var request = new XMLHttpRequest();
request.open('GET', url, true);
var self = this;
request.onreadystatechange = function(event) {
if(request.readyState == 4) {
self.cache[url] = request.responseText;
console.log('HTTP');
callback(request.responseText);
}
};
request.send(null);
}
};
Чтобы использовать его, вы должны сделать звонок Request.get(..)
и возвращает кэшированные данные, если они доступны, или выполняет AJAX-вызов в противном случае. Третий параметр может быть передан для контроля длительности кэширования данных, если вы ищете детальный контроль над кэшированием.
Request.get('<url>', function(response) { .. }); // HTTP
// assuming the first call has returned by now
Request.get('<url>', function(response) { .. }); // Cache
Request.get('<url>', function(response) { .. }); // Cache
Очень простым решением было бы сделать ваш первый вызов ajax синхронным. Это один из необязательных параметров.