ECMAScript6(15):Promise 對象

Promise 創建

Promise 對象用來傳遞異步操做消息,表明一個將來纔會知道結果的事件,而且對不一樣事件提供統一的 API 以便進一步處理。Promise 具備如下特色:ajax

  • 由異步操做結果決定改狀態,其餘操做毫不影響該狀態;
  • 對象狀態不受外界影響:Promise 表明的異步操做有三個狀態:json

    1. Pending: 進行中
    2. Resolved: 已完成(Fulfilled)
    3. Rejected: 已失敗
  • 一旦狀態改變,就不會再變:Promise 的狀態只有2種可能:數組

    1. 從 Pending 到 Resolved
    2. 從 Pending 到 Rejected

對於同一個 promise, 當以上狀態發生一個(只能發生其一),就不會再改變了。以後任什麼時候間你都能獲得這個狀態,且永不改變。
有了 Promise 就能夠將層層的回調寫爲同步的樣子,表示起來更清晰。不過須要注意如下幾點:promise

  • Promise 一旦創建就當即執行,而且沒法中斷或取消
  • 若是沒有設置回調函數,那麼 Promise 中的產生的錯誤不會拋到外部
  • Pending 狀態時,咱們沒法知道其具體進度

Promise 的基本結構以下:瀏覽器

var promise = new Promise(function(resolve, reject){
  if(/*異步操做成功*/){
    resolve(value);
  } else {
    reject(error);
  }
});

構造函數接受一個回調函數爲參數,回調函數具備2個參數,也都是函數,resolve 在 Promise 狀態變爲 resolved 時調用,reject 在 Promise 狀態變爲 rejected 時調用。resolve 的接受一個參數——值或另外一個 promise 對象; rejectj接受一個參數——錯誤。須要說明的是,這裏的 resole 和 reject 函數已經由系統部署好了,咱們能夠不寫。服務器

promise 構建好之後咱們就能夠調用它的then()方法,then(resolve(value){},reject(value){})方法接受2個函數參數,resolve 在 Promise 狀態變爲 resolved 時調用,reject 在 Promise 狀態變爲 rejected 時調用。其中 reject 參數是可選的。和構造函數不一樣的是,then 方法的 reject 和 resolve 都使用 promise 傳出的值做爲其惟一的參數。app

這裏寫一個簡單的例子,理解一下:異步

function timeout(ms){
  return new Promise((resolve, reject) => {
    console.log("promise");            //"promise"
    setTimeout(resolve, ms, 'done');
  });
}
timeout(2000).then((value) => {
  console.log(value);                  //2秒後獲得 "done"
});

利用 Promise 異步加載圖片:async

function loadImageAsync(url){
  return new Promise(function(resole, reject){
    var image = new Image();
    image.onload = function(){
      resolve(image);
    };
    image.onerror = function(){
      reject(new Error(`Could not load image at ${url}`));
    };
    image.src = url;
  });
}

利用 Promise 實現 Ajax:函數

var id = document.getElementById("primary");
  var getJSON = function(url){
    var promise = new Promise(function(resolve, reject){
      var client = new XMLHttpRequest();
      client.open("GET", url);
      client.onreadystatechange = handler;
      client.response = "json";
      client.setRequestHeader("Accept", "application/json");
      client.send();

      function handler(){
        if(client.readyState !== 4) return;
        if(this.status === 200){
          resolve(client.response);
        } else {
          reject(new Error(this.statusText));
        }
      }
    });
    return promise;
  }
  getJSON('info.json').then(
    json => id.innerHTML = "<pre>" + json + "</pre>",
    err => id.innerHTML = err
  );

若是 resolve 的參數是一個promise:

var p1 = new Promise(function(resolve, reject){
  //...
});
var p2 = new Promise(function(resolve, reject){
  //...
  resolve(p1);
});

上面代碼中 p1 的狀態傳給了 p2,也就是p1運行完成(狀態爲 resolve 或 reject)後 p2 的回調函數會馬上開始執行:

var p1 = new Promise(function(resolve, reject){
  setTimeout(() => reject(new Error('failed')), 3000);
});
var p2 = new Promise(function(resolve, reject){
  setTimeout(() => resolve(p1), 1000);
});
p2.then(result => console.log(result));
p2.catch(error => console.log(error));

p1 創建,進入 setTimeout 異步計時器。以後 p2 創建,進入 setTimeout 異步計時器。1s 後 p2 準備執行 resolve, 可是 resolve 的參數是 p1, 此時 p1 仍是 Pending 狀態,因此 p2 開始等待。又過了 2s, p1 的 reject 執行,變爲 rejected 狀態,隨即 p2 也跟着變成 rejected 狀態。

Promise 對象方法

  • then() 方法

then(resolve(value){},reject(value){})方法接受2個函數參數,resolve 在 Promise 狀態變爲 resolved 時調用,reject 在 Promise 狀態變爲 rejected 時調用。其中 reject 參數是可選的。和構造函數不一樣的是,then 方法的 reject 和 resolve 都使用 promise 傳出的值做爲其惟一的參數。
then() 方法返回一個新的 Promise 實例,注意,不是以前那個。所以能夠用鏈式調用,不斷添加"回調"函數。 then 的返回值成了下一個 then 中回調函數的參數:

var p = new Promise(function(resolve, reject){
  resolve("from new Promise");
}).then(function (value){
  console.log(value);     //from new Promise    其次輸出這個
  return "from the first 'then'";
}).then(function(value){
  console.log(value);     //from the first 'then'    最後輸出這個
  return "from the second 'then'";
});
console.log(p);           //Promise{...}    先輸出這個

注意,若是 promise 的狀態是 resolved 則執行 then參數中的第一個回調函數;若是 promise 的狀態是 rejected 則執行 then參數中的第二個回調函數。這個狀態是不斷傳遞下來的,這一點和以前的例子相似。

  • catch() 方法:

catch(reject) 方法是 then(null, reject) 的別名,在發生錯誤的時候執行其參數函數:

new Promise(function(resolve, reject){
  resolve("resolved");
}).then(function(val){
  console.log(val);           //resolved
  throw new Error("man-made Error");
}).catch(function(err){
  console.log(err.message);   //man-made Error
});

錯誤會從最初的請求沿着回調函數,一直被傳遞下來。這一點和傳統的錯誤冒泡相似,不管哪裏有錯誤均可以被捕獲到:

new Promise(function(resolve, reject){
  reject(new Error("original Error"));
}).then(function(val){
  console.log(val);           //不執行
  throw new Error("man-made Error");
}).catch(function(err){
  console.log(err.message);   //original Error
});

固然也能夠在半路截住錯誤:

new Promise(function(resolve, reject){
  reject(new Error("original Error"));
}).then(function(val){
  console.log(val);           //不執行
  throw new Error("man-made Error");
}, function(err){
  console.log(`Uncaught Error: ${err.message}`);  //Uncaught Error: original Error
}).catch(function(err){
  console.log(err.message);   //不執行
});

這裏須要注意如下幾點:

  1. reject 和 throw 同樣能夠拋出錯誤。
  2. 在 Promise 狀態變爲 resolved 或 rejected 以後拋出的錯誤會被忽略。
  3. 建議老是使用 catch() 方法,而不要在 then() 方法中定義 reject 函數。
  4. 若是一個 promise 既沒有 catch方法,也沒有能夠捕獲到錯誤的 then 方法,那麼這個錯誤就消失了。它不會到 promise 外面來。
  5. try...catch... 只能捕獲同步代碼的錯誤,不能捕獲異步代碼的錯誤(這個是 ES5 就有的)。
  6. catch() 方法能夠繼續拋出錯誤,就像 try...catch 中的 catch 同樣能夠拋出錯誤。

這裏須要說明的是第4條:錯誤不會到 Promise 外面是 ES6 規範的說法。具體理解(瀏覽器環境):控制檯依舊會報錯,可是不影響 promise 語句以後續代碼執行。此外,promise 語句內的異步語句(如事件,定時器等等)拋出的錯誤,不屬於 promise 內部,發生錯誤會傳播出去:

var p = new Promise(function(resolve, reject){
  resolve("ok");
  setTimeout(function(){throw new Error("setTimeout error")},0);
});
p.then(function(val){console.log(val);});     //ok
//Uncaught Error: setTimeout error

其次,就以上前兩個注意事項舉一例說明:

new Promise(function(resolve, reject){
  resolve("resolved");
  throw "original Error";     //被忽略
}).then(function(val){
  console.log(val);           //resolved
  throw (new Error("man-made Error"));
}).catch(function(err){
  console.log(err.message);   //man-made Error
});

catch 方法的返回值仍是一個新的 promise 對象,能夠繼續調用 then 等其餘方法:

new Promise(function(resolve, reject){
  reject(new Error("reject"));
}).catch(function(err){
  console.log("1st catch");   //被跳過
  return "continue";
}).then(function(val){
  console.log(val);           //continue
});

若是 catch以前沒有錯誤,該 catch 會被跳過。這意味着,catch 不能捕獲在其後面的語句中出現的錯誤:

new Promise(function(resolve, reject){
  resolve("resolved");
}).catch(function(err){
  console.log("1st catch");   //被跳過
}).then(function(val){
  console.log(val);           //resolved
  throw (new Error());
}).catch(function(err){
  console.log("2nd catch");   //2nd catch
});
  • finally() 方法

finally() 接受一個回調函數(無參數)爲參數,和 try...catch...finally 中的 finally 相似,不論 promise 是什麼狀態,該回調函數都必定會運行。能夠用它關閉文件,或者關閉服務器等:

server.listen(0).then(function(){
  //do sth.
}).finally(server.stop);

finally() 內部實現以下:

Promise.prototype.finally = function(callback){
  return this.then(
    value => {Promise.resolve(callback()).then(() => value)},
    error => {Promise.resolve(callback()).then(() => {throw error})}
  );
};
  • done() 方法

done() 方法用在 promise 處理語句的末端,用來處理可能未捕獲的錯誤,並拋向全局。若是其帶有參數,能夠等效爲 done() 以前多了一個 then():

p.done(fun1, fun2);
//至關於
p.then(fun1,fun2).done();

done() 內部實現以下:

Promise.prototype.done = function(onResolve, onRejected){
  this.then(onResolve, onRejected).catch(function(err){
    setTimeout(() => {throw err}, 0);
  });
};

Promise 靜態方法

  • Promise.all()

將多個 promise 對象合併成一個新的 promise 實例。其接受一個裝僅有 promise 對象的可遍歷結構爲參數,若是不是 promise 對象,系統會調用 Promise.resolve() 進行類型轉換。
promise.all() 方法獲得的新的 promise 對象狀態由構成它的全部 promise 對象決定,具體分爲2種狀況:

  1. 當全部構成它的 promise 對象的狀態都變成 resolved,這個新的對象狀態才變爲 resolved。此時構成它全部的 Promise 的返回值構成一個數組做爲新的 promise 對象的回調函數參數;
  2. 當全部構成它的 promise 對象的狀態有一個變成 rejected,這個新的對象狀態就變爲 rejected。此時第一個被 reject 的 Promise 的返回值做爲新的 promise 對象的回調函數參數;
//僞代碼, 因爲沒有正確的 url
var getJSON = function(url){
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.response = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
    function handler(){
      if(client.readyState !== 4) return;
      if(this.status === 200){
        resolve(client.response);
      } else {
        reject(new Error(this.statusText));
      }
    }
  });
  return promise;
}
var pros = ['url1', 'url2', 'url3'].map(url => getJSON(url));
Promise.all(pros).then(function(){
  console.log("all successful");
}, function(){
  console.log("one rejected");       //one rejected, 因爲沒有正確的 url
});
  • Promise.race()

將多個 promise 對象合併成一個新的 promise 實例。其接受一個裝僅有 promise 對象的可遍歷結構爲參數,若是不是 promise 對象,系統會調用 Promise.resolve() 進行類型轉換。
和 promise.all() 不一樣的是 Promise.race() 方法獲得的新的 promise 對象狀態由構成它的 promise 對象中最早改變狀態的那一個決定。

//僞代碼, 因爲沒有正確的 url
var getJSON = function(url){
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.response = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
    function handler(){
      if(client.readyState !== 4) return;
      if(this.status === 200){
        resolve(client.response);
      } else {
        reject(new Error(this.statusText));
      }
    }
  });
  return promise;
}
//若是5s不能得到數據就報錯
var p = Promise.race([
  getJSON("url"),
  new Promise(function(resolve, reject){
    setTimeout(() => reject(new Error("Timeout")), 5000);
  })
]).then(res => console.log(res))
.catch(err => console.log(err));    //Error, 因爲沒有正確的 url
  • Promise.resolve()

將現有對象轉化爲 promise 對象:

var p = Promise.resolve($.ajax('url'));  //jQuery的 $.ajax 方法
//等同於:
var p = new Promise(function(resolve){
  resolve($.ajax('url'));
});

若是傳入 Promise.resolve() 的對象不具備 then 方法(ie. unthenable), 則返回一個狀態爲 resolved 的新 promise 對象。

Promise.resolve("hello").then(function(val){
  console.log(val);                             //hello
});

若是你僅僅想獲得一個 promise 對象,那利用 resolve() 方法是最簡單的:

var promise = Promise.resolve();
  • Promise.reject()

Promise.reject(reason), 返回一個狀態爲 rejected 的 promise 實例。參數 reason 會被傳遞被實例的回調函數。

Promise.reject(new Error("error occured")).catch(err => console.log(err.message));  //error occured

應用舉例

  • 加載圖片:
var preloadImage = function(url){
  return new Promise(function(resolve, reject){
    var image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = url;
  });
};
  • 使用 Generator 管理流程,用 promise 進行異步操做
function getFoo(){
  return new Promise(function(resolve){
    resolve("foo");
  });
}
function* gen(){
  try{
    var foo = yield getFoo();
    console.log(foo);
  } catch(e) {
    console.log(e);
  }
}

var it = gen();
(function go(result){
  if(result.done) return result.value;
  return result.value.then(function(value){
    return go(it.next(value));
  }, function(err){
    return go(it.throw(error));
  });
})(it.next());      //foo
  1. 異步中模擬 sleep 函數
const sleep = (time) => new Promise(function(resolve){
  setTimeout(resolve, time);
});
(async () => {
  for(var i = 0; i < 5; i++){
    await sleep(1000);
    console.log(new Date, i);
  }
  await sleep(1000);
  console.log(new Date, i);
})();
相關文章
相關標籤/搜索