日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

瀏覽器中的 JAVAScript 是典型的事件驅動型程序,即它們會等待用戶觸發后才真正的執行,而基于的JavaScript的服務器通常要等待客戶端通過網絡發送請求,然后才能執行。這種異步編程在JavaScript是很常見的,下面就來介紹幾個異步編程的重要特性,它們可以使編寫異步代碼更容易。

本文將按照異步編程方式的出現時間來歸納整理:

深入淺出JavaScript異步編程

一、什么是異步

下面先來看看同步和異步的概念:

  • 同步: 在執行某段代碼時,在沒有得到返回結果之前,其他代碼暫時是無法執行的,但是一旦執行完成拿到返回值,即可執行其他代碼。也就是說,在此段代碼執行完未返回結果之前,會阻塞之后的代碼執行,這樣的情況稱為同步。
  • 異步: 當某一代碼執行異步過程調用發出后,這段代碼不會立刻得到返回結果。而是在異步調用發出之后,一般通過回調函數處理這個調用之后拿到結果。異步調用發出后,不會影響阻塞后面的代碼執行,這樣的情況稱為異步。

下面來看一個例子:

// 同步
function syncAdd(a, b) {
  return a + b;
}

syncAdd(1, 2) // 立即得到結果:3

// 異步
function asyncAdd(a, b) {
  setTimeout(function() {
    console.log(a + b);
  }, 1000)
}

asyncAdd(1, 2) // 1s后打印結果:3

這里定義了同步函數 syncAdd 和異步函數 asyncAdd,調用 syncAdd(1, 2) 函數時會等待得到結果之后再執行后面的代碼。而調用 asyncAdd(1, 2) 時則會在得到結果之前繼續執行,直到 1 秒后得到結果并打印。

我們知道,JavaScript 是單線程的,如果代碼同步執行,就可能會造成阻塞;而如果使用異步則不會阻塞,不需要等待異步代碼執行的返回結果,可以繼續執行該異步任務之后的代碼邏輯。因此,在 JavaScript 編程中,會大量使用異步。

那為什么單線程的JavaScript還能實現異步呢,其實也沒有什么魔法,只是把一些操作交給了其他線程處理,然后采用了事件循環的機制來處理返回結果。

二、回調函數

在最基本的層面上,JavaScript的異步編程式通過回調實現的。回調的是函數,可以傳給其他函數,而其他函數會在滿足某個條件時調用這個函數。下面就來看看常見的不同形式的基于回調的異步編程。

1. 定時器

一種最簡單的異步操作就是在一定時間之后運行某些代碼。如下面代碼:

setTimeout(asyncAdd(1, 2), 8000)

setTimeout()方法的第一個參數是一個函數,第二個參數是以毫秒為單位的時間間隔。asyncAdd()方法可能是一個回調函數,而setTimeout()方法就是注冊回調函數的函數。它還代指在什么異步條件下調用回調函數。setTimeout()方法只會調用一次回調函數。

2. 事件監聽

給目標 DOM 綁定一個監聽函數,用的最多的是 addEventListener

document.getElementById('#myDiv').addEventListener('click', (e) => {
  console.log('我被點擊了')
}, false);

通過給 id 為 myDiv 的一個元素綁定了點擊事件的監聽函數,把任務的執行時機推遲到了點擊這個動作發生時。此時,任務的執行順序與代碼的編寫順序無關,只與點擊事件有沒有被觸發有關

這里使用addEventListener注冊了回調函數,這個方法的第一個參數是一個字符串,指定要注冊的事件類型,如果用戶點擊了指定的元素,瀏覽器就會調用回調函數,并給他傳入一個對象,其中包含著事件的詳細信息。

3. 網絡請求

JavaScript中另外一種常見的異步操作就是網絡請求:

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 創建 Http 請求
xhr.open("GET", SERVER_URL, true);
// 設置狀態監聽函數
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 當請求成功時
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 設置請求失敗時的監聽函數
xhr.onerror = function() {
  console.error(this.statusText);
};
// 發送 Http 請求
xhr.send(null);

這里使用XMLHttpRequest類及回調函數來發送HTTP請求并異步處理服務器返回的響應。

4. Node中的回調與事件

Node.js服務端JavaScript環境底層就是異步的,定義了很多使用回調和事件的API。例如讀取文件默認的API就是異步的,它會在讀取文件內容之后調用一個回調函數:

const fs = require('fs');
let options = {}

//  讀取配置文件,調用回調函數
fs.readFile('config.json', 'utf8', (err, data) => {
    if(err) {
      throw err;
    }else{
     Object.assign(options, JSON.parse(data))
    }
  startProgram(options)
});

fs.readFile()方法以接收兩個參數的回調作為最后一個參數。它會異步讀取指定文件,如果讀取成功就會將第二個參數傳遞給回調的第二個參數,如果發生錯誤,就會將錯誤傳遞給回調的第一個參數。

三、Promise

1. Promise的概念

Promise是一種為簡化異步編程而設計的核心語言特性,它是一個對象,表示異步操作的結果。在最簡單的情況下,Promise就是一種處理回調的不同方式。不過,使用Promise也有實際的用處,基于回調的異步編程會有一個很現實的問題,那就是經常出現回調多層嵌套的情況,會造成代碼難以理解。Promise可以讓這種嵌套回調以一種更線性的鏈式形式表達出來,因此更容易閱讀和理解。

回調的另一個問題就是難以處理錯誤, 如果一個異步函數拋出異常,則該異常沒有辦法傳播到異步操作的發起者。異步編程的一個基本事實就是它破壞了異常處理。而Promise則標準化了異步錯誤處理,通過Promise鏈提供一種讓錯誤正確傳播的途經。

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

(1)Promise實例有三個狀態:

  • pending 狀態:表示進行中。Promise 實例創建后的初始態;
  • fulfilled 狀態:表示成功完成。在執行器中調用 resolve 后達成的狀態;
  • rejected 狀態:表示操作失敗。在執行器中調用 reject 后達成的狀態。

(2)Promise實例有兩個過程

  • pending -> fulfilled : Resolved(已完成);
  • pending -> rejected:Rejected(已拒絕)。

Promise的特點:

  • 一旦狀態改變就不會再變,promise對象的狀態改變,只有兩種可能:從pending變為fulfilled,從pending變為rejected。當 Promise 實例被創建時,內部的代碼就會立即被執行,而且無法從外部停止。比如無法取消超時或消耗性能的異步調用,容易導致資源的浪費;
  • 如果不設置回調函數,Promise內部拋出的錯誤,不會反映到外部;
  • Promise 處理的問題都是“一次性”的,因為一個 Promise 實例只能 resolve 或 reject 一次,所以面對某些需要持續響應的場景時就會變得力不從心。比如上傳文件獲取進度時,默認采用的就是事件監聽的方式來實現。

下面來看一個例子:

const https = require('https');

function httpPromise(url){
  return new Promise((resolve,reject) => {
    https.get(url, (res) => {
      resolve(data);
    }).on("error", (err) => {
      reject(error);
    });
  })
}

httpPromise().then((data) => {
  console.log(data)
}).catch((error) => {
  console.log(error)
})

可以看到,Promise 會接收一個執行器,在這個執行器里,需要把目標異步任務給放進去。在 Promise 實例創建后,執行器里的邏輯會立刻執行,在執行的過程中,根據異步返回的結果,決定如何使用 resolve 或 reject 來改變 Promise實例的狀態。

在這個例子里,當用 resolve 切換到了成功態后,Promise 的邏輯就會走到 then 中傳入的方法里去;用 reject 切換到失敗態后,Promise 的邏輯就會走到 catch 傳入的方法中。

這樣的邏輯,本質上與回調函數中的成功回調和失敗回調沒有差異。但這種寫法大大地提高了代碼的質量。當我們進行大量的異步鏈式調用時,回調地獄不復存在了。取而代之的是層級簡單、賞心悅目的 Promise 調用鏈:

httpPromise(url1)
    .then(res => {
        console.log(res);
        return httpPromise(url2);
    })
    .then(res => {
        console.log(res);
        return httpPromise(url3);
    })
    .then(res => {
      console.log(res);
      return httpPromise(url4);
    })
    .then(res => console.log(res));

2. Promise的創建

Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。

Promise構造函數接受一個函數作為參數,該函數的兩個參數分別是resolvereject

const promise = new Promise((resolve, reject) => {
  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一般情況下,我們會用new Promise()來創建Promise對象。除此之外,還也可以使用promise.resolve和 promise.reject這兩個方法來創建:

(1)Promise.resolve

Promise.resolve(value)的返回值是一個promise對象,我們可以對返回值進行.then調用,如下代碼:

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

resolve(11)會讓promise對象進入確定(resolve狀態),并將參數11傳遞給后面then中指定的onFulfilled 函數;

(2)Promise.reject

Promise.reject 的返回值也是一個promise對象,如下代碼:

Promise.reject(new Error("我錯了!"));

上面是以下代碼的簡單形式:

new Promise((resolve, reject) => {
   reject(new Error("我錯了!"));
});

下面來綜合看看resolve方法和reject方法:

function testPromise(ready) {
  return new Promise(resolve,reject) => {
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};

testPromise(true).then((msg) => {
  console.log(msg);
},(error) => {
  console.log(error);
});

上面的代碼給testPromise方法傳遞一個參數,返回一個promise對象,如果為true,那么調用Promise對象中的resolve()方法,并且把其中的參數傳遞給后面的then第一個函數內,因此打印出 “hello world”, 如果為false,會調用promise對象中的reject()方法,則會進入then的第二個函數內,會打印No thanks

3. Promise的作用

在開發中可能會碰到這樣的需求:使用ajax發送A請求,成功后拿到數據,需要把數據傳給B請求,那么需要這樣編寫代碼:

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

這段代碼之所以看上去很亂,歸結其原因有兩點:

  • 第一是嵌套調用,下面的任務依賴上個任務的請求結果,并在上個任務的回調函數內部執行新的業務邏輯,這樣當嵌套層次多了之后,代碼的可讀性就變得非常差了。
  • 第二是任務的不確定性,執行每個任務都有兩種可能的結果(成功或者失敗),所以體現在代碼中就需要對每個任務的執行結果做兩次判斷,這種對每個任務都要進行一次額外的錯誤處理的方式,明顯增加了代碼的混亂程度。

既然原因分析出來了,那么問題的解決思路就很清晰了:

  • 消滅嵌套調用;
  • 合并多個任務的錯誤處理。

這么說可能有點抽象,不過 Promise 解決了這兩個問題。接下來就看看 Promise 是怎么消滅嵌套調用和合并多個任務的錯誤處理的。

Promise出現之后,代碼可以這樣寫:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

通過引入 Promise,上面這段代碼看起來就非常線性了,也非常符合人的直覺。Promise 利用了三大技術手段來解決回調地獄:回調函數延遲綁定、返回值穿透、錯誤冒泡。

下面來看一段代碼:

let readFilePromise = (filename) => {
  fs.readFile(filename, (err, data) => {
    if(err) {
      reject(err);
    }else {
      resolve(data);
    }
  })
}
readFilePromise('1.json').then(data => {
  return readFilePromise('2.json')
});

可以看到,回調函數不是直接聲明的,而是通過后面的 then 方法傳入的,即延遲傳入,這就是回調函數延遲綁定。接下來針對上面的代碼做一下調整,如下:

let x = readFilePromise('1.json').then(data => {
  return readFilePromise('2.json')  //這是返回的Promise
});
x.then()

根據 then 中回調函數的傳入值創建不同類型的 Promise,然后把返回的 Promise 穿透到外層,以供后續的調用。這里的 x 指的就是內部返回的 Promise,然后在 x 后面可以依次完成鏈式調用。這便是返回值穿透的效果,這兩種技術一起作用便可以將深層的嵌套回調寫成下面的形式。

readFilePromise('1.json').then(data => {
    return readFilePromise('2.json');
}).then(data => {
    return readFilePromise('3.json');
}).then(data => {
    return readFilePromise('4.json');
});

這樣就顯得清爽許多,更重要的是,它更符合人的線性思維模式,開發體驗更好,兩種技術結合產生了鏈式調用的效果。

這樣解決了多層嵌套的問題,那另外一個問題,即每次任務執行結束后分別處理成功和失敗的情況怎么解決的呢?Promise 采用了錯誤冒泡的方式。下面來看效果:

readFilePromise('1.json').then(data => {
    return readFilePromise('2.json');
}).then(data => {
    return readFilePromise('3.json');
}).then(data => {
    return readFilePromise('4.json');
}).catch(err => {
  // xxx
})

這樣前面產生的錯誤會一直向后傳遞,被 catch 接收到,就不用頻繁地檢查錯誤了。從上面的這些代碼中可以看到,Promise 解決效果也比較明顯:實現鏈式調用,解決多層嵌套問題;實現錯誤冒泡后一站式處理,解決每次任務中判斷錯誤、增加代碼混亂度的問題。

4. Promise的方法

Promise常用的方法:then()、catch()、all()、race()、finally()、allSettled()、any()。

(1)then()

當Promise執行的內容符合成功條件時,調用resolve函數,失敗就調用reject函數。那Promise創建完了,該如何調用呢?這時就該then出場了:

promise.then(function(value) {
  // success
}, function(error) {
  // fAIlure
});

then方法接受兩個回調函數作為參數。第一個回調函數是Promise對象的狀態變為resolved時調用,第二個回調函數是Promise對象的狀態變為rejected時調用。其中第二個參數可以省略。

then方法返回的是一個新的Promise實例。因此可以采用鏈式寫法,即then方法后面再調用另一個then方法。當寫有順序的異步事件時,需要串行時,可以這樣寫:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    
})

(2)catch()

Promise對象的catch方法相當于then方法的第二個參數,指向reject的回調函數。

不過catch方法還有一個作用,就是在執行resolve回調函數時,如果出現錯誤,拋出異常,不會停止運行,而是進入catch方法中:

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
}); 

(3)all()

all方法可以完成并行任務, 它接收一個數組,數組的每一項都是一個promise對象。當數組中所有的promise的狀態都達到resolved時,all方法的狀態就會變成resolved,如果有一個狀態變成了rejected,那么all方法的狀態就會變成rejected

let promise1 = new Promise((resolve,reject)=>{
 setTimeout(()=>{
       resolve(1);
 },2000)
});
let promise2 = new Promise((resolve,reject)=>{
 setTimeout(()=>{
       resolve(2);
 },1000)
});
let promise3 = new Promise((resolve,reject)=>{
 setTimeout(()=>{
       resolve(3);
 },3000)
});

Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);  //結果為:[1,2,3] 
})

調用all方法時的結果成功的時候是回調函數的參數也是一個數組,這個數組按順序保存著每一個promise對象resolve執行時的值。

(4)race()

race方法和all一樣,接受的參數是一個每項都是promise的數組,但與all不同的是,當最先執行完的事件執行完之后,就直接返回該promise對象的值。

如果第一個promise對象狀態變成resolved,那自身的狀態變成了resolved;反之,第一個promise變成rejected,那自身狀態就會變成rejected

let promise1 = new Promise((resolve,reject) => {
 setTimeout(() =>  {
       reject(1);
 },2000)
});
let promise2 = new Promise((resolve,reject) => {
 setTimeout(() => {
       resolve(2);
 },1000)
});
let promise3 = new Promise((resolve,reject) => {
 setTimeout(() => {
       resolve(3);
 },3000)
});
Promise.race([promise1,promise2,promise3]).then(res => {
 console.log(res); //結果:2
},rej => {
    console.log(rej)};
)

那么race方法有什么實際作用呢?當需要執行一個任務,超過多長時間就不做了,就可以用這個方法來解決:

Promise.race([promise1, timeOutPromise(5000)]).then(res => console.log(res))

(5)finally()

finally方法用于指定不管 Promise 對象最后狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。

promise.then(result => {···})
    .catch(error => {···})
       .finally(() => {···});

上面代碼中,不管promise最后的狀態如何,在執行完thencatch指定的回調函數以后,都會執行finally方法指定的回調函數。

下面來看例子,服務器使用 Promise 處理請求,然后使用finally方法關掉服務器。

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

finally方法的回調函數不接受任何參數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled還是rejected。這表明,finally方法里面的操作,應該是與狀態無關的,不依賴于 Promise 的執行結果。

finally本質上是then方法的特例:

promise
.finally(() => {
  // 語句
});

// 等同于
promise
.then(
  result => {
    // 語句
    return result;
  },
  error => {
    // 語句
    throw error;
  }
);

上面代碼中,如果不使用finally方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了finally方法,則只需要寫一次。

(6)allSettled()

Promise.allSettled 的語法及參數跟 Promise.all 類似,其參數接受一個 Promise 的數組,返回一個新的 Promise。唯一的不同在于,執行完之后不會失敗,也就是說當 Promise.allSettled 全部處理完成后,我們可以拿到每個 Promise 的狀態,而不管其是否處理成功。

下面使用 allSettled 實現的一段代碼:

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回結果:
// [
//    { status: 'fulfilled', value: 2 },
//    { status: 'rejected', reason: -1 }
// ]

可以看到,Promise.allSettled 最后返回的是一個數組,記錄傳進來的參數中每個 Promise 的返回值,這就是和 all 方法不太一樣的地方。你也可以根據 all 方法提供的業務場景的代碼進行改造,其實也能知道多個請求發出去之后,Promise 最后返回的是每個參數的最終狀態。

(7)any()

any 方法返回一個 Promise,只要參數 Promise 實例有一個變成 fullfilled 狀態,最后 any 返回的實例就會變成 fullfilled 狀態;如果所有參數 Promise 實例都變成 rejected 狀態,包裝實例就會變成 rejected 狀態。

下面對上面 allSettled 這段代碼進行改造,來看下改造完的代碼和執行結果:

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.any([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回結果:2

可以看出,只要其中一個 Promise 變成 fullfilled 狀態,那么 any 最后就返回這個 Promise。由于上面 resolved 這個 Promise 已經是 resolve 的了,故最后返回結果為 2。

5. Promise的異常處理

錯誤處理是所有編程范型都必須要考慮的問題,在使用 JavaScript 進行異步編程時,也不例外。如果我們不做特殊處理,會怎樣呢?來看下面的代碼,先定義一個必定會失敗的方法

let fail = () => {
    setTimeout(() => {
 throw new Error("fail");
    }, 1000);
};

調用:

console.log(1);
try {
    fail();
} catch (e) {
    console.log("captured");
}
console.log(2);

可以看到打印出了 1 和 2,并在 1 秒后,獲得一個“Uncaught Error”的錯誤打印,注意觀察這個錯誤的堆棧:

Uncaught Error: fail
    at <anonymous>:3:9

可以看到,其中的 setTimeout (async) 這樣的字樣,表示著這是一個異步調用拋出的堆棧。但是,captured”這樣的字樣也并未打印,因為母方法 fail() 本身的原始順序執行并沒有失敗,這個異常的拋出是在回調行為里發生的。 從上面的例子可以看出,對于異步編程來說,我們需要使用一種更好的機制來捕獲并處理可能發生的異常。

Promise 除了支持 resolve 回調以外,還支持 reject 回調,前者用于表示異步調用順利結束,而后者則表示有異常發生,中斷調用鏈并將異常拋出:

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});

上面的代碼中,flag 參數用來控制流程是順利執行還是發生錯誤。在錯誤發生的時候,no 字符串會被傳遞給 reject 函數,進一步傳遞給調用鏈:

Promise.resolve()
       .then(exe(false))
       .then(exe(true));

上面的調用鏈,在執行的時候,第二行就傳入了參數 false,它就已經失敗了,異常拋出了,因此第三行的 exe 實際沒有得到執行,執行結果如下:

false
Uncaught (in promise) no

這就說明,通過這種方式,調用鏈被中斷了,下一個正常邏輯 exe(true) 沒有被執行。 但是,有時候需要捕獲錯誤,而繼續執行后面的邏輯,該怎樣做?這種情況下就要在調用鏈中使用 catch 了:

Promise.resolve()
       .then(exe(false))
       .catch((info) => { console.log(info); })
       .then(exe(true));

這種方式下,異常信息被捕獲并打印,而調用鏈的下一步,也就是第四行的 exe(true) 可以繼續被執行。將看到這樣的輸出:

false
no
true

6. Promise的實現

這一部分就來簡單實現一下Promise及其常用的方法。

(1)Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
  // 保存初始化狀態
  var self = this;

  // 初始化狀態
  this.state = PENDING;

  // 用于保存 resolve 或者 rejected 傳入的值
  this.value = null;

  // 用于保存 resolve 的回調函數
  this.resolvedCallbacks = [];

  // 用于保存 reject 的回調函數
  this.rejectedCallbacks = [];

  // 狀態轉變為 resolved 方法
  function resolve(value) {
    // 判斷傳入元素是否為 Promise 值,如果是,則狀態改變必須等待前一個狀態改變后再進行改變
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保證代碼的執行順序為本輪事件循環的末尾
    setTimeout(() => {
      // 只有狀態為 pending 時才能轉變,
      if (self.state === PENDING) {
        // 修改狀態
        self.state = RESOLVED;

        // 設置傳入的值
        self.value = value;

        // 執行回調函數
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 狀態轉變為 rejected 方法
  function reject(value) {
    // 保證代碼的執行順序為本輪事件循環的末尾
    setTimeout(() => {
      // 只有狀態為 pending 時才能轉變
      if (self.state === PENDING) {
        // 修改狀態
        self.state = REJECTED;

        // 設置傳入的值
        self.value = value;

        // 執行回調函數
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 將兩個方法傳入函數執行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到錯誤時,捕獲錯誤,執行 reject 函數
    reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判斷兩個參數是否為函數類型,因為這兩個參數是可選參數
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };

  // 如果是等待狀態,則將函數加入對應列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }

  // 如果狀態已經凝固,則直接執行對應狀態的函數

  if (this.state === RESOLVED) {
    onResolved(this.value);
  }

  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

(2)Promise.then

then 方法返回一個新的 promise 實例,為了在 promise 狀態發生變化時(resolve / reject 被調用時)再執行 then 里的函數,我們使用一個 callbacks 數組先把傳給then的函數暫存起來,等狀態改變時再調用。

那么,怎么保證后一個 then 里的方法在前一個 then(可能是異步)結束之后再執行呢?

可以將傳給 then 的函數和新 promise 的 resolve 一起 push 到前一個 promise 的 callbacks 數組中,達到承前啟后的效果:

  • 承前:當前一個 promise 完成后,調用其 resolve 變更狀態,在這個 resolve 里會依次調用 callbacks 里的回調,這樣就執行了 then 里的方法了
  • 啟后:上一步中,當 then 里的方法執行完成后,返回一個結果,如果這個結果是個簡單的值,就直接調用新 promise 的 resolve,讓其狀態變更,這又會依次調用新 promise 的 callbacks 數組里的方法,循環往復。。如果返回的結果是個 promise,則需要等它完成之后再觸發新 promise 的 resolve,所以可以在其結果的 then 里調用新 promise 的 resolve
then(onFulfilled, onReject){
    // 保存前一個promise的this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封裝前一個promise成功時執行的函數
      let fulfilled = () => {
        try{
          const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //啟后
        }catch(err){
          reject(err)
        }
      }
      // 封裝前一個promise失敗時執行的函數
      let rejected = () => {
        try{
          const result = onReject(self.reason);
          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
        }catch(err){
          reject(err)
        }
      }
      switch(self.status){
        case PENDING: 
          self.onFulfilledCallbacks.push(fulfilled);
          self.onRejectedCallbacks.push(rejected);
          break;
        case FULFILLED:
          fulfilled();
          break;
        case REJECT:
          rejected();
          break;
      }
    })
   }

注意:

  • 連續多個 then 里的回調方法是同步注冊的,但注冊到了不同的 callbacks 數組中,因為每次 then 都返回新的 promise 實例(參考上面的例子和圖)
  • 注冊完成后開始執行構造函數中的異步事件,異步完成之后依次調用 callbacks 數組中提前注冊的回調

(3)Promise.all

該方法的參數是 Promise 的實例數組, 然后注冊一個 then 方法。 待數組中的 Promise 實例的狀態都轉為 fulfilled 之后則執行 then 方法.,這里主要就是一個計數邏輯, 每當一個 Promise 的狀態變為 fulfilled 之后就保存該實例返回的數據, 然后將計數減一, 當計數器變為 0 時, 代表數組中所有 Promise 實例都執行完畢.

Promise.all = function (arr) {
  let args = Array.prototype.slice.call(arr)
  return new Promise(function (resolve, reject) {
    if (args.length === 0) return resolve([])
    let remaining = args.length
    function res(i, val) {
      try {
        if (val && (typeof val === 'object' || typeof val === 'function')) {
          let then = val.then
          if (typeof then === 'function') {
            then.call(val, function (val) { // 這里如果傳入參數是 promise的話需要將結果傳入 args, 而不是 promise實例
              res(i, val) 
            }, reject)
            return
          }
        }
        args[i] = val
        if (--remaining === 0) {
          resolve(args)
        }
      } catch (ex) {
        reject(ex)
      }
    }
    for (let i = 0; i < args.length; i++) {
      res(i, args[i])
    }
  })
}

(4)Promise.race

該方法的參數是 Promise 實例數組, 然后其 then 注冊的回調方法是數組中的某一個 Promise 的狀態變為 fulfilled 的時候就執行. 因為 Promise 的狀態只能改變一次, 那么我們只需要把 Promise.race 中產生的 Promise 對象的 resolve 方法, 注入到數組中的每一個 Promise 實例中的回調函數中即可:

oPromise.race = function (args) {
  return new oPromise((resolve, reject) => {
    for (let i = 0, len = args.length; i < len; i++) {
      args[i].then(resolve, reject)
    }
  })
}

四、Generator

1. Generator 概述

(1)Generator

Generator(生成器)是 ES6 中的關鍵詞,通俗來講 Generator 是一個帶星號的函數(它并不是真正的函數),可以配合 yield 關鍵字來暫停或者執行函數。先來看一個例子:

function* gen() {
  console.log("enter");
  let a = yield 1;
  let b = yield (function () {return 2})();
  return 3;
}
var g = gen()           // 阻塞,不會執行任何語句
console.log(typeof g)   // 返回 object 這里不是 "function"
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next()) 

輸出結果如下:

object
enter
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }
{ value: undefined, done: true }

Generator 中配合使用 yield 關鍵詞可以控制函數執行的順序,每當執行一次 next 方法,Generator 函數會執行到下一個存在 yield 關鍵詞的位置。

總結,Generator 的執行的關鍵點如下:

  • 調用 gen() 后,程序會阻塞,不會執行任何語句;
  • 調用 g.next() 后,程序繼續執行,直到遇到 yield 關鍵詞時執行暫停;
  • 一直執行 next 方法,最后返回一個對象,其存在兩個屬性:value 和 done。

(2)yield

yield 同樣也是 ES6 的關鍵詞,配合 Generator 執行以及暫停。yield 關鍵詞最后返回一個迭代器對象,該對象有 value 和 done 兩個屬性,其中 done 屬性代表返回值以及是否完成。yield 配合著 Generator,再同時使用 next 方法,可以主動控制 Generator 執行進度。

下面來看看多個 Generator 配合 yield 使用的情況:

function* gen1() {
    yield 1;
    yield* gen2();
    yield 4;
}
function* gen2() {
    yield 2;
    yield 3;
}
var g = gen1();
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

執行結果如下:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{value: undefined, done: true}

可以看到,使用 yield 關鍵詞的話還可以配合著 Generator 函數嵌套使用,從而控制函數執行進度。這樣對于 Generator 的使用,以及最終函數的執行進度都可以很好地控制,從而形成符合你設想的執行順序。即便 Generator 函數相互嵌套,也能通過調用 next 方法來按照進度一步步執行。

(3)生成器原理

其實,在生成器內部,如果遇到 yield 關鍵字,那么 V8 引擎將返回關鍵字后面的內容給外部,并暫停該生成器函數的執行。生成器暫停執行后,外部的代碼便開始執行,外部代碼如果想要恢復生成器的執行,可以使用 result.next 方法。

那 V8 是怎么實現生成器函數的暫停執行和恢復執行的呢?

它用到的就是協程,協程是—種比線程更加輕量級的存在。我們可以把協程看成是跑在線程上的任務,一個線程上可以存在多個協程,但是在線程上同時只能執行一個協程。比如,當前執行的是 A 協程,要啟動 B 協程,那么 A 協程就需要將主線程的控制權交給 B 協程,這就體現在 A 協程暫停執行,B 協程恢復執行; 同樣,也可以從 B 協程中啟動 A 協程。通常,如果從 A 協程啟動 B 協程,我們就把 A 協程稱為 B 協程的父協程。

正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。每一時刻,該線程只能執行其中某一個協程。最重要的是,協程不是被操作系統內核所管理,而完全是由程序所控制(也就是在用戶態執行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。

2. Generator 和 thunk 結合

下面先來了解一下什么是 thunk 函數,以判斷數據類型為例:

let isString = (obj) => {
  return Object.prototype.toString.call(obj) === '[object String]';
};
let isFunction = (obj) => {
  return Object.prototype.toString.call(obj) === '[object Function]';
};
let isArray = (obj) => {
  return Object.prototype.toString.call(obj) === '[object Array]';
};
....

可以看到,這里出現了很多重復的判斷邏輯,平常在開發中類似的重復邏輯的場景也同樣會有很多。下面來進行封裝:

let isType = (type) => {
  return (obj) => {
    return Object.prototype.toString.call(obj) === `[object ${type}]`;
  }
}

封裝之后就可以這樣使用,從而來減少重復的邏輯代碼:

let isString = isType('String');
let isArray = isType('Array');
isString("123");    // true
isArray([1,2,3]);   // true

相應的 isString 和 isArray 是由 isType 方法生產出來的函數,通過上面的方式來改造代碼,明顯簡潔了不少。像 isType 這樣的函數稱為 thunk 函數,它的基本思路都是接收一定的參數,會生產出定制化的函數,最后使用定制化的函數去完成想要實現的功能。

這樣的函數在 JS 的編程過程中會遇到很多,抽象度比較高的 JS 代碼往往都會采用這樣的方式。那 Generator 和 thunk 函數的結合是否能帶來一定的便捷性呢?

下面以文件操作的代碼為例,看一下 Generator 和 thunk 的結合能夠對異步操作產生的效果:

const readFileThunk = (filename) => {
  return (callback) => {
    fs.readFile(filename, callback);
  }
}
const gen = function* () {
  const data1 = yield readFileThunk('1.txt')
  console.log(data1.toString())
  const data2 = yield readFileThunk('2.txt')
  console.log(data2.toString)
}
let g = gen();
g.next().value((err, data1) => {
  g.next(data1).value((err, data2) => {
    g.next(data2);
  })
})

readFileThunk 就是一個 thunk 函數,上面的這種編程方式就讓 Generator 和異步操作關聯起來了。上面第三段代碼執行起來嵌套的情況還算簡單,如果任務多起來,就會產生很多層的嵌套,可讀性不強,因此有必要把執行的代碼進行封裝優化:

function run(gen){
  const next = (err, data) => {
    let res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}
run(g);

可以看到, run 函數和上面的執行效果其實是一樣的。代碼雖然只有幾行,但其包含了遞歸的過程,解決了多層嵌套的問題,并且完成了異步操作的一次性的執行效果。這就是通過 thunk 函數完成異步操作的情況。

3. Generator 和 Promise 結合

其實 Promise 也可以和 Generator 配合來實現上面的效果。還是利用上面的輸出文件的例子,對代碼進行改造,如下所示:

const readFilePromise = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if(err) {
        reject(err);
      }else {
        resolve(data);
      }
    })
  }).then(res => res);
}
// 這塊和上面 thunk 的方式一樣
const gen = function* () {
  const data1 = yield readFilePromise('1.txt')
  console.log(data1.toString())
  const data2 = yield readFilePromise('2.txt')
  console.log(data2.toString)
}
// 這里和上面 thunk 的方式一樣
function run(gen){
  const next = (err, data) => {
    let res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}
run(g);

可以看到,thunk 函數的方式和通過 Promise 方式執行效果本質上是一樣的,只不過通過 Promise 的方式也可以配合 Generator 函數實現同樣的異步操作。

4. co 函數庫

co 函數庫用于處理 Generator 函數的自動執行。核心原理其實就是通過和 thunk 函數以及 Promise 對象進行配合,包裝成一個庫。它使用起來非常簡單,比如還是用上面那段代碼,第三段代碼就可以省略了,直接引用 co 函數,包裝起來就可以使用了,代碼如下:

const co = require('co');
let g = gen();
co(g).then(res =>{
  console.log(res);
})

這段代碼比較簡單,幾行就完成了之前寫的遞歸的那些操作。那么為什么 co 函數庫可以自動執行 Generator 函數,它的處理原理如下:

  1. 因為 Generator 函數就是一個異步操作的容器,它需要一種自動執行機制,co 函數接受 Generator 函數作為參數,并最后返回一個 Promise 對象。
  2. 在返回的 Promise 對象里面,co 先檢查參數 gen 是否為 Generator 函數。如果是,就執行該函數;如果不是就返回,并將 Promise 對象的狀態改為 resolved。
  3. co 將 Generator 函數的內部指針對象的 next 方法,包裝成 onFulfilled 函數。這主要是為了能夠捕捉拋出的錯誤。
  4. 關鍵的是 next 函數,它會反復調用自身。

五、Async/Await

1. async/await 的概念

ES7 新增了兩個關鍵字: async和await,代表異步JavaScript編程范式的遷移。它改進了生成器的缺點,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力。其實 async/await 是 Generator 的語法糖,它能實現的效果都能用then鏈來實現,它是為優化then鏈而開發出來的。

從字面上來看,async是“異步”的簡寫,await則為等待,所以 async 用來聲明異步函數,這個關鍵字可以用在函數聲明、函數表達式、箭頭函數和方法上。因為異步函數主要針對不會馬上完成的任務,所以自然需要一種暫停和恢復執行的能力,使用await關鍵字可以暫停異步代碼的執行,等待Promise解決。async 關鍵字可以讓函數具有異步特征,但總體上代碼仍然是同步求值的。

它們的用法很簡單,首先用 async 關鍵字聲明一個異步函數:

async function httpRequest() {
}

然后就可以在這個函數內部使用 await 關鍵字了:

async function httpRequest() {
  let res1 = await httpPromise(url1)
  console.log(res1)
}

這里,await關鍵字會接收一個期約并將其轉化為一個返回值或一個拋出的異常。通過情況下,我們不會使用await來接收一個保存期約的變量,更多的是把他放在一個會返回期約的函數調用面前,比如上述例子。這里的關鍵就是,await關鍵字并不會導致程序阻塞,代碼仍然是異步的,而await只是掩蓋了這個事實,這就意味著任何使用await的代碼本身都是異步的。

下面來看看async函數返回了什么:

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)
深入淺出JavaScript異步編程

可以看到,async 函數返回的是 Promise 對象。如果異步函數使用return關鍵字返回了值(如果沒有return則會返回undefined),這個值則會被 Promise.resolve() 包裝成 Promise 對象。異步函數始終返回Promise對象。

2. await 到底在等啥?

那await到底在等待什么呢?

一般我們認為 await 是在等待一個 async 函數完成。不過按語法說明,await 等待的是一個表達式,這個表達式的結果是 Promise 對象或其它值。

因為 async 函數返回一個 Promise 對象,所以 await 可以用于等待一個 async 函數的返回值——這也可以說是 await 在等 async 函數。但要清楚,它等的實際是一個返回值。注意,await 不僅用于等 Promise 對象,它可以等任意表達式的結果。所以,await 后面實際是可以接普通函數調用或者直接量的。所以下面這個示例完全可以正確運行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test(); // something hello async

await 表達式的運算結果取決于它等的是什么:

  • 如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的內容;
  • 如果它等到的是一個 Promise 對象,await 就就會阻塞后面的代碼,等著 Promise 對象 resolve,然后將得到的值作為 await 表達式的運算結果。

下面來看一個例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒鐘之后出現hello world
  console.log('cuger')   // 3秒鐘之后出現cug
}
testAwt();
console.log('cug')  //立即輸出cug

這就是 await 必須用在 async 函數中的原因。async 函數調用不會造成阻塞,它內部所有的阻塞都被封裝在一個 Promise 對象中異步執行。await暫停當前async的執行,所以'cug''最先輸出,hello world'和 cuger 是3秒鐘后同時出現的。

3. async/await的優勢

單一的 Promise 鏈并不能凸顯 async/await 的優勢。但是,如果處理流程比較復雜,那么整段代碼將充斥著 then,語義化不明顯,代碼不能很好地表示執行流程,這時async/await的優勢就能體現出來了。

假設一個業務,分多個步驟完成,每個步驟都是異步的,而且依賴于上一個步驟的結果。首先用 setTimeout 來模擬異步操作:

/**
 * 傳入參數 n,表示這個函數執行的時間(毫秒)
 * 執行的結果是 n + 200,這個值將用于下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

現在用 Promise 方式來實現這三個步驟的處理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:vartest>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

輸出結果 result 是 step3() 的參數 700 + 200 = 900doIt() 順序執行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一致。

如果用 async/await 來實現呢,會是這樣:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

結果和之前的 Promise 實現是一樣的,但是這個代碼看起來會清晰得多,幾乎和同步代碼一樣。

async/await對比Promise的優勢就顯而易見了:

  • 代碼讀起來更加同步,Promise雖然擺脫了回調地獄,但是then的鏈式調?也會帶來額外的理解負擔;
  • Promise傳遞中間值很麻煩,?async/await?乎是同步的寫法,?常優雅;
  • 錯誤處理友好,async/await可以?成熟的try/catch,Promise的錯誤捕獲比較冗余;
  • 調試友好,Promise的調試很差,由于沒有代碼塊,不能在?個返回表達式的箭頭函數中設置斷點,如果在?個.then代碼塊中使?調試器的步進(step-over)功能,調試器并不會進?后續的.then代碼塊,因為調試器只能跟蹤同步代碼的每?步。

4. async/await 的異常處理

利用 async/await 的語法糖,可以像處理同步代碼的異常一樣,來處理異步代碼,這里還用上面的示例:

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});
const run = async () => {
 try {
  await exe(false)();
  await exe(true)();
 } catch (e) {
  console.log(e);
 }
}
run();

這里定義一個異步方法 run,由于 await 后面需要直接跟 Promise 對象,因此通過額外的一個方法調用符號 () 把原有的 exe 方法內部的 Thunk 包裝拆掉,即執行 exe(false)() 或 exe(true)() 返回的就是 Promise 對象。在 try 塊之后,使用 catch 來捕捉。運行代碼會得到這樣的輸出:

false
no

這個 false 就是 exe 方法對入參的輸出,而這個 no 就是 setTimeout 方法 reject 的回調返回,它通過異常捕獲并最終在 catch 塊中輸出。就像我們所認識的同步代碼一樣,第四行的 exe(true) 并未得到執行。

分享到:
標簽:JavaScript
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定