1. 程式人生 > >關於非同步請求的一些事

關於非同步請求的一些事

先看這樣一個例子:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
        }
    });

    return result;
}

var result = foo(); // It always ends up being `undefined`.

在這個例子中,使用ajax進行非同步請求,但是響應的返回值始終是undefined。那這是什麼原因呢?這個問題最初也讓我很困惑。下面我們來分析一下原因。

找出問題的原因

我們知道ajax中的a代表非同步,那就意味著傳送請求(亦或是接受響應)需要花費一點時間。而在上述例子中,$.ajax是被立即返回的,return result; 這句語句在success 函式返回響應前就已經被執行了。主要原因還是沒有理解非同步和同步之間的區別。

下面這個類比希望可以使同步和非同步的區別更清晰。

Synchronous(同步流)

想象你給你的一個朋友打電話,你希望他可以幫你一個忙,你會進行一段時間的等待,直到他的回答。

那麼在正常同步流的程式碼中也會發生一樣的事情。

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

執行 findItem 需要花費一段時間,所有在 var item = findItem(); 這條語句之後的程式碼都需要等待函式返回結果。

Asynchronous(非同步流)

同樣你又給你的朋友打了一個電話,但是現在你很忙,你給你的朋友留言讓他給你回一個電話,然後你掛掉電話就做其他的事情了,直到你的朋友回你電話,你才會停下手中的事情處理這個電話。

這也正是ajax請求的過程。

findItem(function(item) {
    // Do something with item
});
doSomethingElse();

不去等待響應而是立即繼續執行ajax之後的語句。而為了得到響應我們會定義一個回撥函式,一旦收到響應就會執行這個回撥函式。但是注意在ajax請求之後的語句會在回撥函式呼叫前被執行。

解決問題的方法

1. 使用回撥函式

上面的例子中

var result = foo();
// Code that depends on 'result'

改為

foo(function(result) {
    // Code that depends on 'result'
});

然後我們給 foo 傳遞函式引數

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

函式 foo 定義如下

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

一旦ajax 請求成功,$.ajax 就會呼叫callback 函式並且將響應傳遞給callback 函式。

2.使用promise

Promise API是ES6新特性之一,但是各大瀏覽器已經有了很好的支援(IE11及以上支援)。MDN這麼定義它:

Promise 物件用於非同步計算。一個 Promise 表示一個現在、將來或永不可能可用的值

promise的語法:

new Promise(
    /* executor */
    function(resolve, reject) {...}
);

這裡是一段使用promise 的例子:

function delay() {
        // `delay` returns a promise
  return new Promise(function(resolve, reject) {
        // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); 
        // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

delay().then(function(v) { 
        // `delay` returns a promise
      console.log(v); 
        // Log the value once it is resolved
}).catch(function(v) {
        // Or do something else if it is rejected 
        // (it would not happen in this example, since `reject` is not called).
});

結合ajax 使用:

function ajax(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open('GET', url);
    xhr.send();
  });
}

ajax("/echo/json").then(function(result) {
  // Code depending on result
}).catch(function() {
  // An error occurred
});

這裡有關於promise 的更多資訊。

3.jQuey: 使用延遲函式

jQery的延遲函式和promise 很像,但是API略有不同。每一個jQery的ajax方法都會返回一個延遲函式。

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

始終要牢記promise 和延遲函式都只有一個存有未來值 的容器而不是值本身。看下面的例子:

function checkPassword() {
    return $.ajax({
        url: '/password',
        data: {
            username: $('#username').val(),
            password: $('#password').val()
        },
        type: 'POST',
        dataType: 'json'
    });
}

if (checkPassword()) {
    // Tell the user they're logged in
}

這段程式碼就誤解了非同步和延遲過程,$.ajax() 會向伺服器傳送對/password 的請求,jQery內部機制會立即會返回一個ajax延遲函式,而這個並不是伺服器發回來的響應。這也就意味著下面的判斷語句(if)始終都是true,程式會認為讀者已經登入,而這是不正確的。

我們可以進行一個小的修改:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});

現在我們仍然會向伺服器發起請求,$.ajax() 也會立即返回一個延遲物件,但是我們通過監聽.done().fail() 事件能正確處理伺服器返回的響應。.done() 事件被呼叫時,伺服器返回的是一個正常的響應(http 200),我們通過檢查他的返回物件是否為true判斷使用者是否登陸。
.fail() 處理函式是處理一些錯誤的事件。例如使用者網路斷開或伺服器錯誤等。

本文程式碼均來自StackOverflow上這個話題