ES6語法——異步(Promise、Generator、async)

http://es6.ruanyifeng.com/阮一峯ES6

異步,簡單說就是一個任務不是連續完成的。比如,有一個任務是讀取文件進行處理,向操作系統發出請求,要求讀取文件;程序執行其他任務;等到操作系統返回文件,再處理文件。相應地,連續的執行就叫做同步,不能插入其它任務,操作系統從硬盤讀取文件的這段時間,程序只能乾等着。

異步編程的方法:回調函數、事件監聽、發佈/訂閱、Promise對象、Generator函數。

回調函數callback:把任務的第二段單獨寫在一個函數中,等到重新執行這個任務時,直接調用這個函數。

// 讀取文件進行處理
// 等到操作系統返回/etc/passwd這個文件後,回調函數纔會執行。
fs.readFile('/etc/passwd','utf-8',function(err,data){
  if(err) throw err;
  console.log(data);
});
// 回調  寫法複雜,難以分清執行順序,影響維護
let ajax=function(callback){
  console.log("ajax");
  setTimeout(function(){
    callback&&callback.call()
  },1000);
}
ajax(function(){
  console.log('callback');
});

 

十三、Promise對象

Promise是異步編程的一種解決方案,比傳統的解決方案(回調函數和事件)更合理和更強大。

Promise是一個容器,保存着某個未來纔會結束的事件(通常是一個異步操作)的結果。Promise是一個對象,從它可以獲取異步操作的消息,提供統一的 API,各種異步操作都可以用同樣的方法進行處理。

(1)對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。

(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種情況發生,狀態就凝固了,會一直保持這個結果,稱爲resolved(已定型)。如果改變已經發生了,再對Promise對象添加回調函數,也會立即得到這個結果。而對於事件(Event),如果錯過了它再去監聽,是得不到結果的。

Promise對象將異步操作以同步操作的流程表達,避免了層層嵌套的回調函數;提供統一的接口,使控制異步操作更加容易。也有一些缺點,一旦新建它就會立即執行,無法中途取消;如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部;當處於pending狀態時,無法得知目前進展到哪一個階段。

如果某些事件不斷地反覆發生,一般使用Stream模式比部署Promise更好。

1、new Promise()

Promise對象是一個構造函數,用來生成Promise實例。接受一個函數作爲參數,該函數的參數分別是resolve和reject函數。

resolve函數將Promise對象的狀態從“未完成”變爲“成功”,在異步操作成功時調用,並將異步操作的結果,作爲參數傳遞出去;reject函數將Promise對象的狀態從“未完成”變爲“失敗”,在異步操作失敗時調用,並將異步操作報出的錯誤,作爲參數傳遞出去。

Promise新建後就會立即執行。調用resolve或reject以後,Promise的使命就完成了,後繼操作應該放到then方法裏面,而不應該直接寫在resolve或reject後面。最好在它們前面加上return語句。

const promise=new Promise(function(resolve,reject){
  // ... some code

  if(/* 異步操作成功 */){
    resolve(value);
  }else{
    reject(error);
  }
});

2、Promise.prototype.then()、catch()和finally()

Promise實例生成後,用then方法分別指定resolve和rejected狀態的回調函數(異步操作無法使用return把結果返回給調用者),第二個函數是可選的。

返回新的Promise實例,可以採用鏈式寫法,指定一組按照次序調用的回調函數。前一個回調函數,有可能返回的還是一個Promise對象(即有異步操作),後一個回調函數,會等待該Promise對象的狀態發生變化,纔會被調用。

promise.then(function(value){
  // success
},function(error){
  // failure
});
/** 執行步驟:(假設有一個.then)
1. 定義函數ajax;
2. 執行ajax();
3. 打印"ajax",new Promise()並返回;
   function(resolve,reject)開始異步執行;
4. .then指定了function(resolve,reject)的回調;
5. function(resolve,reject)執行完。
*/
let ajax=function(){
  console.log("ajax");
  return new Promise(function(resolve,reject){
    setTimeout(resolve,1000);
  })
}

ajax()
.then(function(){
  console.log("timeout1");
  return new Promise(function(resolve,reject){
    setTimeout(resolve,2000);
  })
})
.then(function(){
  console.log("timeout2")
})

catch方法是.then(null,rehection)或.then(undefined,rejection)的別名,用於指定發生錯誤時(rejected狀態)的回調函數。一般不要在then方法使用第二個參數),總是使用catch方法。Promise對象的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲爲止,即錯誤總是會被下一個catch語句捕獲。

Promise對象拋出的錯誤不會傳遞到外層代碼,不會終止程序。

const promise=new Promise(function(resolve,reject){
  throw new Error('test');
});
promise.catch(function(error){
  console.log(error);
})
let ajax=function(){
  return new Promise(function(resolve,reject){
    throw new Error("promise1 error");
    return resolve();
  })
}

/* 錯誤一直向後傳遞,直到catch捕獲,跳過中間的.then */
ajax()
.then(function(){
  console.log("promise1");
  return new Promise(function(resolve,reject){
    return resolve();
  })
})
.then(function(){
  console.log("promise2");
})
.catch(function(){
  console.log("reject");
})

console.log("outside");
// outside
// reject

/* 每一步.then通過reject處理錯誤,不影響之後的.then */
ajax()
.then(function(){
  console.log("promise1");
  return new Promise(function(resolve,reject){
    return resolve();
  })
},function(error){
  console.log("reject"+error);
})
.then(function(){
  console.log("promise2");
})

console.log("outside");
// outside
// rejectError: promise1 error
// promise2

finally方法指定不管Promise對象最後狀態如何,都會執行的操作。

server.listen(port)
  .then(function(){
    // ...
  })
  .finally(server.stop);

3、Promise.all()和race()

Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。

參數必須具有Iterator接口,且返回的每個成員都是Promise實例,否則調用Promise.resolve方法將成員轉爲Promise實例。

只有所有成員的狀態是fulfilled,p的狀態纔是fulfilled,此時將所有成員的返回值組成一個數組傳給p的回調函數。如果p的狀態是rejected,將第一個被reject的實例的返回值傳給p的回調函數。

const p=Promise.all([p1,p2,p3]);
/* 所有圖片加載完再添加到頁面 */
function loadImg(src){
  return new Promise((resolve,reject)=>{
    let img=document.createElement('img');
    img.src=src;
    img.οnlοad=function(){
      resolve(img);
    }
    img.οnerrοr=function(err){
      reject(err);
    }
  })
}
function showImgs(imgs){
  imgs.forEach(function(img){
    document.body.appendChild(img);
  })
}
Promise.all([
  loadImg("https://img-blog.csdn.net/20180830223234851?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lhb2NvbmcxOTkz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70"),
  loadImg("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1546709317586&di=b8a1d3ca2d74a9a0e143a08487a753b5&imgtype=0&src=http%3A%2F%2Fdesk-fd.zol-img.com.cn%2Ft_s960x600c5%2Fg5%2FM00%2F02%2F03%2FChMkJlbKxpKIBV3oABpO1k7kXwEAALHnQB8IREAGk7u679.jpg"),
  loadImg("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1546709346183&di=714f1f97a96934a9a3ee1d42a5fbd775&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201405%2F22%2F20140522162020_jJPVm.jpeg")
]).then(showImgs)

Promise.race方法與Promise.all方法的不同之處在於,只要成員中有一個實例率先改變狀態,p的狀態就跟着改變。率先改變的Promise實例的返回值,就傳遞給p的回調函數。

/* 有一張圖片加載完就添加到頁面 */
function showImg(img){
    document.body.appendChild(img);
}
Promise.race([
  loadImg("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1546709317586&di=b8a1d3ca2d74a9a0e143a08487a753b5&imgtype=0&src=http%3A%2F%2Fdesk-fd.zol-img.com.cn%2Ft_s960x600c5%2Fg5%2FM00%2F02%2F03%2FChMkJlbKxpKIBV3oABpO1k7kXwEAALHnQB8IREAGk7u679.jpg"),
  loadImg("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1546709346183&di=714f1f97a96934a9a3ee1d42a5fbd775&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201405%2F22%2F20140522162020_jJPVm.jpeg"),
  loadImg("https://img-blog.csdn.net/20180830223234851?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lhb2NvbmcxOTkz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70")
]).then(showImg)

4、Promise.resolve()和reject()

Promise.resolve方法將現有對象轉爲Promise對象。

Promise.reject(reason)方法返回一個新的Promise實例,該實例的狀態爲rejected。參數會原封不動地作爲reject的理由,變成後續方法的參數。

十四、Generator函數

1、簡介

語法上,Generator函數是一個狀態機,封裝了多個內部狀態;返回一個遍歷器對象,可以依次遍歷Generator函數內部的每一個狀態。形式上,Generator函數是一個普通函數,但function關鍵字與函數名之間有一個星號,函數體內部使用yield表達式定義不同的內部狀態。

function* helloWorldGenerator(){
  yield 'hello';
  yield 'world';
  return 'ending';
}
let hw=helloWorldGenerator();
console.log(hw.next()); // {value: "hello", done: false}
console.log(hw.next()); // {value: "world", done: false}
console.log(hw.next()); // {value: "ending", done: true}
console.log(hw.next()); // {value: undefined, done: true}

上述函數有三個狀態hello,world和return語句(結束執行)。調用Generator函數後,該函數並不執行,返回一個指向內部狀態的指針對象(遍歷器對象)。每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。將yield或return後面表達式的值作爲返回的對象的value值。

Generator函數是分段執行的,yield表達式是暫停執行的標記,next方法可以恢復執行。可以不用yield表達式,這時就變成了一個單純的暫緩執行函數。

2、狀態機

let clock=function* (){
  while(true){
    console.log("Tick!");
    yield;
    console.log("Tock!");
    yield;
  }
}
let status=clock();
status.next(); // Tick!
status.next(); // Tock!
status.next(); // Tick!
status.next(); // Tock!

十五、async函數

async函數就是將Generator函數的星號(*)替換成async,將yield替換成await。對Generator函數的改進,體現在以下幾點。

(1)內置執行器:才能真正執行,而async函數自帶執行器,執行與普通函數相同,不需要調用next方法或用co模塊。

(2)更好的語義:對比星號和yield,async表示函數裏有異步操作,await表示緊跟在後面的表達式需要等待結果。

(3)更廣的適用性:async函數的await命令後面,可以是Promise對象和原始類型的值(自動轉成立即resolved的Promise對象)。

(4)返回值是Promise。

async函數可以看作多個異步操作,包裝成的一個Promise對象,而await命令是內部then命令的語法糖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章