精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

你不知道的前端異常處理(萬字長(zhǎng)文,建議收藏)

開發(fā) 前端
我們天天和各種異常打交道,就好像我們天天和 Bug 打交道一樣。因此正確認(rèn)識(shí)異常,并作出合適的異常處理就顯得很重要了。

[[330955]]

除了調(diào)試,處理異常或許是程序員編程時(shí)間占比最高的了。我們天天和各種異常打交道,就好像我們天天和 Bug 打交道一樣。因此正確認(rèn)識(shí)異常,并作出合適的異常處理就顯得很重要了。

我們先嘗試拋開前端這個(gè)限定條件,來看下更廣泛意義上程序的報(bào)錯(cuò)以及異常處理。不管是什么語言,都會(huì)有異常的發(fā)生。而我們程序員要做的就是正確識(shí)別程序中的各種異常,并針對(duì)其做相應(yīng)的異常處理。

然而,很多人對(duì)異常的處理方式是事后修補(bǔ),即某個(gè)異常發(fā)生的時(shí)候,增加對(duì)應(yīng)的條件判斷,這真的是一種非常低效的開發(fā)方式,非常不推薦大家這么做。那么究竟如何正確處理異常呢?由于不同語言有不同的特性,因此異常處理方式也不盡相同。但是異常處理的思維框架一定是一致的。本文就前端異常進(jìn)行詳細(xì)闡述,但是讀者也可以稍加修改延伸到其他各個(gè)領(lǐng)域。

本文討論的異常指的是軟件異常,而非硬件異常。

什么是異常

用直白的話來解釋異常的話,就是程序發(fā)生了意想不到的情況,這種情況影響到了程序的正確運(yùn)行。

從根本上來說,異常就是一個(gè)數(shù)據(jù)結(jié)構(gòu),其保存了異常發(fā)生的相關(guān)信息,比如錯(cuò)誤碼,錯(cuò)誤信息等。以 JS 中的標(biāo)準(zhǔn)內(nèi)置對(duì)象 Error 為例,其標(biāo)準(zhǔn)屬性有 name 和 message。然而不同的瀏覽器廠商有自己的自定義屬性,這些屬性并不通用。比如 Mozilla 瀏覽器就增加了 filename 和 stack 等屬性。

值得注意的是錯(cuò)誤只有被拋出,才會(huì)產(chǎn)生異常,不被拋出的錯(cuò)誤不會(huì)產(chǎn)生異常。比如: 

  1. function t() {  
  2.   console.log("start");  
  3.   new Error();  
  4.   console.log("end");  
  5.  
  6. t(); 

(動(dòng)畫演示)

這段代碼不會(huì)產(chǎn)生任何的異常,控制臺(tái)也不會(huì)有任何錯(cuò)誤輸出。

異常的分類

按照產(chǎn)生異常時(shí)程序是否正在運(yùn)行,我們可以將錯(cuò)誤分為編譯時(shí)異常和運(yùn)行時(shí)異常。

編譯時(shí)異常指的是源代碼在編譯成可執(zhí)行代碼之前產(chǎn)生的異常。而運(yùn)行時(shí)異常指的是可執(zhí)行代碼被裝載到內(nèi)存中執(zhí)行之后產(chǎn)生的異常。

編譯時(shí)異常

我們知道 TS 最終會(huì)被編譯成 JS,從而在 JS Runtime中執(zhí)行。既然存在編譯,就有可能編譯失敗,就會(huì)有編譯時(shí)異常。

比如我使用 TS 寫出了如下代碼: 

  1. const s: string = 123

這很明顯是錯(cuò)誤的代碼, 我給 s 聲明了 string 類型,但是卻給它賦值 number。

當(dāng)我使用 tsc(typescript 編譯工具,全稱是 typescript compiler)嘗試編譯這個(gè)文件的時(shí)候會(huì)有異常拋出: 

  1. tsc a.ts  
  2. a.ts:1:7 - error TS2322: Type '123' is not assignable to type 'string'.  
  3. 1 const s: string = 123 
  4.         ~  
  5. Found 1 error. 

這個(gè)異常就是編譯時(shí)異常,因?yàn)槲业拇a還沒有執(zhí)行。

然而并不是你用了 TS 才存在編譯時(shí)異常,JS 同樣有編譯時(shí)異常。有的人可能會(huì)問 JS 不是解釋性語言么?是邊解釋邊執(zhí)行,沒有編譯環(huán)節(jié),怎么會(huì)有編譯時(shí)異常?

別急,我舉個(gè)例子你就明白了。如下代碼: 

  1. function t() {  
  2.   console.log('start')  
  3.   await sa  
  4.   console.log('end')  
  5.  
  6. t() 

上面的代碼由于存在語法錯(cuò)誤,不會(huì)編譯通過,因此并不會(huì)打印start,側(cè)面證明了這是一個(gè)編譯時(shí)異常。盡管 JS 是解釋語言,也依然存在編譯階段,這是必然的,因此自然也會(huì)有編譯異常。

總的來說,編譯異常可以在代碼被編譯成最終代碼前被發(fā)現(xiàn),因此對(duì)我們的傷害更小。接下來,看一下令人心生畏懼的運(yùn)行時(shí)異常。

運(yùn)行時(shí)異常

相信大家對(duì)運(yùn)行時(shí)異常非常熟悉。這恐怕是廣大前端碰到最多的異常類型了。眾所周知的 NPE(Null Pointer Exception) 就是運(yùn)行時(shí)異常。

將上面的例子稍加改造,得到下面代碼: 

  1. function t() {  
  2.   console.log("start");  
  3.   throw 1;  
  4.   console.log("end");  
  5.  
  6. t(); 

(動(dòng)畫演示)

注意 end 沒有打印,并且 t 沒有彈出棧。實(shí)際上 t 最終還是會(huì)被彈出的,只不過和普通的返回不一樣。

如上,則會(huì)打印出start。由于異常是在代碼運(yùn)行過程中拋出的,因此這個(gè)異常屬于運(yùn)行時(shí)異常。相對(duì)于編譯時(shí)異常,這種異常更加難以發(fā)現(xiàn)。上面的例子可能比較簡(jiǎn)單,但是如果我的異常是隱藏在某一個(gè)流程控制語句(比如 if else)里面呢?程序就可能在客戶的電腦走入那個(gè)拋出異常的 if 語句,而在你的電腦走入另一條。這就是著名的 《在我電腦上好好的》 事件。

異常的傳播

異常的傳播和我之前寫的瀏覽器事件模型有很大的相似性。只不過那個(gè)是作用在 DOM 這樣的數(shù)據(jù)結(jié)構(gòu),這個(gè)則是作用在函數(shù)調(diào)用棧這種數(shù)據(jù)結(jié)構(gòu),并且事件傳播存在捕獲階段,異常傳播是沒有的。不同 C 語言,JS 中異常傳播是自動(dòng)的,不需要程序員手動(dòng)地一層層傳遞。如果一個(gè)異常沒有被 catch,它會(huì)沿著函數(shù)調(diào)用棧一層層傳播直到???。

異常處理中有兩個(gè)關(guān)鍵詞,它們是throw(拋出異常) 和 catch(處理異常)。 當(dāng)一個(gè)異常被拋出的時(shí)候,異常的傳播就開始了。異常會(huì)不斷傳播直到遇到第一個(gè) catch。 如果程序員沒有手動(dòng) catch,那么一般而言程序會(huì)拋出類似unCaughtError,表示發(fā)生了一個(gè)異常,并且這個(gè)異常沒有被程序中的任何 catch 語言處理。未被捕獲的異常通常會(huì)被打印在控制臺(tái)上,里面有詳細(xì)的堆棧信息,從而幫助程序員快速排查問題。實(shí)際上我們的程序的目標(biāo)是避免 unCaughtError這種異常,而不是一般性的異常。

一點(diǎn)小前提

由于 JS 的 Error 對(duì)象沒有 code 屬性,只能根據(jù) message 來呈現(xiàn),不是很方便。我這里進(jìn)行了簡(jiǎn)單的擴(kuò)展,后面很多地方我用的都是自己擴(kuò)展的 Error ,而不是原生 JS Error ,不再贅述。 

  1. oldError = Error;  
  2. Error = function ({ code, message, fileName, lineNumber }) {  
  3.   error = new oldError(message, fileName, lineNumber);  
  4.   error.code = code;  
  5.   return error;  
  6. }; 

手動(dòng)拋出 or 自動(dòng)拋出

異常既可以由程序員自己手動(dòng)拋出,也可以由程序自動(dòng)拋出。 

  1. throw new Error(`I'm Exception`); 

(手動(dòng)拋出的例子) 

  1. a = null 
  2. a.toString(); // Thrown: TypeError: Cannot read property 'toString' of null 

(程序自動(dòng)拋出的例子)

自動(dòng)拋出異常很好理解,畢竟我們哪個(gè)程序員沒有看到過程序自動(dòng)拋出的異常呢?

“這個(gè)異常突然就跳出來!嚇我一跳!”,某不知名程序員如是說。

那什么時(shí)候應(yīng)該手動(dòng)拋出異常呢?

一個(gè)指導(dǎo)原則就是你已經(jīng)預(yù)知到程序不能正確進(jìn)行下去了。比如我們要實(shí)現(xiàn)除法,首先我們要考慮的是被除數(shù)為 0 的情況。當(dāng)被除數(shù)為 0 的時(shí)候,我們應(yīng)該怎么辦呢?是拋出異常,還是 return 一個(gè)特殊值?答案是都可以,你自己能區(qū)分就行,這沒有一個(gè)嚴(yán)格的參考標(biāo)準(zhǔn)。 我們先來看下拋出異常,告訴調(diào)用者你的輸入,我處理不了這種情況。 

  1. function divide(a, b) {  
  2.   a = +a;  
  3.   b = +b; // 轉(zhuǎn)化成數(shù)字  
  4.   if (!b) {  
  5.     // 匹配 +0, -0, NaN  
  6.     throw new Error({  
  7.       code: 1,  
  8.       message: "Invalid dividend " + b,  
  9.     });  
  10.   }  
  11.   if (Number.isNaN(a)) {  
  12.     // 匹配 NaN  
  13.     throw new Error({  
  14.       code: 2,  
  15.       message: "Invalid divisor " + a,  
  16.     });  
  17.   }  
  18.   return a / b;  

上面代碼會(huì)在兩種情況下拋出異常,告訴調(diào)用者你的輸入我處理不了。由于這兩個(gè)異常都是程序員自動(dòng)手動(dòng)拋出的,因此是可預(yù)知的異常。

剛才說了,我們也可以通過返回值來區(qū)分異常輸入。我們來看下返回值輸入是什么,以及和異常有什么關(guān)系。

異常 or 返回

如果是基于異常形式(遇到不能處理的輸入就拋出異常)。當(dāng)別的代碼調(diào)用divide的時(shí)候,需要自己 catch。 

  1. function t() {  
  2.   try {  
  3.     divide("foo", "bar");  
  4.   } catch (err) {  
  5.     if (err.code === 1) {  
  6.       return console.log("被除數(shù)必須是除0之外的數(shù)");  
  7.     }  
  8.     if (err.code === 2) {  
  9.       return console.log("除數(shù)必須是數(shù)字");  
  10.     }  
  11.     throw new Error("不可預(yù)知的錯(cuò)誤");  
  12.   }  

然而就像上面我說的那樣,divide 函數(shù)設(shè)計(jì)的時(shí)候,也完全可以不用異常,而是使用返回值來區(qū)分。 

  1. function divide(a, b) {  
  2.   a = +a;  
  3.   b = +b; // 轉(zhuǎn)化成數(shù)字  
  4.   if (!b) {  
  5.     // 匹配 +0, -0, NaN  
  6.     return new Error({  
  7.       code: 1,  
  8.       message: "Invalid dividend " + b,  
  9.     });  
  10.   }  
  11.   if (Number.isNaN(a)) {  
  12.     // 匹配 NaN  
  13.     return new Error({  
  14.       code: 2,  
  15.       message: "Invalid divisor " + a,  
  16.     });  
  17.   }  
  18.   return a / b;  

當(dāng)然,我們使用方式也要作出相應(yīng)改變。 

  1. function t() {  
  2.   const res = divide("foo", "bar");  
  3.   if (res.code === 1) {  
  4.     return console.log("被除數(shù)必須是除0之外的數(shù)");  
  5.   } 
  6.   if (res.code === 2) {  
  7.     return console.log("除數(shù)必須是數(shù)字");  
  8.   }  
  9.   return new Error("不可預(yù)知的錯(cuò)誤");  

這種函數(shù)設(shè)計(jì)方式和拋出異常的設(shè)計(jì)方式從功能上說都是一樣的,只是告訴調(diào)用方的方式不同。如果你選擇第二種方式,而不是拋出異常,那么實(shí)際上需要調(diào)用方書寫額外的代碼,用來區(qū)分正常情況和異常情況,這并不是一種良好的編程習(xí)慣。

然而在 Go 等返回值可以為復(fù)數(shù)的語言中,我們無需使用上面蹩腳的方式,而是可以: 

  1. res, err :divide("foo", "bar");  
  2. if err != nil {  
  3.     log.Fatal(err)  

這是和 Java 和 JS 等語言使用的 try catch 不一樣的的地方,Go 是通過 panic recover defer 機(jī)制來進(jìn)行異常處理的。感興趣的可以去看看 Go 源碼關(guān)于錯(cuò)誤測(cè)試部分

可能大家對(duì) Go 不太熟悉。沒關(guān)系,我們來繼續(xù)看下 shell。實(shí)際上 shell 也是通過返回值來處理異常的,我們可以通過 $? 拿到上一個(gè)命令的返回值,這本質(zhì)上也是一種調(diào)用棧的傳播行為,而且是通過返回值而不是捕獲來處理異常的。

作為函數(shù)返回值處理和 try catch 一樣,這是語言的設(shè)計(jì)者和開發(fā)者共同決定的一件事情。

上面提到了異常傳播是作用在函數(shù)調(diào)用棧上的。當(dāng)一個(gè)異常發(fā)生的時(shí)候,其會(huì)沿著函數(shù)調(diào)用棧逐層返回,直到第一個(gè) catch 語句。當(dāng)然 catch 語句內(nèi)部仍然可以觸發(fā)異常(自動(dòng)或者手動(dòng))。如果 catch 語句內(nèi)部發(fā)生了異常,也一樣會(huì)沿著其函數(shù)調(diào)用棧繼續(xù)執(zhí)行上述邏輯,專業(yè)術(shù)語是 stack unwinding。

實(shí)際上并不是所有的語言都會(huì)進(jìn)行 stack unwinding,這個(gè)我們會(huì)在接下來的《運(yùn)行時(shí)異??梢曰謴?fù)么?》部分講解。

偽代碼來描述一下: 

  1. function bubble(error, fn) {  
  2.   if (fn.hasCatchBlock()) { 
  3.      runCatchCode(error);  
  4.   }  
  5.   if (callstack.isNotEmpty()) {  
  6.     bubble(error, callstack.pop());  
  7.   }  

從我的偽代碼可以看出所謂的 stack unwinding 其實(shí)就是 callstack.pop()

這就是異常傳播的一切!僅此而已。

異常的處理

我們已經(jīng)了解來異常的傳播方式了。那么接下來的問題是,我們應(yīng)該如何在這個(gè)傳播過程中處理異常呢?

我們來看一個(gè)簡(jiǎn)單的例子: 

  1. function a() {  
  2.   b();  
  3.  
  4. function b() {  
  5.   c();  
  6.  
  7. function c() {  
  8.   throw new Error("an error  occured");  
  9.  
  10. a(); 

我們將上面的代碼放到 chrome 中執(zhí)行, 會(huì)在控制臺(tái)顯示如下輸出:

我們可以清楚地看出函數(shù)的調(diào)用關(guān)系。即錯(cuò)誤是在 c 中發(fā)生的,而 c 是 b 調(diào)用的,b 是 a 調(diào)用的。這個(gè)函數(shù)調(diào)用棧是為了方便開發(fā)者定位問題而存在的。

上面的代碼,我們并沒有 catch 錯(cuò)誤,因此上面才會(huì)有uncaught Error。

那么如果我們 catch ,會(huì)發(fā)生什么樣的變化呢?catch 的位置會(huì)對(duì)結(jié)果產(chǎn)生什么樣的影響?在 a ,b,c 中 catch 的效果是一樣的么?

我們來分別看下: 

  1. function a() {  
  2.   b();  
  3.  
  4. function b() {  
  5.   c();  
  6.  
  7. function c() {  
  8.   try {  
  9.     throw new Error("an error  occured");  
  10.   } catch (err) {  
  11.     console.log(err);  
  12.   }  
  13.  
  14. a(); 

(在 c 中 catch)

我們將上面的代碼放到 chrome 中執(zhí)行, 會(huì)在控制臺(tái)顯示如下輸出:

可以看出,此時(shí)已經(jīng)沒有uncaught Error啦,僅僅在控制臺(tái)顯示了標(biāo)準(zhǔn)輸出,而非錯(cuò)誤輸出(因?yàn)槲矣玫氖?console.log,而不是 console.error)。然而更重要是的是,如果我們沒有 catch,那么后面的同步代碼將不會(huì)執(zhí)行。

比如在 c 的 throw 下面增加一行代碼,這行代碼是無法被執(zhí)行的,無論這個(gè)錯(cuò)誤有沒有被捕獲。 

  1. function c() {  
  2.   try { 
  3.      throw new Error("an error  occured");  
  4.     console.log("will never run");  
  5.   } catch (err) {  
  6.     console.log(err);  
  7.   }  

我們將 catch 移動(dòng)到 b 中試試看。 

  1. function a() {  
  2.   b();  
  3.  
  4. function b() {  
  5.   try {  
  6.     c();  
  7.   } catch (err) {  
  8.     console.log(err);  
  9.   }  
  10.  
  11. function c() {  
  12.   throw new Error("an error  occured");  
  13.  
  14. a(); 

(在 b 中 catch)

在這個(gè)例子中,和上面在 c 中捕獲沒有什么本質(zhì)不同。其實(shí)放到 a 中捕獲也是一樣,這里不再貼代碼了,感興趣的自己試下。

既然處于函數(shù)調(diào)用棧頂部的函數(shù)報(bào)錯(cuò), 其函數(shù)調(diào)用棧下方的任意函數(shù)都可以進(jìn)行捕獲,并且效果沒有本質(zhì)不同。那么問題來了,我到底應(yīng)該在哪里進(jìn)行錯(cuò)誤處理呢?

答案是責(zé)任鏈模式。我們先來簡(jiǎn)單介紹一下責(zé)任鏈模式,不過細(xì)節(jié)不會(huì)在這里展開。

假如 lucifer 要請(qǐng)假。

  •  如果請(qǐng)假天數(shù)小于等于 1 天,則主管同意即可
  •  如果請(qǐng)假大于 1 天,但是小于等于三天,則需要 CTO 同意。
  •  如果請(qǐng)假天數(shù)大于三天,則需要老板同意。

這就是一個(gè)典型的責(zé)任鏈模式。誰有責(zé)任干什么事情是確定的,不要做自己能力范圍之外的事情。比如主管不要去同意大于 1 天的審批。

舉個(gè)例子,假設(shè)我們的應(yīng)用有三個(gè)異常處理類,它們分別是:用戶輸入錯(cuò)誤,網(wǎng)絡(luò)錯(cuò)誤 和 類型錯(cuò)誤。如下代碼,當(dāng)代碼執(zhí)行的時(shí)候會(huì)報(bào)錯(cuò)一個(gè)用戶輸入異常。這個(gè)異常沒有被 C 捕獲,會(huì) unwind stack 到 b,而 b 中 catch 到這個(gè)錯(cuò)誤之后,通過查看 code 值判斷其可以被處理,于是打印I can handle this。 

  1. function a() {  
  2.   try {  
  3.     b();  
  4.   } catch (err) {  
  5.     if (err.code === "NETWORK_ERROR") {  
  6.       return console.log("I can handle this");  
  7.     }  
  8.     // can't handle, pass it down  
  9.     throw err;  
  10.   }  
  11.  
  12. function b() {  
  13.   try {  
  14.     c();  
  15.   } catch (err) {  
  16.     if (err.code === "INPUT_ERROR") {  
  17.       return console.log("I can handle this");  
  18.     }  
  19.     // can't handle, pass it down  
  20.     throw err;  
  21.   }  
  22.  
  23. function c() {  
  24.   throw new Error({  
  25.     code: "INPUT_ERROR",  
  26.     message: "an error  occured",  
  27.   });  
  28.  
  29. a(); 

而如果 c 中拋出的是別的異常,比如網(wǎng)絡(luò)異常,那么 b 是無法處理的,雖然 b catch 住了,但是由于你無法處理,因此一個(gè)好的做法是繼續(xù)拋出異常,而不是吞沒異常。不要畏懼錯(cuò)誤,拋出它。只有沒有被捕獲的異常才是可怕的,如果一個(gè)錯(cuò)誤可以被捕獲并得到正確處理,它就不可怕。

舉個(gè)例子: 

  1. function a() {  
  2.   try {  
  3.     b();  
  4.   } catch (err) {  
  5.     if (err.code === "NETWORK_ERROR") {  
  6.       return console.log("I can handle this");  
  7.     }  
  8.     // can't handle, pass it down  
  9.     throw err;  
  10.   }  
  11.  
  12. function b() {  
  13.   try {  
  14.     c();  
  15.   } catch (err) {  
  16.     if (err.code === "INPUT_ERROR") {  
  17.       return console.log("I can handle this");  
  18.     }  
  19.   }  
  20.  
  21. function c() {  
  22.   throw new Error({  
  23.     code: "NETWORK_ERROR",  
  24.     message: "an error  occured",  
  25.   });  
  26.  
  27. a(); 

如上代碼不會(huì)有任何異常被拋出,它被完全吞沒了,這對(duì)我們調(diào)試問題簡(jiǎn)直是災(zāi)難。因此切記不要吞沒你不能處理的異常。正確的做法應(yīng)該是上面講的那種只 catch 你可以處理的異常,而將你不能處理的異常 throw 出來,這就是責(zé)任鏈模式的典型應(yīng)用。

這只是一個(gè)簡(jiǎn)單的例子,就足以繞半天。實(shí)際業(yè)務(wù)肯定比這個(gè)復(fù)雜多得多。因此異常處理絕對(duì)不是一件容易的事情。

如果說誰來處理是一件困難的事情,那么在異步中決定誰來處理異常就是難上加難,我們來看下。

同步與異步

同步異步一直是前端難以跨越的坎,對(duì)于異常處理也是一樣。以 NodeJS 中用的比較多的讀取文件 API 為例。它有兩個(gè)版本,一個(gè)是異步,一個(gè)是同步。同步讀取僅僅應(yīng)該被用在沒了這個(gè)文件無法進(jìn)行下去的時(shí)候。比如讀取一個(gè)配置文件。而不應(yīng)該在比如瀏覽器中讀取用戶磁盤上的一個(gè)圖片等,這樣會(huì)造成主線程阻塞,導(dǎo)致瀏覽器卡死。 

  1. // 異步讀取文件  
  2. fs.readFileSync();  
  3. // 同步讀取文件  
  4. fs.readFile(); 

當(dāng)我們?cè)噲D同步讀取一個(gè)不存在的文件的時(shí)候,會(huì)拋出以下異常: 

  1. fs.readFileSync('something-not-exist.lucifer');  
  2. console.log('腦洞前端');  
  3. Thrown:  
  4. Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'  
  5.     at Object.openSync (fs.js:446:3)  
  6.     at Object.readFileSync (fs.js:348:35) {  
  7.   errno: -2,  
  8.   syscall: 'open',  
  9.   code: 'ENOENT',  
  10.   path: 'something-not-exist.lucifer'  

并且腦洞前端是不會(huì)被打印出來的。這個(gè)比較好理解,我們上面已經(jīng)解釋過了。

而如果以異步方式的話: 

  1. fs.readFile('something-not-exist.lucifer', (err, data) => {if(err) {throw err}});  
  2. console.log('lucifer')  
  3. lucifer  
  4. undefined  
  5. Thrown:  
  6. [Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'] {  
  7.   errno: -2,  
  8.   code: 'ENOENT',  
  9.   syscall: 'open',  
  10.   path: 'something-not-exist.lucifer'  
  11.  
  12. > 

腦洞前端是會(huì)被打印出來的。

其本質(zhì)在于 fs.readFile 的函數(shù)調(diào)用已經(jīng)成功,并從調(diào)用棧返回并執(zhí)行到下一行的console.log('lucifer')。因此錯(cuò)誤發(fā)生的時(shí)候,調(diào)用棧是空的,這一點(diǎn)可以從上面的錯(cuò)誤堆棧信息中看出來。

不明白為什么調(diào)用棧是空的同學(xué)可以看下我之前寫的《一文看懂瀏覽器事件循環(huán)》 

而 try catch 的作用僅僅是捕獲當(dāng)前調(diào)用棧的錯(cuò)誤(上面異常傳播部分已經(jīng)講過了)。因此異步的錯(cuò)誤是無法捕獲的,比如; 

  1. try {  
  2.   fs.readFile("something-not-exist.lucifer", (err, data) => {  
  3.     if (err) {  
  4.       throw err;  
  5.     }  
  6.   });  
  7. } catch (err) {  
  8.   console.log("catching an error");  

上面的 catching an error 不會(huì)被打印。因?yàn)殄e(cuò)誤拋出的時(shí)候, 調(diào)用棧中不包含這個(gè) catch 語句,而僅僅在執(zhí)行fs.readFile的時(shí)候才會(huì)。

如果我們換成同步讀取文件的例子看看: 

  1. try {  
  2.   fs.readFileSync("something-not-exist.lucifer");  
  3. } catch (err) { 
  4.   console.log("catching an error");  

上面的代碼會(huì)打印 catching an error。因?yàn)樽x取文件被同步發(fā)起,文件返回之前線程會(huì)被掛起,當(dāng)線程恢復(fù)執(zhí)行的時(shí)候, fs.readFileSync 仍然在函數(shù)調(diào)用棧中,因此 fs.readFileSync 產(chǎn)生的異常會(huì)冒泡到 catch 語句。

簡(jiǎn)單來說就是異步產(chǎn)生的錯(cuò)誤不能用 try catch 捕獲,而要使用回調(diào)捕獲。

可能有人會(huì)問了,我見過用 try catch 捕獲異步異常啊。 比如: 

  1. rejectIn = (ms) =>  
  2.   new Promise((_, r) => {  
  3.     setTimeout(() => {  
  4.       r(1);  
  5.     }, ms);  
  6.   });  
  7. async function t() {  
  8.   try {  
  9.     await rejectIn(0);  
  10.   } catch (err) {  
  11.     console.log("catching an error", err);  
  12.   }  
  13.  
  14. t(); 

本質(zhì)上這只是一個(gè)語法糖,是 Promise.prototype.catch 的一個(gè)語法糖而已。而這一語法糖能夠成立的原因在于其用了 Promise 這種包裝類型。如果你不用包裝類型,比如上面的 fs.readFile 不用 Promise 等包裝類型包裝,打死都不能用 try catch 捕獲。

而如果我們使用 babel 轉(zhuǎn)義下,會(huì)發(fā)現(xiàn) try catch 不見了,變成了 switch case 語句。這就是 try catch “可以捕獲異步異常”的原因,僅此而已,沒有更多。

(babel 轉(zhuǎn)義結(jié)果)

我使用的 babel 轉(zhuǎn)義環(huán)境都記錄在這里,大家可以直接點(diǎn)開鏈接查看.

雖然瀏覽器并不像 babel 轉(zhuǎn)義這般實(shí)現(xiàn),但是至少我們明白了一點(diǎn)。目前的 try catch 的作用機(jī)制是無法捕獲異步異常的。

異步的錯(cuò)誤處理推薦使用容器包裝,比如 Promise。然后使用 catch 進(jìn)行處理。實(shí)際上 Promise 的 catch 和 try catch 的 catch 有很多相似的地方,大家可以類比過去。

和同步處理一樣,很多原則都是通用的。比如異步也不要去吞沒異常。下面的代碼是不好的,因?yàn)樗虥]了它不能處理的異常。 

  1. p = Promise.reject(1);  
  2. p.catch(() => {}); 

更合適的做法的應(yīng)該是類似這種: 

  1. p = Promise.reject(1);  
  2. p.catch((err) => {  
  3.   if (err == 1) {  
  4.     return console.log("I can handle this");  
  5.   }  
  6.   throw err;  
  7. }); 

徹底消除運(yùn)行時(shí)異??赡苊??

我個(gè)人對(duì)目前前端現(xiàn)狀最為頭疼的一點(diǎn)是:大家過分依賴運(yùn)行時(shí),而嚴(yán)重忽略編譯時(shí)。我見過很多程序,你如果不運(yùn)行,根本不知道程序是怎么走的,每個(gè)變量的 shape 是什么。怪不得處處都可以看到 console.log。我相信你一定對(duì)此感同身受。也許你就是那個(gè)寫出這種代碼的人,也許你是給別人擦屁股的人。為什么會(huì)這樣? 就是因?yàn)榇蠹姨蕾囘\(yùn)行時(shí)。TS 的出現(xiàn)很大程度上改善了這一點(diǎn),前提是你用的是 typescript,而不是 anyscript。其實(shí) eslint 以及 stylint 對(duì)此也有貢獻(xiàn),畢竟它們都是靜態(tài)分析工具。

我強(qiáng)烈建議將異常保留在編譯時(shí),而不是運(yùn)行時(shí)。不妨極端一點(diǎn)來看:假如所有的異常都在編譯時(shí)發(fā)生,而一定不會(huì)在運(yùn)行時(shí)發(fā)生。那么我們是不是就可以信心滿滿地對(duì)應(yīng)用進(jìn)行重構(gòu)啦?

幸運(yùn)的是,我們能夠做到。只不過如果當(dāng)前語言做不到的話,則需要對(duì)現(xiàn)有的語言體系進(jìn)行改造。這種改造成本真的很大。不僅僅是 API,編程模型也發(fā)生了翻天覆地的變化,不然函數(shù)式也不會(huì)這么多年沒有得到普及了。

不熟悉函數(shù)編程的可以看看我之前寫的函數(shù)式編程入門篇

如果才能徹底消除異常呢?在回答這個(gè)問題之前,我們先來看下一門號(hào)稱沒有運(yùn)行時(shí)異常的語言 elm。elm 是一門可以編譯為 JS 的函數(shù)式編程語言,其封裝了諸如網(wǎng)絡(luò) IO 等副作用,是一種聲明式可推導(dǎo)的語言。 有趣的是,elm 也有異常處理。 elm 中關(guān)于異常處理(Error Handling)部分有兩個(gè)小節(jié)的內(nèi)容,分別是:Maybe 和 Result。elm 之所以沒有運(yùn)行時(shí)異常的一個(gè)原因就是它們。 一句話概括“為什么 elm 沒有異常”的話,那就是elm 把異常看作數(shù)據(jù)(data)。

舉個(gè)簡(jiǎn)單的例子: 

  1. maybeResolveOrNot = (ms) =>  
  2.   setTimeout(() => {  
  3.     if (Math.random() > 0.5) {  
  4.       console.log("ok");  
  5.     } else {  
  6.       throw new Error("error");  
  7.     }  
  8.   }); 

上面的代碼有一半的可能報(bào)錯(cuò)。那么在 elm 中就不允許這樣的情況發(fā)生。所有的可能發(fā)生異常的代碼都會(huì)被強(qiáng)制包裝一層容器,這個(gè)容器在這里是 Maybe。

在其他函數(shù)式編程語言名字可能有所不同,但是意義相同。實(shí)際上,不僅僅是異常,正常的數(shù)據(jù)也會(huì)被包裝到容器中,你需要通過容器的接口來獲取數(shù)據(jù)。如果難以理解的話,你可以將其簡(jiǎn)單理解為 Promsie(但并不完全等價(jià))。

Maybe 可能返回正常的數(shù)據(jù) data,也可能會(huì)生成一個(gè)錯(cuò)誤 error。某一個(gè)時(shí)刻只能是其中一個(gè),并且只有運(yùn)行的時(shí)候,我們才真正知道它是什么。從這一點(diǎn)來看,有點(diǎn)像薛定諤的貓。

不過 Maybe 已經(jīng)完全考慮到異常的存在,一切都在它的掌握之中。所有的異常都能夠在編譯時(shí)推導(dǎo)出來。當(dāng)然要想推導(dǎo)出這些東西,你需要對(duì)整個(gè)編程模型做一定的封裝會(huì)抽象,比如 DOM 就不能直接用了,而是需要一個(gè)中間層。

再來看下一個(gè)更普遍的例子 NPE: 

  1. null.toString(); 

elm 也不會(huì)發(fā)生。原因也很簡(jiǎn)單,因?yàn)?null 也會(huì)被包裝起來,當(dāng)你通過這個(gè)包裝類型就行訪問的時(shí)候,容器有能力避免這種情況,因此就可以不會(huì)發(fā)生異常。當(dāng)然這里有一個(gè)很重要的前提就是可推導(dǎo),而這正是函數(shù)式編程語言的特性。這部分內(nèi)容超出了本文的討論范圍,不再這里說了。

運(yùn)行時(shí)異??梢曰謴?fù)么?

最后要討論的一個(gè)主題是運(yùn)行時(shí)異常是否可以恢復(fù)。先來解釋一下,什么是運(yùn)行時(shí)異常的恢復(fù)。 還是用上面的例子: 

  1. function t() {  
  2.   console.log("start");  
  3.   throw 1;  
  4.   console.log("end"); 
  5.   
  6. t(); 

這個(gè)我們已經(jīng)知道了, end 是不會(huì)打印的。 盡管你這么寫也是無濟(jì)于事: 

  1. function t() {  
  2.   try {  
  3.     console.log("start");  
  4.     throw 1;  
  5.     console.log("end");  
  6.   } catch (err) {  
  7.     console.log("relax, I can handle this");  
  8.   }  
  9.  
  10. t(); 

如果我想讓它打印呢?我想讓程序面對(duì)異??梢宰约?recover 怎么辦?我已經(jīng)捕獲這個(gè)錯(cuò)誤, 并且我確信我可以處理,讓流程繼續(xù)走下去吧!如果有能力做到這個(gè),這個(gè)就是運(yùn)行時(shí)異常恢復(fù)。

遺憾地告訴你,據(jù)我所知,目前沒有任何一個(gè)引擎能夠做到這一點(diǎn)。

這個(gè)例子過于簡(jiǎn)單, 只能幫助我們理解什么是運(yùn)行時(shí)異?;謴?fù),但是不足以讓我們看出這有什么用?

[[330962]]

我們來看一個(gè)更加復(fù)雜的例子,我們這里直接使用上面實(shí)現(xiàn)過的函數(shù)divide。 

  1. function t() {  
  2.   try { 
  3.      const res = divide("foo", "bar");  
  4.     alert(`you got ${res}`);  
  5.   } catch (err) {  
  6.     if (err.code === 1) {  
  7.       return console.log("被除數(shù)必須是除0之外的數(shù)");  
  8.     }  
  9.     if (err.code === 2) {  
  10.       return console.log("除數(shù)必須是數(shù)字");  
  11.     }  
  12.     throw new Error("不可預(yù)知的錯(cuò)誤");  
  13.   }  

如上代碼,會(huì)進(jìn)入 catch ,而不會(huì) alert。因此對(duì)于用戶來說, 應(yīng)用程序是沒有任何響應(yīng)的。這是不可接受的。

要吐槽一點(diǎn)的是這種事情真的是挺常見的,只不過大家用的不是 alert 罷了。

如果我們的代碼在進(jìn)入 catch 之后還能夠繼續(xù)返回出錯(cuò)位置繼續(xù)執(zhí)行就好了。

如何實(shí)現(xiàn)異常中斷的恢復(fù)呢?我剛剛說了:據(jù)我所知,目前沒有任何一個(gè)引擎能夠做到異?;謴?fù)。那么我就來發(fā)明一個(gè)新的語法解決這個(gè)問題。 

  1. function t() {  
  2.   try {  
  3.     const res = divide("foo", "bar");  
  4.     alert(`you got ${res}`);  
  5.   } catch (err) {  
  6.     console.log("releax, I can handle this");  
  7.     resume - 1;  
  8.   }  
  9.  
  10. t(); 

上面的 resume 是我定義的一個(gè)關(guān)鍵字,功能是如果遇到異常,則返回到異常發(fā)生的地方,然后給當(dāng)前發(fā)生異常的函數(shù)一個(gè)返回值 -1,并使得后續(xù)代碼能夠正常運(yùn)行,不受影響。這其實(shí)是一種 fallback。

這絕對(duì)是一個(gè)超前的理念。當(dāng)然挑戰(zhàn)也非常大,對(duì)現(xiàn)有的體系沖擊很大,很多東西都要改。我希望社區(qū)可以考慮把這個(gè)東西加到標(biāo)準(zhǔn)。

最佳實(shí)踐

通過前面的學(xué)習(xí),你已經(jīng)知道了異常是什么,異常是怎么產(chǎn)生的,以及如何正確處理異常(同步和異步)。接下來,我們談一下異常處理的最佳實(shí)踐。

我們平時(shí)開發(fā)一個(gè)應(yīng)用。 如果站在生產(chǎn)者和消費(fèi)者的角度來看的話。當(dāng)我們使用別人封裝的框架,庫,模塊,甚至是函數(shù)的時(shí)候,我們就是消費(fèi)者。而當(dāng)我們寫的東西被他人使用的時(shí)候,我們就是生產(chǎn)者。

實(shí)際上,就算是生產(chǎn)者內(nèi)部也會(huì)有多個(gè)模塊構(gòu)成,多個(gè)模塊之間也會(huì)有生產(chǎn)者和消費(fèi)者的再次身份轉(zhuǎn)化。不過為了簡(jiǎn)單起見,本文不考慮這種關(guān)系。這里的生產(chǎn)者指的就是給他人使用的功能,是純粹的生產(chǎn)者。

從這個(gè)角度出發(fā),來看下異常處理的最佳實(shí)踐。

作為消費(fèi)者

當(dāng)作為消費(fèi)者的時(shí)候,我們關(guān)心的是使用的功能是否會(huì)拋出異常,如果是,他們有哪些異常。比如: 

  1. import foo from "lucifer";  
  2. try {  
  3.   foo.bar();  
  4. } catch (err) {  
  5.   // 有哪些異常?  

當(dāng)然,理論上 foo.bar 可能產(chǎn)生任何異常,而不管它的 API 是這么寫的。但是我們關(guān)心的是可預(yù)期的異常。因此你一定希望這個(gè)時(shí)候有一個(gè) API 文檔,詳細(xì)列舉了這個(gè) API 可能產(chǎn)生的異常有哪些。

比如這個(gè) foo.bar 4 種可能的異常 分別是 A,B,C 和 D。其中 A 和 B 是我可以處理的,而 C 和 D 是我不能處理的。那么我應(yīng)該: 

  1. import foo from "lucifer";  
  2. try {  
  3.   foo.bar();  
  4. } catch (err) {  
  5.   if (err.code === "A") {  
  6.     return console.log("A happened");  
  7.   }  
  8.   if (err.code === "B") {  
  9.     return console.log("B happened");  
  10.   }  
  11.   throw err;  

可以看出,不管是 C 和 D,還是 API 中沒有列舉的各種可能異常,我們的做法都是直接拋出。

作為生產(chǎn)者

如果你作為生產(chǎn)者,你要做的就是提供上面提到的詳細(xì)的 API,告訴消費(fèi)者你的可能錯(cuò)誤有哪些。這樣消費(fèi)者就可以在 catch 中進(jìn)行相應(yīng)判斷,處理異常情況。

你可以提供類似上圖的錯(cuò)誤表,讓大家可以很快知道可能存在的可預(yù)知異常有哪些。不得不吐槽一句,在這一方面很多框架,庫做的都很差。希望大家可以重視起來,努力維護(hù)良好的前端開發(fā)大環(huán)境。

總結(jié)

本文很長(zhǎng),如果你能耐心看完,你真得給可以給自己鼓個(gè)掌 。

我從什么是異常,以及異常的分類,讓大家正確認(rèn)識(shí)異常,簡(jiǎn)單來說異常就是一種數(shù)據(jù)結(jié)構(gòu)而已。

接著,我又講到了異常的傳播和處理。這兩個(gè)部分是緊密聯(lián)系的。異常的傳播和事件傳播沒有本質(zhì)不同,主要不同是數(shù)據(jù)結(jié)構(gòu)不同,思想是類似的。具體來說異常會(huì)從發(fā)生錯(cuò)誤的調(diào)用處,沿著調(diào)用?;赝?,直到第一個(gè) catch 語句或者棧為空。如果棧為空都沒有碰到一個(gè) catch,則會(huì)拋出uncaught Error。 需要特別注意的是異步的異常處理,不過你如果對(duì)我講的原理了解了,這都不是事。

然后,我提出了兩個(gè)腦洞問題:

  •  徹底消除運(yùn)行時(shí)異常可能么?
  •  運(yùn)行時(shí)異??梢曰謴?fù)么?

這兩個(gè)問題非常值得研究,但由于篇幅原因,我這里只是給你講個(gè)輪廓而已。如果你對(duì)這兩個(gè)話題感興趣,可以和我交流。

最后,我提到了前端異常處理的最佳實(shí)踐。大家通過兩種角色(生產(chǎn)者和消費(fèi)者)的轉(zhuǎn)換,認(rèn)識(shí)一下不同決定關(guān)注點(diǎn)以及承擔(dān)責(zé)任的不同。具體來說提到了 明確聲明可能的異常以及 處理你應(yīng)該處理的,不要吞沒你不能處理的異常。當(dāng)然這個(gè)最佳實(shí)踐仍然是輪廓性的。如果大家想要一份 前端最佳實(shí)踐 checklist,可以給我留言。留言人數(shù)較多的話,我考慮專門寫一個(gè)前端最佳實(shí)踐 checklist 類型的文章。

大家也可以關(guān)注我的公眾號(hào)《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認(rèn)識(shí)你不知道的前端。 

 

責(zé)任編輯:龐桂玉 來源: segmentfault
相關(guān)推薦

2021-10-18 11:58:56

負(fù)載均衡虛擬機(jī)

2022-09-06 08:02:40

死鎖順序鎖輪詢鎖

2021-01-19 05:49:44

DNS協(xié)議

2022-04-25 10:56:33

前端優(yōu)化性能

2020-08-31 14:30:47

Redis數(shù)據(jù)結(jié)構(gòu)數(shù)據(jù)庫

2022-09-14 09:01:55

shell可視化

2024-03-07 18:11:39

Golang采集鏈接

2020-11-16 10:47:14

FreeRTOS應(yīng)用嵌入式

2020-07-15 08:57:40

HTTPSTCP協(xié)議

2023-06-12 08:49:12

RocketMQ消費(fèi)邏輯

2022-07-19 16:03:14

KubernetesLinux

2020-07-09 07:54:35

ThreadPoolE線程池

2022-10-10 08:35:17

kafka工作機(jī)制消息發(fā)送

2024-05-10 12:59:58

PyTorch人工智能

2024-01-11 09:53:31

面試C++

2022-09-08 10:14:29

人臉識(shí)別算法

2024-01-05 08:30:26

自動(dòng)駕駛算法

2021-08-26 05:02:50

分布式設(shè)計(jì)

2022-07-15 16:31:49

Postman測(cè)試

2025-09-08 06:25:00

RPCRPC框架微服務(wù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

深夜福利网站在线观看| 日韩欧美一区二区三区四区 | 性生交大片免费看l| 国精一区二区三区| 久久久国产一区二区三区四区小说 | 免费国产亚洲视频| 欧美福利小视频| 日韩一区二区a片免费观看| 日韩精品一区二区三区中文| 欧美午夜女人视频在线| 国产一二三四区在线观看| 男女污污视频在线观看| 精品亚洲成av人在线观看| 51精品国产黑色丝袜高跟鞋| 精品人妻伦九区久久aaa片| 视频福利一区| 欧美刺激脚交jootjob| 国产高潮免费视频| 国产传媒在线| 亚洲精品美国一| 日韩欧美亚洲v片| 天天操天天操天天| 国产精品一区二区久久精品爱涩| 欧美最顶级的aⅴ艳星| 久久久久久久久艹| 99久久久国产精品美女| 亚洲视频在线视频| yy6080午夜| 一区二区视频| 91精品婷婷国产综合久久竹菊| 国产福利视频在线播放| av电影免费在线看| 亚洲一区二区三区国产| 欧美少妇在线观看| 日本成人在线播放| 国产精品色哟哟网站| 欧美污视频久久久| 麻豆app在线观看| 97国产一区二区| 国产伦精品一区二区三区照片91| 国产成人精品一区二三区四区五区| 人人狠狠综合久久亚洲| 国产精品91免费在线| 国产精品视频免费播放| 日韩一级不卡| 国产91ⅴ在线精品免费观看| 日韩精品一区二区av| 激情久久久久久久| 欧美日韩成人网| 久久激情免费视频| 亚洲午夜久久久久久尤物| 久久99国产精品久久久久久久久| 亚洲av无码一区二区三区在线| 天天久久综合| 另类图片亚洲另类| 国产精品白嫩白嫩大学美女| 亚洲欧美综合国产精品一区| 久久久久久久久国产精品| 麻豆一区二区三区精品视频| 精品成人一区| 68精品国产免费久久久久久婷婷| 亚洲天堂日韩av| 裸体素人女欧美日韩| 国产精品777| 96日本xxxxxⅹxxx17| 韩国v欧美v日本v亚洲v| 91在线短视频| 人人妻人人玩人人澡人人爽| 91视频国产观看| 日本午夜一区二区三区| 91精彩在线视频| **欧美大码日韩| av动漫免费观看| 久草在线视频福利| 一本到一区二区三区| 天天色综合社区| 国产精品**亚洲精品| 精品精品国产高清一毛片一天堂| 久久精品女同亚洲女同13| 香蕉久久夜色精品国产更新时间| 一色桃子一区二区| 欧美日韩一级大片| 美女91精品| 91精品在线观看视频| 亚洲成人黄色片| 久久久另类综合| 五月天综合婷婷| 国内精彩免费自拍视频在线观看网址| 欧美视频在线免费| 岛国毛片在线播放| 日本一道高清一区二区三区| 中文字幕日韩av| 国产在线拍揄自揄拍| 日日欢夜夜爽一区| 成人av资源网| 国产三级在线| 亚洲国产一二三| 国产又大又黄又粗的视频| 一区二区精彩视频| 亚洲一区二区国产| 国产污片在线观看| 久久成人久久鬼色| 久久综合久久综合这里只有精品| 欧美一级二级三级区| 亚洲va在线va天堂| 天堂av在线8| 亚洲品质自拍| 欧美精品videosex牲欧美| 中文字幕第一页在线播放| gogogo免费视频观看亚洲一| 国产日韩视频在线播放| 亚洲综合电影| 亚洲精品在线网站| 日韩欧美综合视频| 免费成人在线网站| 欧美激情第六页| 男女免费观看在线爽爽爽视频| 欧美视频在线不卡| 国产精品三级在线观看无码| 欧美午夜电影在线观看| 91精品久久久久久久久久久久久 | 天天做天天爱夜夜爽| 国产精品一区久久久久| 亚洲综合第一| av高清一区| 亚洲摸下面视频| 国产情侣在线视频| 国产成人精品aa毛片| 欧美 另类 交| 欧美爱爱视频| 色老头一区二区三区| 99久久久久久久久| 久久综合网色—综合色88| 日韩网站在线免费观看| 91成人福利| 欧美日韩国产第一页| 99视频免费看| 亚洲精品乱码久久久久久久久| 亚洲综合婷婷久久| 欧美日韩一二三四| 国产成人精品一区二区在线| 青青久在线视频免费观看| 精品国产老师黑色丝袜高跟鞋| 欧美一级大片免费看| 欧美久久综合| 成人在线资源网址| 波多一区二区| 亚洲黄一区二区| 日韩av在线播| 2017欧美狠狠色| 女人扒开屁股爽桶30分钟| 一区二区三区日本久久久 | 久久久精品国产免大香伊| 国产极品粉嫩福利姬萌白酱| 日韩影视高清在线观看| 91av在线看| 黄色的视频在线免费观看| 精品国产91久久久久久| 人妻丰满熟妇aⅴ无码| 可以免费看不卡的av网站| 日韩欧美精品久久| 久草综合在线| 欧美日本国产在线| 午夜视频在线播放| 欧美在线你懂得| 欧美性生给视频| 国产精品一区在线观看乱码| 国产免费黄色一级片| 亚洲精品亚洲人成在线观看| 国产精品第一第二| 国产秀色在线www免费观看| 日韩一级免费观看| 日韩成人av毛片| 国产欧美日本一区视频| 特黄视频免费观看| 在线国产精品一区| 狼狼综合久久久久综合网| av在线不卡精品| 蜜臀久久99精品久久久久久宅男 | 97精品视频在线| 九九九伊在人线综合| 欧美日韩国产系列| 日本三级片在线观看| 欧美国产日韩a欧美在线观看 | 久久综合九色综合97婷婷 | 欧美一级做a| 久久久久久久久爱| 在线观看的av| 精品国产乱码久久久久久牛牛 | 欧美一区国产二区| 国产黄色片免费看| 亚洲图片欧美激情| 亚洲天堂网一区二区| 久久99久国产精品黄毛片色诱| 黄色一级在线视频| 国产精品久久久久久麻豆一区软件 | 久久成人亚洲精品| 男女视频在线观看| 日韩欧美综合在线| 免费在线不卡av| 亚洲国产精品一区二区久久| 韩国三级hd中文字幕| 成人在线综合网| 国产三级国产精品国产专区50| 欧美日韩18| 亚洲图片都市激情| 自拍偷拍欧美一区| 成人资源av| 99久久这里有精品| 国产精品va在线播放| 精品丝袜在线| 欧美乱大交xxxxx另类电影| 国产大学生校花援交在线播放| 精品日韩成人av| 在线观看国产精品视频| 欧美色视频日本版| 国产一级视频在线播放| 伊人色综合久久天天| 国产农村妇女精品一区| 久久久久久久综合日本| 精品国产人妻一区二区三区| 国产高清视频一区| 亚洲欧美日韩一二三区| 麻豆精品国产传媒mv男同| 国产91在线视频观看| 伊人久久综合| 菠萝蜜视频在线观看入口| 91精品推荐| 中文字幕欧美日韩一区二区| 日韩精品网站| 亚欧洲精品在线视频免费观看| 自拍亚洲一区| 欧美一区二区三区在线播放 | 中文字幕在线播| 欧美日韩在线影院| 国产精品自拍99| 欧美日韩另类在线| 日韩精品在线免费看| 亚洲综合网站在线观看| 久久网一区二区| 亚洲国产色一区| 久久香蕉精品视频| 亚洲二区视频在线| 国产亚洲小视频| 亚洲一区中文日韩| 国产精品500部| 日韩欧美主播在线| 中文字幕 人妻熟女| 欧美在线不卡视频| 亚洲熟妇av乱码在线观看| 欧美日韩高清在线播放| 国产又粗又大又爽| 91精品国产全国免费观看 | 成人免费毛片高清视频| 日本道中文字幕| 久久一区二区视频| 日本理论中文字幕| 国产精品理论在线观看| 内射一区二区三区| 亚洲国产精品欧美一二99| 成人精品免费在线观看| 色婷婷av一区二区三区软件| 中文在线最新版天堂| 欧美剧情电影在线观看完整版免费励志电影 | 日本xxxxxxxxx18| 亚洲国产成人自拍| 翔田千里88av中文字幕| 亚洲成人激情综合网| 天天爽夜夜爽人人爽| 欧美视频精品在线观看| 精品人妻av一区二区三区| 亚洲第一精品夜夜躁人人躁| 可以在线观看的av| 久久精品国亚洲| 都市激情久久综合| 国产成人午夜视频网址| 精品视频91| 久久草视频在线看| 日韩在线第七页| 久久精品xxx| 视频在线观看91| www.污网站| 2019国产精品| 欧美国产日韩在线观看成人| 欧美日韩免费在线观看| 一级全黄裸体免费视频| 亚洲第一国产精品| 浪潮av一区| 欧美最顶级的aⅴ艳星| 精品91福利视频| 欧美极品一区二区| 欧美成人午夜| the porn av| 成人性生交大片免费看视频在线 | 国产精品久久久久久av| 视频一区日韩精品| 日韩wuma| 99精品国产一区二区青青牛奶| 高清一区二区视频| 波多野结衣91| a在线视频播放观看免费观看| 国产一二三精品| 91九色单男在线观看| 四虎永久精品在线| 久久99精品久久久久久秒播放器 | 播五月开心婷婷综合| 人妻互换一区二区激情偷拍| 亚洲动漫第一页| av天堂一区二区三区| 久久aⅴ国产紧身牛仔裤| 内射国产内射夫妻免费频道| 美女诱惑一区二区| 国产高清成人久久| 亚洲视频免费在线观看| 成人免费毛片视频| 精品国免费一区二区三区| 婷婷激情在线| 国产激情综合五月久久| 麻豆一区二区麻豆免费观看| 在线观看av的网址| 久久99这里只有精品| 五月天精品视频| 午夜影视日本亚洲欧洲精品| 国产露脸无套对白在线播放| 亚洲午夜久久久久久久| 91福利在线尤物| www.一区二区三区| 亚洲国产精品成人| 国产3p在线播放| 国产精品日日摸夜夜摸av| 精品黑人一区二区三区| 亚洲男人天堂网站| 麻豆免费在线| 国产日韩一区二区| 雨宫琴音一区二区在线| xxxx视频在线观看| 亚洲已满18点击进入久久| 国产jzjzjz丝袜老师水多| 久久亚洲国产精品成人av秋霞| 成人全视频免费观看在线看| 亚洲韩国在线| 精油按摩中文字幕久久| 亚洲人与黑人屁股眼交| 7799精品视频| 国产欧美黑人| 91精品综合久久| 欧美日韩国产综合网| 中文字幕人妻无码系列第三区| 亚洲人成网站影音先锋播放| 国产巨乳在线观看| 精品综合久久久久久97| 中文字幕久久精品一区二区 | 91精品视频网| 18av在线视频| 国产精品二区在线| 亚洲美女毛片| 熟女俱乐部一区二区| 91成人免费在线| 在线观看av黄网站永久| 91日本在线视频| 亚洲激情社区| 91视频免费观看网站| 欧美日韩综合在线免费观看| 国产丝袜在线| 国产一区二区无遮挡| 久久av一区二区三区| 天美传媒免费在线观看| 7777精品久久久大香线蕉 | 精品久久久久久中文字幕一区奶水| 天堂av资源网| 国产精品v日韩精品| 在线国产一区二区| 黄色网址在线视频| 欧美在线影院一区二区| 成人无遮挡免费网站视频在线观看| 春色成人在线视频| 久久精品免费| 日韩在线观看视频一区二区| 亚洲国产另类 国产精品国产免费| 色多多在线观看| 亚洲一区二区精品在线观看| 国产不卡视频在线播放| 嫩草影院一区二区三区| 久久中文久久字幕| 四虎884aa成人精品最新| 黄大色黄女片18第一次| 亚洲图片欧美视频| 成人免费在线观看| av成人综合网| 免费成人小视频| 精品成人久久久| 色偷偷91综合久久噜噜| 国产精品jk白丝蜜臀av小说 | av一区二区久久| 亚洲天堂aaa| 69av成年福利视频| 自拍偷拍欧美| 婷婷综合在线视频| 日韩av最新在线观看| 91丨精品丨国产|