本博客著作權(quán)歸饑人谷_Lyndon和饑人谷所有,轉(zhuǎn)載請注明出處
學(xué)習(xí)AJAX的時候,對狀態(tài)鎖、代碼封裝兩個部分很感興趣。狀態(tài)鎖保證了在一些特殊情況下發(fā)出正確請求,獲得正確的返回數(shù)據(jù);代碼封裝使得代碼可讀性提升,代碼結(jié)構(gòu)化且適合維護(hù)。兩者都非常有用,因此我寫一個博客來梳理一下。
>>> 為什么需要狀態(tài)鎖?
當(dāng)數(shù)據(jù)請求速度/網(wǎng)速很慢的時候,如果用戶多次點擊請求按鈕,那么很有可能發(fā)出多次重復(fù)的請求,在get方式下,如果不對用戶的多次重復(fù)點擊做出處理,那么每次構(gòu)造的URL很有可能是一致的,最終就會返回很多重復(fù)的數(shù)據(jù),違背了開發(fā)者的初衷。
狀態(tài)鎖是一種優(yōu)雅的方法,概括而言:狀態(tài)鎖事先聲明一個變量,其中true表示開啟(鎖住用戶操作,用戶操作無效),false表示關(guān)閉(用戶可以進(jìn)行操作,操作將被處理),其核心的步驟如下:
1. 初始狀態(tài)下,狀態(tài)鎖是關(guān)閉的,用戶可以進(jìn)行操作
var lock = false;
2. 創(chuàng)建AJAX對象時進(jìn)行邏輯判斷,如果狀態(tài)鎖為開啟(true)狀態(tài),那么將忽視用戶的頻繁點擊,否則將發(fā)送請求
if(lock){
return;
}
3. 請求一經(jīng)發(fā)出,需要經(jīng)歷處理過程,在這時,狀態(tài)鎖啟動,直到響應(yīng)就緒才關(guān)閉,否則,狀態(tài)鎖開啟,無法進(jìn)行請求
lock = true;
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
...
lock = false;
}
}else{
lock = true;
}
>>> 不設(shè)置狀態(tài)鎖會怎樣
以下是不設(shè)置狀態(tài)鎖時前端頁面和服務(wù)器的代碼,只需要觀察Network的請求就可以獲知問題:
<div id="ct">
<ul id="news"></ul>
<button id="btn">點我加載</button>
</div>
<script>
function $(id){
return document.querySelector(id);
}
var btn = $("#btn");
var ul = $("#news");
var pageIndex = 0;
btn.addEventListener("click", function(){
var xhr = new XMLHttpRequest();
xhr.open("get", "/loadMore?index=" + pageIndex + "&length=5", true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.status === 200 || xhr.status === 304){
var results = JSON.parse(xhr.responseText);
console.log(results);
var fragment = document.createDocumentFragment();
for(var i = 0; i < results.length; i++){
var node = document.createElement("li");
node.innerText = results[i];
fragment.appendChild(node);
}
ul.appendChild(fragment);
pageIndex = pageIndex + 5;
}else{
console.log("error");
}
}
};
})
</script>
app.get('/loadMore', function(req, res) {
var pageIndex = parseInt(req.query.index);
var length = parseInt(req.query.length);
data = [];
for(var i = 0; i < length; i++){
var news = "新聞" + (i + pageIndex).toString();
data.push(news);
}
setTimeout(function(){
res.send(data)}, 5000
)
});
服務(wù)端故意讓每次的響應(yīng)時間延遲5s,也就是點擊后不會立即有數(shù)據(jù)渲染在頁面上,數(shù)據(jù)拖延了5s才向前端進(jìn)行發(fā)送。如果用戶很急迫地一連點擊5次按鈕,返回結(jié)果是:

其原因是:每次的readyState都沒有到4(請求已完成,響應(yīng)已經(jīng)就緒)時,用戶就已經(jīng)迫不及待地發(fā)出了下一個請求,這時候的pageIndex并沒有執(zhí)行加5的操作,導(dǎo)致每次的請求都是http://localhost:8080/loadMore?index=0&length=5,而當(dāng)數(shù)據(jù)全部展現(xiàn)到頁面上后,再進(jìn)行一次點擊,此時的pageIndex已經(jīng)變成25了,新的請求就會變成http://localhost:8080/loadMore?index=25&length=5,輸出結(jié)果會非常的混亂。
>>> 添加狀態(tài)鎖
按照之前的說法,加入一個狀態(tài)鎖可以保證的效果是:當(dāng)響應(yīng)還沒有完成的時候,無論用戶怎么點擊按鈕,我都讓這一行為return為空,也即不返回任何結(jié)果/不產(chǎn)生任何效力。
以下是添加注釋的JS代碼。
// 狀態(tài)鎖初始狀態(tài)為關(guān)閉(false)狀態(tài),用戶可以發(fā)出請求
var lock = false;
function $(id){
return document.querySelector(id);
}
var btn = $("#btn");
var ul = $("#news");
var pageIndex = 0;
btn.addEventListener("click", function(){
var xhr = new XMLHttpRequest();
// 如果狀態(tài)鎖狀態(tài)為開啟(true),則忽略用戶點擊操作,不發(fā)送AJAX請求
if(lock){
return;
}
// 如果狀態(tài)鎖狀態(tài)為關(guān)閉,則發(fā)送AJAX請求
if(!lock) {
xhr.open("get", "/loadMore?index=" + pageIndex + "&length=5", true);
xhr.send();
// 執(zhí)行過程中,狀態(tài)鎖為開啟狀態(tài),用戶無論怎樣點擊都是無效的
lock = true;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 304) {
var results = JSON.parse(xhr.responseText);
console.log(results);
var fragment = document.createDocumentFragment();
for (var i = 0; i < results.length; i++) {
var node = document.createElement("li");
node.innerText = results[i];
fragment.appendChild(node);
}
// 如果響應(yīng)就緒,狀態(tài)鎖為關(guān)閉狀態(tài),用戶可以進(jìn)行下一次的請求
lock = false;
ul.appendChild(fragment);
pageIndex = pageIndex + 5;
} else {
console.log("error");
// 否則,響應(yīng)出錯,狀態(tài)鎖保持開啟狀態(tài)
lock = true;
}
}
};
}
})
添加狀態(tài)鎖后,返回的結(jié)果會變?yōu)檎!?/p>

>>> AJAX封裝
AJAX封裝的第一個出發(fā)點:一個頁面上通常有多處需要使用AJAX,如果不進(jìn)行封裝,每次需要使用AJAX時,都需要寫相似度極高的代碼,造成信息冗余,而AJAX封裝抽取出普遍的通則,這樣在多次使用AJAX時僅需要直接調(diào)用封裝完成的代碼即可,便利了前端開發(fā)。
AJAX封裝的第二個出發(fā)點:將復(fù)雜的問題進(jìn)行拆解,由大化小,且力求使得每一個降解的子代碼段變得邏輯更加簡潔。如果不進(jìn)行合理的封裝,代碼中的一個函數(shù)內(nèi)既有if...else...,又有循環(huán),還有其他的變量計算,看起來非常缺乏條理。
針對這一弊端,將原有的代碼按照功能劃分成多個子部分,比如專門負(fù)責(zé)創(chuàng)建AJAX的、專門處理數(shù)據(jù)請求的、數(shù)據(jù)到來之后渲染頁面的,這樣在AJAX最核心的部分中只需要調(diào)用定義的函數(shù),整個代碼段的結(jié)構(gòu)會非常明晰,也方便后期維護(hù)。
1. 基礎(chǔ)的變量聲明放在script的最前面
var btn = document.querySelector("#load-more");
var ct = document.querySelector("#ct");
var pageIndex = 0;
var isDataArrive = true;
2. 創(chuàng)建事件偵聽器的時候,盡量在核心部分使用函數(shù),函數(shù)的布局跟著邏輯行進(jìn),比如:1)數(shù)據(jù)尚未來臨如何應(yīng)對 2)數(shù)據(jù)來臨如何應(yīng)對 3)加載數(shù)據(jù) 4)渲染頁面
btn.addEventListener("click", function(e) {
e.preventDefault();
// 數(shù)據(jù)尚未來臨,操作無效
if (!isDataArrive) {
return;
}
// 否則執(zhí)行數(shù)據(jù)加載,加載的數(shù)據(jù)為news,因為news需要經(jīng)過處理才能展現(xiàn)在頁面上,因此構(gòu)建一個匿名函數(shù)用以渲染頁面
loadData(function (news) {
renderPage(news);
pageIndex = pageIndex + 5;
isDataArrive = true;
})
isDataArrive = false;
});
3. 在實際應(yīng)用場景中,一個頁面中會有很多需要利用AJAX的地方,所以經(jīng)常是傳遞一個AJAX對象,然后直接將其中的value放到相應(yīng)的函數(shù)中。其中每一個AJAX對象應(yīng)該包含這些要素:1)請求方式 2)請求接口地址 3)傳遞的參數(shù) 4)請求成功后執(zhí)行什么 5)請求失敗后執(zhí)行什么
function loadData(callback){
// 請求方式、URL、參數(shù)、請求成功后怎樣、請求失敗后怎樣
// ajax("get", url, data, onSuccess, onError)
ajax({
type: "get",
url: "/loadMore",
data: {
index: pageIndex,
length: 5
},
// 請求成功后執(zhí)行,這里的callback相當(dāng)于上一段代碼后中的匿名函數(shù)
onSuccess: callback,
onError: function(){
console.log("error")
}
})
}
4. 既然已經(jīng)定義好了AJAX對象,就要開始將其中的value放入對應(yīng)的函數(shù)中
function ajax(options){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.status === 200 || xhr.status === 304){
var results = JSON.parse(xhr.responseText)
// 往`callback`中傳遞參數(shù)
options.onSuccess(results);
}else{
options.onError();
}
}
}
var query = "?";
for(key in options.data){
query += key + "=" + options.data[key] + "&"
}
query = query.substr(0, query.length - 1);
xhr.open(options.type, options.url + query, true);
xhr.send();
}
5. 接下來是渲染頁面的部分
function renderPage(news){
var fragment = document.createDocumentFragment();
for(var i = 0; i < news.length; i++){
var node = document.createElement("li");
node.innerText = news[i];
fragment.appendChild(node);
}
ct.appendChild(fragment);
}
>>> 總結(jié)
AJAX封裝最明顯的特征就是:大問題拆解為小問題,但是小問題之間又環(huán)環(huán)相扣。需要熟悉的是AJAX對象,以及如何將對象中的值與回調(diào)函數(shù)結(jié)合起來。當(dāng)然在面臨更加靈活的AJAX對象時(比如需要綜合考慮到get和post兩種請求方式,數(shù)據(jù)返回格式可能不是JSON字符串),需要對代碼做出更優(yōu)化封裝,以應(yīng)對更多樣的情況并考慮到容錯。