ארכיון פוסטים מהקטגוריה "JavaScript"
Sequence ב-JavaScript
יום שלישי, 5 בדצמבר, 2006מהן פונקציות אסינכרוניות?
פונקציה אסינכרונית היא פונקציה שסיום פעולתה תלוי בפעולה של גוף אחר. התוכנית בה היא רצה אינה מחכה שאותו גוף יחזיר תשובה אלא אותו גוף "דוחף" תשובה כשהוא סיים. הדחיפה הזו מתבצעת לתוך פונקצית callback – פונקציה שהוגדרה במיוחד למטרת קבלת התשובה, ואותו גוף קורא לה עם ארגומנטים שהוא מזין. חתימתה של פונקצית ה-callback מכילה את הארגומנטים האלה, ומשתמשת בהם לצורך סיום הפעולה.
לדוגמה – שימוש ב-XMLHTTP במצב אסינכרוני. בשליחת הבקשה לשרת שאר הסקריפטים ממשיכים לרוץ והעמוד ממשיך לתפקד. לאותו אובייקט XMLHTTP מוצמד אירוע בשם onreadystatechange שרץ בכל פעם שהשרת מחזיר תשובה. זאת להבדיל ממצב סינכרוני, בו הדפדפן מחכה לתשובה מהשרת. החלון 'קופא' ורק בקבלת התשובה הכל חוזר לחיים. התנהגות זו היא פשוט לא אינטרנט-קורקט, ולא ידידותית למשתמש.
בד"כ כשתהיה לנו פונקציה אסינכרונית, נרצה שבסיומה תתבצע פונקציה אחרת. את הקריאה לפונקציה השניה נשים ב-callback של הראשונה.
אם יש לנו מספר רב של פונקציות ו-callbacks הן מתחילות להיות תלויות אחת בשניה – בסיום של A קרא ל-B, בסיום של B קרא ל-C וכו'. אם אני מוציא את B מהשרשרת – הרסתי את התור, כי הקריאה ל-C לא תתבצע.
תרחיש פונקציות אסינכרוניות בתור
ניקח לדוגמה את הרצף הבא:
- הרצת אפקט המשנה שקיפות של אלמנט לידי תצוגה, ובסיום:
- הופעה של כפתור בתוך אותו אלמנט, ובלחיצה:
- שליחת בקשת XMLHTTP לשרת, ובקבלת תשובה:
- הודעת alert של סיום
החלוקה לפונקציות די פשוטה:
function EffectElement() {
var div=document.getElementById("div");
var opacity=0;
var iv=setInterval(function () {
opacity+=.05;
div.SetOpacity(opacity);
div.style.filter="alpha(opacity="+(opacity*100)+")";
if (opacity>=1) {
clearTimeout(iv);
ShowButton();
}
},50);
}
function ShowButton() {
var button=document.getElementById("button");
button.style.display="";
button.onclick=function () {
RequestServer();
};
}
function RequestServer() {
var xh=new XMLHttpRequest();
xh.open("GET","url.htm",true); // true - async call
xh.onreadystatechange=function () {
if (xh.readyState==4) {
div.innerHTML=xh.responseText;
Finish();
}
};
xh.send(null);
}
function Finish() {
alert("Finish");
}
onload=EffectElement;
אנו שמים לב שב-callback של כל פונקציה בתור אפשר להפעיל את הפונקציה הבאה. אך מה יקרה במצב בו נרצה להוריד את אחת הפונקציות מהתור? נצטרך לשנות בפונקציה שהייתה אחריה את הקריאה, לפונקציה אחרת. ומה יקרה אם נרצה לשנות את הסדר? בלאגן.
פתרון גנרי לפונקציות אסינכרוניות בתור
ניסיתי לחשוב על פתרון גנרי לבעיה. כזה שיתן לי במינימום קוד ומקסימום גמישות להחליט מי בא לפני מי ואיך לקרוא לפונקציה הבאה בלי להיות תלוי בה.
הסינטקס הבסיסי הוא כזה:
var sequence=new Devign.Sequence(); sequence.Add(EffectElement); sequence.Add(ShowButton); sequence.Add(RequestServer); sequence.Add(Finish); sequence.Start();
ובתוך כל פונקצית callback יש לקדם את ה-sequence הנוכחי בעזרת
currSequence.Next();
כאשר currSequence נשלח עם הפונקציה כארגומנט יחיד.
פשוט, לא?
אפשר לשחק עם הסדר של הפונקציות, להוריד ולהוסיף כאוות נפשנו.
המחלקה Devign.Sequence
// Devign.Sequence class
Devign.Sequence=function () {
// private fields
// a list of functions
this.list=[];
this.index=-1;
this.aborted=false;
// /private fields
// public fields
this.Finished=false;
// /public fields
};
// public methods
Devign.Sequence.prototype={
// adds a new function
Add:function (sequenceFunction) {
this.list.push(sequenceFunction);
},
// starts the sequence from the first function
// fires 'OnStart' if exists
Start:function () {
this.index=-1;
this.aborted=false;
this.Next();
if (typeof(this.OnStart)=="function") this.OnStart();
},
// ends the sequence
// fires 'OnEnd' if exists
End:function () {
if (typeof(this.OnEnd)=="function") this.OnEnd();
this.Finished=true;
},
// proceeds the sequence
// if the sequence has finished calls 'End'
Next:function () {
// if sequence was aborted - ignore next statements
if (this.aborted) return;
this.index++;
if (this.index==this.list.length) return this.End();
var tf=this.list[this.index];
// calls the function with the sequence as an argument
if (tf) tf(this);
},
// aborts the sequence by setting the index to 'not started' and the flag aborted to true
Abort:function () {
this.index=-1;
this.aborted=true;
}
};
// /public methods
// /Devign.Sequence class
למופע של המחלקה יש מערך של פונקציות, אליו ניתן להוסיף בעזרת המתודה Add. בנוסף קיים גם שדה מספרי בשם index השומר את האינדקס הנוכחי של הפונקציה הרצה (כאשר 1- מסמל שעוד לא התחלנו לרוץ).
למופע ניתן להגדיר גם שני אירועים – OnStart ו-OnEnd. אם הוגדרו הם יורצו בזמן המתאים.
בכל פעם שמתקדמים בתהליך, האינדקס עולה, והפונקציה נשלפת מתוך המערך. הפונקציה נקראת עם הפרמטר this שמצביע באותו זמן על מופע המחלקה. הרפרנס למחלקה נשלח כדי שיהיה אפשר לזהותו מתוך הפונקציה, ולקדם אותו בעזרת המתודה Next.
קוד התוכנית במלואו
<script type="text/javascript" src="Devign.Sequence.js"></script>
<script type="text/javascript">
//<![CDATA[
var sequence=new Devign.Sequence();
sequence.Add(EffectElement);
sequence.Add(ShowButton);
sequence.Add(RequestServer);
sequence.Add(Finish);
onload=function () {
sequence.Start();
};
function EffectElement(currSequence) {
var div=document.getElementById("div");
var opacity=0;
var iv=setInterval(function () {
opacity+=.05;
div.style.opacity=opacity;
div.style.filter="alpha(opacity="+(opacity*100)+")";
if (opacity>=1) {
clearTimeout(iv);
currSequence.Next();
}
},50);
}
function ShowButton(currSequence) {
var button=document.getElementById("button");
button.style.display="";
button.onclick=function () {
currSequence.Next();
};
}
function RequestServer(currSequence) {
var div=document.getElementById("div");
var xh=window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
xh.open("GET","XMLHttpRequest.htm",true); // true – async call
xh.onreadystatechange=function () {
if (xh.readyState==4) {
div.innerHTML=xh.responseText;
currSequence.Next();
}
};
xh.send(null);
}
function Finish(currSequence) {
alert("Finish");
currSequence.Next();
}
//]]>
</script>
<div id="div" style="filter:alpha(opacity=0);opacity:0;height:100px;width:100px;border:1px solid #000;background-color:#93B9CE;">
<input type="button" id="button" value="Click" style="display:none"/>
</div>