国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

【W(wǎng)eb技術(shù)】807- 你不知道的前端異常處理

共 16551字,需瀏覽 34分鐘

 ·

2020-12-18 03:10


?

這是腦洞前端第「95」篇原創(chuàng)文章

?

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

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

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

?

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

?

什么是異常

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

從根本上來(lái)說(shuō),異常就是一個(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)生異常。比如:

function?t()?{
??console.log("start");
??new?Error();
??console.log("end");
}
t();

(動(dòng)畫演示)

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

異常的分類

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

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

編譯時(shí)異常

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

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

const?s:?string?=?123;

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

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

tsc?a.ts
a.ts:1:7?-?error?TS2322:?Type?'123'?is?not?assignable?to?type?'string'.

1?const?s:?string?=?123;
????????~


Found?1?error.

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

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

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

function?t()?{
??console.log('start')
??await?sa
??console.log('end')
}
t()

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

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

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

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

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

function?t()?{
??console.log("start");
??throw?1;
??console.log("end");
}
t();

(動(dòng)畫演示)

?

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

?

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

異常的傳播

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

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

一點(diǎn)小前提

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

oldError?=?Error;
Error?=?function?({?code,?message,?fileName,?lineNumber?})?{
??error?=?new?oldError(message,?fileName,?lineNumber);
??error.code?=?code;
??return?error;
};

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

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

throw?new?Error(`I'm?Exception`);

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

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

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

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

?

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

?

那什么時(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)。我們先來(lái)看下拋出異常,告訴調(diào)用者「你的輸入,我處理不了」這種情況。

function?divide(a,?b)?{
??a?=?+a;
??b?=?+b;?//?轉(zhuǎn)化成數(shù)字
??if?(!b)?{
????//?匹配?+0,?-0,?NaN
????throw?new?Error({
??????code:?1,
??????message:?"Invalid?dividend?"?+?b,
????});
??}
??if?(Number.isNaN(a))?{
????//?匹配?NaN
????throw?new?Error({
??????code:?2,
??????message:?"Invalid?divisor?"?+?a,
????});
??}
??return?a?/?b;
}

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

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

異常 or 返回

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

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

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

function?divide(a,?b)?{
??a?=?+a;
??b?=?+b;?//?轉(zhuǎn)化成數(shù)字
??if?(!b)?{
????//?匹配?+0,?-0,?NaN
????return?new?Error({
??????code:?1,
??????message:?"Invalid?dividend?"?+?b,
????});
??}
??if?(Number.isNaN(a))?{
????//?匹配?NaN
????return?new?Error({
??????code:?2,
??????message:?"Invalid?divisor?"?+?a,
????});
??}
??return?a?/?b;
}

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

function?t()?{
??const?res?=?divide("foo",?"bar");

??if?(res.code?===?1)?{
????return?console.log("被除數(shù)必須是除0之外的數(shù)");
??}
??if?(res.code?===?2)?{
????return?console.log("除數(shù)必須是數(shù)字");
??}
??return?new?Error("不可預(yù)知的錯(cuò)誤");
}

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

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

res,?err?:=?divide("foo",?"bar");
if?err?!=?nil?{
????log.Fatal(err)
}

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

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

?

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

?

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

?

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

?

偽代碼來(lái)描述一下:

function?bubble(error,?fn)?{
??if?(fn.hasCatchBlock())?{
????runCatchCode(error);
??}
??if?(callstack.isNotEmpty())?{
????bubble(error,?callstack.pop());
??}
}
?

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

?

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

異常的處理

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

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

function?a()?{
??b();
}
function?b()?{
??c();
}
function?c()?{
??throw?new?Error("an?error??occured");
}
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ā)者定位問(wèn)題而存在的。

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

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

我們來(lái)分別看下:

function?a()?{
??b();
}
function?b()?{
??c();
}
function?c()?{
??try?{
????throw?new?Error("an?error??occured");
??}?catch?(err)?{
????console.log(err);
??}
}
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 下面增加一行代碼,這行代碼是無(wú)法被執(zhí)行的,「無(wú)論這個(gè)錯(cuò)誤有沒有被捕獲」。

function?c()?{
??try?{
????throw?new?Error("an?error??occured");
????console.log("will?never?run");
??}?catch?(err)?{
????console.log(err);
??}
}

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

function?a()?{
??b();
}
function?b()?{
??try?{
????c();
??}?catch?(err)?{
????console.log(err);
??}
}
function?c()?{
??throw?new?Error("an?error??occured");
}

a();

(在 b 中 catch)

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

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

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

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

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

這就是一個(gè)典型的責(zé)任鏈模式。誰(shuí)有責(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ò)誤之后,通過(guò)查看 code 值判斷其可以被處理,于是打印I can handle this。

function?a()?{
??try?{
????b();
??}?catch?(err)?{
????if?(err.code?===?"NETWORK_ERROR")?{
??????return?console.log("I?can?handle?this");
????}
????//?can't?handle,?pass?it?down
????throw?err;
??}
}
function?b()?{
??try?{
????c();
??}?catch?(err)?{
????if?(err.code?===?"INPUT_ERROR")?{
??????return?console.log("I?can?handle?this");
????}
????//?can't?handle,?pass?it?down
????throw?err;
??}
}
function?c()?{
??throw?new?Error({
????code:?"INPUT_ERROR",
????message:?"an?error??occured",
??});
}

a();

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

舉個(gè)例子:

function?a()?{
??try?{
????b();
??}?catch?(err)?{
????if?(err.code?===?"NETWORK_ERROR")?{
??????return?console.log("I?can?handle?this");
????}
????//?can't?handle,?pass?it?down
????throw?err;
??}
}
function?b()?{
??try?{
????c();
??}?catch?(err)?{
????if?(err.code?===?"INPUT_ERROR")?{
??????return?console.log("I?can?handle?this");
????}
??}
}
function?c()?{
??throw?new?Error({
????code:?"NETWORK_ERROR",
????message:?"an?error??occured",
??});
}

a();

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

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

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

同步與異步

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

//?異步讀取文件
fs.readFileSync();
//?同步讀取文件
fs.readFile();

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

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

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

而如果以異步方式的話:

fs.readFile('something-not-exist.lucifer',?(err,?data)?=>?{if(err)?{throw?err}});
console.log('lucifer')
lucifer
undefined
Thrown:
[Error:?ENOENT:?no?such?file?or?directory,?open?'something-not-exist.lucifer']?{
??errno:?-2,
??code:?'ENOENT',
??syscall:?'open',
??path:?'something-not-exist.lucifer'
}
>

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

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

?

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

?

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

try?{
??fs.readFile("something-not-exist.lucifer",?(err,?data)?=>?{
????if?(err)?{
??????throw?err;
????}
??});
}?catch?(err)?{
??console.log("catching?an?error");
}

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

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

try?{
??fs.readFileSync("something-not-exist.lucifer");
}?catch?(err)?{
??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 語(yǔ)句。

簡(jiǎn)單來(lái)說(shuō)就是「異步產(chǎn)生的錯(cuò)誤不能用 try catch 捕獲,而要使用回調(diào)捕獲?!?/strong>

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

rejectIn?=?(ms)?=>
??new?Promise((_,?r)?=>?{
????setTimeout(()?=>?{
??????r(1);
????},?ms);
??});
async?function?t()?{
??try?{
????await?rejectIn(0);
??}?catch?(err)?{
????console.log("catching?an?error",?err);
??}
}

t();

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

而如果我們使用 babel 轉(zhuǎn)義下,會(huì)發(fā)現(xiàn) try catch 不見了,變成了 switch case 語(yǔ)句。這就是 try catch “可以捕獲異步異?!钡脑颍瑑H此而已,沒有更多。

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

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

?

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

?

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

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

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

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

p?=?Promise.reject(1);
p.catch((err)?=>?{
??if?(err?==?1)?{
????return?console.log("I?can?handle?this");
??}
??throw?err;
});

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

我個(gè)人對(duì)目前前端現(xiàn)狀最為頭疼的一點(diǎn)是:「大家過(guò)分依賴運(yùn)行時(shí),而嚴(yán)重忽略編譯時(shí)」。我見過(guò)很多程序,你如果不運(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)來(lái)看:假如所有的異常都在編譯時(shí)發(fā)生,而一定不會(huì)在運(yùn)行時(shí)發(fā)生。那么我們是不是就可以「信心滿滿」地對(duì)應(yīng)用進(jìn)行重構(gòu)啦?

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

?

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

?

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

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

maybeResolveOrNot?=?(ms)?=>
??setTimeout(()?=>?{
????if?(Math.random()?>?0.5)?{
??????console.log("ok");
????}?else?{
??????throw?new?Error("error");
????}
??});

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

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

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

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

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

null.toString();

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

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

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

function?t()?{
??console.log("start");
??throw?1;
??console.log("end");
}
t();

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

function?t()?{
??try?{
????console.log("start");
????throw?1;
????console.log("end");
??}?catch?(err)?{
????console.log("relax,?I?can?handle?this");
??}
}
t();

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

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

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

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

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

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

?

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

?

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

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

function?t()?{
??try?{
????const?res?=?divide("foo",?"bar");
????alert(`you?got?${res}`);
??}?catch?(err)?{
????console.log("releax,?I?can?handle?this");
????resume?-?1;
??}
}
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í)踐

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

我們平時(shí)開發(fā)一個(gè)應(yīng)用。如果站在生產(chǎn)者和消費(fèi)者的角度來(lái)看的話。當(dāng)我們使用別人封裝的框架,庫(kù),模塊,甚至是函數(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)化。不過(guò)為了簡(jiǎn)單起見,本文不考慮這種關(guān)系。這里的生產(chǎn)者指的就是給他人使用的功能,是純粹的生產(chǎn)者。

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

作為消費(fèi)者

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

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

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

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

import?foo?from?"lucifer";
try?{
??foo.bar();
}?catch?(err)?{
??if?(err.code?===?"A")?{
????return?console.log("A?happened");
??}
??if?(err.code?===?"B")?{
????return?console.log("B?happened");
??}
??throw?err;
}

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

作為生產(chǎn)者

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

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

總結(jié)

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

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

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

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

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

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

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

Reference

[1]

Null Pointer Exception: https://zh.wikipedia.org/wiki/%E7%A9%BA%E6%8C%87%E6%A8%99#NullPointerException

[2]

瀏覽器事件模型: https://lucifer.ren/blog/2019/12/11/browser-event/

[3]

Go 源碼關(guān)于錯(cuò)誤測(cè)試部分: https://github.com/golang/go/blob/master/src/os/error_test.go

[4]

《一文看懂瀏覽器事件循環(huán)》: https://lucifer.ren/blog/2019/12/11/event-loop/

[5]

babel 轉(zhuǎn)義環(huán)境: https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=usage&spec=true&loose=true&code_lz=E4UwViDGAuCSB2ACAvIgFAWwM4EoUD4AoRReEAd0QAVgB7DASyxDTQH0AaRYPZfRAN7ESiZtAAqDDCFoBXaK178hIkcDQBGHAG5hJAL5dsO4fpMBDLAE94kRADNZt6A1pIFeFYmjArgvYjm5OYM0NzgUHDwaAAMJgaIkObQkAAW6CDAPP6qkG5YtAA2IAB0hbQA5mgAREkpqQzwFYFImXTA1Vxt8Yj6hH2EHtpAA&debug=false&forceAllTransforms=true&shippedProposals=true&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Ces2015%2Ces2016%2Ces2017%2Creact%2Cstage-0%2Cstage-1%2Cstage-2%2Cstage-3%2Ces2015-loose%2Ctypescript%2Cflow%2Cenv&prettier=false&targets=Electron-1.8%252CNode-10.13&version=7.10.2&externalPlugins=%40babel%2Fplugin-transform-arrow-functions%407.8.3

[6]

函數(shù)式編程入門篇: https://github.com/azl397985856/functional-programming

推薦閱讀


1、用TypeScript編寫React的最佳實(shí)踐

2、前端動(dòng)畫必知必會(huì):React 和 Vue 都在用的 FLIP 思想實(shí)戰(zhàn)。

3、lucifer與它的《力扣加加》來(lái)啦

4、你不知道的 React 最佳實(shí)踐

5、或許是一本可以徹底改變你刷 LeetCode 效率的題解書

6、迎接Vue3.0 | 在Vue2與Vue3中構(gòu)建相同的組件

7、vite —— 一種新的、更快地 web 開發(fā)工具


?

關(guān)注加加,星標(biāo)加加~

?

如果覺得文章不錯(cuò),幫忙點(diǎn)個(gè)在看唄




瀏覽 32
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 黄色网址av| 色婷婷久综合久久一本国产AV | 精品久久久久久AV2025| 黄色片视频网站| 99久久国| 狠狠躁18三区二区一区免费人| 黄色视频免费在线观看网站| 亚洲成人高清在线| 免费+无码+精品| 精品字幕| 成人视频免费观看18| 91做爱视频| 日韩无码人妻一区二区| 中文字幕在线免费播放| 国产精品每日更新| 息子交尾一区二区三区| 伊人网在线| 日韩五月婷婷| 国产日韩欧美综合精品在线观看| 97中文字幕| 91视频久久| 大香蕉手机在线视频| 久久狼友| 婷婷丁香一区二区三区| 亚洲大片在线观看| 国产亚洲99久久精品熟女| 国产熟女AV| 无码精品人妻一区二区欧美| 亚洲国产另类精品| 在线成人一区二区| 日韩成人电影| 日韩福利视频| 东方av在线观看| 国产精品秘麻豆免费版现看视频| 亚洲无码999| 国产乱子伦-区二区三区四区| 成人A√| 久久99精品国产.久久久久久| 亚洲成人在线视频免费观看| 西西444WWW无码视频软件功能介绍 | 亚洲激情五月| 免费在线观看无码视频| 美日韩精品| 四虎福利| 亚洲免费黄色片| 五月婷婷五月丁香| 在线视频观看一区| 国产丝袜久久| 嫩草视频在线播放| 亚洲AV久久无码| 国产欧美一| 国产AV大香蕉| 国产无遮挡又黄又爽在线观看 | 91二区三区| 操美女的网站| 九七无码| 国产成人午夜视频| 性性性性性XXXXX| 久久国产一级片| 亚洲日韩网站在线观看| 西西444WWW无码视频软件| 综合五月婷婷| 日本午夜三级视频| 青青草综合| 竹菊影视一区二区三区| 欧美性爱操逼视频| 新超碰在线观看| 激情内射| 亚洲欧美熟妇久久久久久久久 | 性爱视频无码| 日韩中文字幕人妻| 青春草在线视频观看| 欧美一级A片免费看| 国产精品色视频| 午夜AAA| 无码人妻精品一区二区三区蜜臀百度 | 射死你天天日| 黄色小说视频网站| 视色影院| v在线| 波多野结衣av在线播放| 久久久成人视频| 日韩无码一区二区三区四区| 国产理论电影| 色女人天堂| 99视频网| 大香蕉综合在线| 四虎精品| 久久婷婷五月综合| 日本精品视频| 亚洲第一中文字幕网| 亚洲女与黑人正在播放| 成人AV电影在线观看| 国产精品毛片A√一区| 亚洲AAAAAA| 成人手机看片| 69精品免费视频| 人人人人摸| 国产在线欧美| 中文人妻无码| 天天爽夜夜爽夜夜爽精品| 蜜桃久久av一区| 岛国AV片| 91人妻人人爽人人澡人人爽| 黄页免费视频| 2025天天操| 国产精品人妻AⅤ在线看| 天天日天天射天天干| 国产精品99久久久久的广告情况| 亚洲无码一| 免费看a| 亚洲免费视频在线观看| 欧美色图视频网站| 人人操人人爽人人爱| 69堂在线观看| 无码人妻一区二区三区三| 成人福利视频在线| 乱伦激情视频| 久久久久99精品成人片三人毛片| AV网站在线免费观看| 久久精品99久久久久久| 国产无码区| 啪啪网站免费| 99在线免费观看| 69精品| 91爱爱com| 中文字幕网站在线观看| henhengan| 中国丰满妇BBwBBwHD| 蜜臀久久99精品久久久久久婷婷| 97国产精品人人爽人人做| 无码国产一区二区三区四区五区 | 大香蕉在线观看视频| 久久久久久久久久久久久自慰小片| 麻豆精品国产| 男女AV在线免费观看| av一区二区在线观看| 亚洲高清电影| 日韩视频中文| 亚洲乱码日产精品BD在线观看| 影音先锋日韩资源| 日本一级a片| 美日韩视频欧美一区二区视频 | 成人特级毛片全部免费播放| 在线大香蕉| 成人视频网| 国产AV无遮挡| 无码人妻丰满熟妇区17水蜜桃| 伊人成人免费视频| 亚洲男人的天堂视频网在线观看+720P | 国产女人精品视频| 欧美激情久久久| 人人色在线观看| 午夜福利国产| 日韩高清av| a国产| 国产无码AV| 色99在线视频| 国产网站视频| 韩日一区二区三区| 无码av中文字幕| 国产AV无码成人精品毛片| 亚洲第一黄色| 免费无码成人| 亚洲在线一区二区| 中文子幕免费毛片| 久久天天操| 亚洲无码高清在线观看| 国内自拍激情视频| 国产日逼片| 久久久精品欧美| 国产精品无码在线| 久久国产AV| 兔子先生和優奈玩游戲脫衣服,運氣報表優奈輸到脫精光 | 久草资源在线观看| 粉嫩av懂色av蜜臀av熟妇| 免费看一区二区三区| 国产午夜无码福利视频| 色色色色色色色色欧美| 永久免费黄色视频网站| 91人妻人人澡人人爽人人玩| 老婆中文字幕乱码中文乱码| 国产成人精品无码免费| 巨乳国产一区| 四虎午夜福利| 91西安站街老熟女露脸| 亚洲AV成人无码精品直播在线| 学生妹毛片| 成人午夜精品福利免费| 不卡无码中文字幕| 日韩少妇无码视频| 日本丰满老熟妇乱子伦| 99热8| 中国一级黄片| 一区二区三区四区av| 精品一区二区三区在线观看| 日本精品二区| 玩弄大乳乳妾高潮乳喷视频| 欧美色图视频在线观看| 日韩第1页| 农村少妇久久久久久久| 国产精品自拍视频| 级婬片AAAAAAA免费| 亚洲精品秘一区二区三区影| 国产一区二区三区在线视频| 欧美性爱中文字幕| 日韩AV性爱| 日韩日批视频| 亚洲三级黄色视频| 无码国产精品一区二区免费96| 午夜精品久久久久久久久久久久| 国产成人片在线观看| 99久久精品国产一区二区成人 | 成人无码日韩| 99在线精品视频| 一牛影视精品av| 玖玖在线播放| 欧美日韩综合| 亚洲日色| 伊人久久大香蕉国产| 国产日逼网站| 91蜜桃在线| 在线观看av中文字幕| 51妺妺嘿嘿午夜成人| 免费AV在线播放| 久久午夜夜伦鲁鲁一区二区| 做aAAAAA免费视频| 日韩在线免费播放| 3344gc在线观看入口| 超碰199| 日韩av综合| 欧美成人高清视频| 搡BBB| 国产婷婷内射| 五月婷婷色播| 九九这里有精品| 毛片69| 亚洲AV成人电影| 国产亚洲一区二区三区| 亚洲秘无码一区二区三区胖子| 操女人的网站| 久久狼友| 日韩A级片| 996热re视频精品视频这里| 特级AV| 成人黄色电影在线观看| 青青草国产在线视频| 中文字幕在线观看二区| 免费看黄色视频| 啪啪人妻| 欧美日韩亚洲天堂| 无码久久久| 牛牛精品一区二区AV| 无码不卡av| 久久久久久AV| 国产精品欧美综合亚洲| 一本无码中文字幕| 人妻互换一二三区免费| 欧美精品一区二区三区蜜臀| 爱色五月| 亚洲AV成人无码| 国产黄色免费网站| 黄色一级aa片| 丁香网五月天| 黑巨茎大战欧美白妞小说| 91嫖妓站街埯店老熟女| 俺去也www俺去也com| 乱子伦】国产精品| 日韩无码专区电影| 100国产精品人妻无码| 伊人天天操| 国产成人一区二区三区A片免费| 色噜噜狠狠一区二区三区300部| 高清无码黄| 在线观看黄色视频网站| 国产人妻人伦精品1国产丝袜 | 一级二级三级毛片| 中文国产字幕| 国产精品剧情| 久久久久久久久久成人| 丁香成人五月天| 天天天日天天天操| 晚上碰视频| 日韩乱伦电影| 欧美第一网站| 久久国产精品免费视频| 三级网站在线播放| 亚洲精品成人无码熟妇在线| 色天使视频| 最新中文字幕777私人在线| 无码av网| 性生活黄色视频| 天天干天天爽| 操鸡视频在线观看| 视频一区二区三区免费| 国精品无码人妻一区二区三区免费| 国产午夜精品一区二区三区四区| 干干日日| 久草在在线视频| 国内精品内射| 国产suv精品一区二区| 国产色自拍| 亚洲日韩欧美一区二区天天天| 嫩操影院| 国产精品囯产三级囯产AV野外| 国产区AV| 爱精品视频| 亚洲欧美国产高清vA在线播放| 在线观看日本vs欧洲vs美洲| 无码人妻91| 青青草中文字幕| 国产一级一片免费播放放a| 天天干天天肏| 欧美精品无码| 校园春色成人| 一起草在线视频| 国产你懂的| 亚洲第一成年人网站| 亚洲精品97久久中文字幕| 蜜桃网站视频| 亚洲操B视频| 丁香五月天堂网| 亚洲AV图片| 黄片视频网站| 91成全在线| 黄色视频网站国产| 免费AV观看| 97人人色| 久久肏| 亚洲污污| 一起操在线观看| 色噜噜狠狠一区二区三区300部| 人妻无码久久| 美女裸体视频网站| 京熱大亂交无碼大亂交| 中文字幕三级av片| 97国产资源| 操逼免费观看| 欧美操逼电影| 欧美大香蕉网| 99久久人妻无码中文字幕系列| 黑人无码AV黑人天堂无码AV| 亚洲内射网| 色色色热热热| 欧美成人aaa| 六月婷婷中文字幕| 综合色国产精品欧美在线| 日韩中文字幕在线人成网站| www99热| 国产在线97| 亚洲福利视频在线| 第四色视频| 婷婷香蕉| 国产丝袜久久| 亚洲天天| 亚洲一区视频在线| 一区亚洲| 大陆搡BBBBB搡BBBBBB | 日韩成人AV在线| 亚洲精品性爱| 色射网| 亚洲色在线视频| 免费A片在线播放| 中文字幕高清| 婷婷综合亚洲| 午夜福利手机在线| 免费观看操逼| 人人操人人爱人人拍| 国产亚洲中文| 亚洲国产精品自在自线| 奇米88888| 在线观看高清无码中文字幕| 亚洲成人一级片| 黄色福利网址| 午夜成人鲁丝片午夜精品| 伊人久久AV诱惑悠悠| 亚洲精品成AV人片天堂无码| 黄色免费在线观看网站| 欧美色视频在线观看| 成人在线观看网| 精品四区| 99国产精品久久久久久久| 夜夜骑夜夜| 久热中文在线观看精品视频 | 你懂的在线免费观看| www.伊人| 午夜8050| 影音先锋成人在线| 日本AAAA片| 99热| 中文无码字幕在线| 国产操女人| 嫩BBB揍BBB揍BBB| 国产精品成人3p一区二区三区| 麻豆午夜成人无码电影| 人妻少妇精品视频一区二区三区 | 亚洲中文字幕久久日| 亚洲人成小说| 天天操天天谢| 国产成人无码免费看片| 亚洲成人视频网| 99久久精品国产一区色| 97人妻精品一区二区三区免| av在线天堂| 四川少妇bbbbbbbbb| 欧美成人视频电影无码高清| 国产亚洲精品码| 欧美熟女内射| 亚洲在线视频播放| 一级特黄A片| 亚洲中文字幕第一页| 三级网站网址| 久久人精品| 四lll少妇BBBB槡BBBB| 国产精品黄视频| 国产一区二区三区免费播放 | 亚洲色爽| 无码网站内射| 日韩中文字幕有码| 人人看人人插| 日韩欧美国产| 一级Aa视频免费看| 97人妻人人操| 青草青视频| 77777精品成人免费A片| 日韩黄色电影| 婷婷五月一区| 丰满大爆乳波霸奶| 51妺妺嘿嘿午夜成人A片| 99re66| 日本精品电影| 亚洲精品成人av无码| 人妻丰满熟妇av无码| 黄片入口| 天天射夜夜操| 操人| 黄页网站免费观看| 中文字幕欧美激情| 初学影院WWWBD英语完整版在线观看 | 天天添天天操| 成人免费黄色网| 肏少妇女情人大骚逼直播一区二区| 少妇高潮喷水| 人人弄人人| 无码中文字幕在线观看| 欧美午夜成人一区二区三区| AV青青草原| 黄色毛片在线观看| 欧美成年人视频| 福利老湿69| 久久草在线| 国产精品秘ThePorn| 啪啪成人网| 肏屄视频免费观看| 操小嫩逼视频| 精品无码一| 激情国产av| 成人无码激情| 欧美性爱香蕉视频| 人人肏人人摸| 男女拍拍拍拍| 一区二区三区免费| 亚洲成人三级| 九九久久精品视频| 日韩女人性爱| 国产天堂网| 99免费热视频在线| 日韩无码人妻一区二区三区| 口爆吞精在线观看| 欧美日韩一区在线观看| 99性视频| 色播婷婷五月天| 男人天堂免费视频| 97资源视频| 成人黄色电影在线观看| 亚洲人妻在线播放| 精品国产久久久久| 日韩一级片在线观看| 国产乱人| 日韩在线91| 国产免费一区二区三区网站免费 | 国产日本欧美韩国久久久久| 在线午夜福利| 九九视频网| 中国女人如毛片| 国产第一页在线| 97无码精品人妻一区二区三区| 人人看人人摸人人插| 国产精品秘久久久久久免费播放| 一区二区三区免费在线| av亚洲波多野结衣白嫩水多波| 炮友露脸青楼传媒刘颖儿| 青青无码| 成人久久综合| 国产特级毛片AAAAAA| 精品人妻少妇| 97精品国产97久久久久久免费| 日韩一级片在线| 日本无码久久嗯啊流水| 日屁视频| 操逼福利视频| 97播播| 一道本一区二区三区免费视频| 你懂的在线观看| 国产精品国产三级国产专区53| 日韩bbbb| 黄色国产视频| 亚洲综合视频在线观看| 亚洲综合在线播放| 中文字幕+乱码+中文字幕一区| 国产操逼网| 亚洲中文字幕免费| 日韩欧美成人在线观看| 亚洲AV久久无码| 亚洲一本在线电影av| 操逼五月天| 亚洲成人视频在线免费观看| 国产欧美另类| 国产日韩欧美综合精品在线观看| jiujiuav| 操屄网| 久久久黄色电影| 久久久久久久三级片| 精品欧美无人区乱码毛片| 在线成人av| 中文字字幕在线| 天天干少妇| 国产精品国产自产拍高清AV| 亚洲无码视频免费在线观看| 综合天天| 亚洲色图第一页| 麻豆91精品91久久久停运原因| 嫩BBB槡BBBB槡BBBB撒尿-百度| 国产成人精品免高潮在线人与禽一| 中文字幕亚洲第一| 久久亚洲AV无码午夜麻豆| 韩国高清无码60.70.80 | 中文字幕在线观看完整av| 亚洲热在线观看| A国产| 精品无码一区二区三区蜜桃李宗瑞 | 色色激情网| 国产福利美女网站| 亚洲免费清高| 干片网| 在线看国产| 欧美天天性爱| 蜜臀无码在线| 熟妇人妻久久中文字幕| 日韩中文在线观看| 人人爽夜夜爽| 自拍偷拍av| 黄片网址大全| 成人黄色视频网| 日本操B久久| 亚洲日本黄色视频| 美女91网站色| 在线A片免费观看| 国产欧美在线综合| 高潮喷水在线观看| av黄色在线| 亚洲视频网站在线观看| 九色PORNY丨自拍蝌蚪| 四川少妇搡BBw搡BBBB搡| 91熟女丰满原味| 国产免费久久| 亚洲加勒比在线| 中文无码第一页| 国内自拍视频网| 日韩极品在线观看| 麻豆视频在线免费观看| 亚洲日韩一区| 日韩高清无码中文字幕| 欧美色色色色色| 在线欧美亚洲| 2025无码视频| 西西人体444www| 中文无码熟妇一区二区| 国产97热人人| 无码在线免费观看视频| 亚洲无码一级| 久久精品99久久久久久| 成年免费视频| 91一起草高清资源| 亚洲av高清| 免费AV观看| 欧美成人精品无码网站| 中文字幕有码视频| 国产福利在线观看| 高清AV在线| jzzijzzij亚洲成熟少妇在线播放 黑人猛躁白人BBBBBBBBB | 狠狠狠狠操| 毛片3| 亚洲无码一二三| 精品9999| 自拍偷拍激情视频| 亚洲中文免费观看| 成人亚洲网| 国产成人精品无码片区在线观91| 亚洲秘无码一区二区三区欧美| 国产乱码一区二区三区四区在线| 亚洲自拍电影| 欧美在线免费观看| 四川BBBBBB搡BBBBB| 亚洲A片一区二区三区电影网| 五月天婷婷综合网| 五月丁香色色| 粉嫩小泬BBBB免费看| 亚洲s在线| 久久三级片| 欧美三级片在线| 91人妻人人澡| 久久久成人视频| 欧美群交在线观看| 黄片免费播放| 成人性生活片| 欧美婬乱片A片AAA毛片地址| av在线天堂网| AV黄色网| 欧美视频在线观看一区| 日皮视频在线观看免费| 国内一级黄片| A级片黄色片| 欧美男女日逼视频| 国产操逼网站| 国产精品自拍视频| 中文字幕无码AV| 三级片中文字幕| 亚洲无码免费在线| 俺来也俺就去www色情网| 特级西西人体444.444人体聚色| 午夜精东影业传媒在线观看| 一插综合网| 在线播放内射| 国产av资源网| 91久久人澡人妻人人做人人爽97| 欧美日韩久久久| 三级毛片视频| 91一级片| 艹美女视频| 亚洲一级a片| 自慰喷水在线观看| 三级影片在线观看性| 日本黄色视频。| 蜜臀av一区| 亚洲国产av电影| www.wuma| 国产热视频| 欧美亚洲三级片| 国产91综合一区在线观看| 日本三级片网站在线观看| 中文字幕av高清片,中文在线观看| 一区二区三区四区无码| 不卡一区| 午夜操一操| 91香蕉视频| 91成人做爰A片| 国产精品久久久久久久久| 秋霞午夜成人无码精品| 免费亚洲婷婷| 伊人免费| 黄色一区二区三区| 性爱无码视频| 亚洲中文字幕电影| 熟女导航| 国产天天操| 久久久国产探花视频| 人人摸人人看| 亚洲女与黑人正在播放| 免费无码视频一区二区| 午夜成人福利视频在线观看 | 亚洲不卡| 欧美成人视频大全| 成人电影亚洲天堂| 亚洲一级免费免费在线观看| 亚洲乱码国产乱码精品天美传媒| 国产精品无码永久免费A片| 欧美爱| 做aAAAAA免费视频| 国产婷婷内射| 午夜免费福利视频| 五月精品在线| 国产丰满大乳无码免费播放| 蜜芽视频| 青青草娱乐视频| 激情无码精品| 无码二区三区| 亚州视频在线| 中文字幕av第一页| 日韩欧美精品一区二区| 国产精品乱| 在线午夜福利| 韩国久久久| 91偷拍视频| 91热视频| 99性爱视频| 高清色色女网站| 亚洲国产97| 丁香花中文字幕| 久热这里只有| 午夜视频网| 亚洲天堂在线视频播放| 国产无遮挡又黄又爽又色视频| 上床视频网站| 嫰BBB槡BBBB槡BBBB| 亚欧洲精品在线视频免费观看| 99久久精| 亚洲AV无码成人精品涩涩麻豆| 欧美黄色影院| 午夜操| 日本伊人在线综合视频| 国产精品成人国产乱| 国产黄色一区| 久久久久成人视频| 天天舔天天射| 久久九九热| 国产女主播在线| 国产女同性系列| 亚洲一区二区三区在线| 国产精品一区二区在线| 一区二区三区日韩| AAAA毛片视频| 99久久久国产精品免费蜜臀| 国产又粗又猛又黄又爽无遮挡| 亚洲综合二区| 在线三级片视频| 亚洲一级视频在线观看| 91ThePorn国产在线观看| 91精品国产综合久久蜜臀使用方法| 午夜福利毛片| 日韩超碰| 欧洲黑人成人A版免费视频| 免费看特别黄色视频| 久久A级片| 日本色区| 欧美久久久久久| 你懂的网址在线观看| 九九性爱视频| 最好看的MV中文字幕国语电影| 国产香蕉视频| 无码精品一区二区三区在线观看| 欧美一区二区三区成人| 伊人成人在线视频观看| 我和岳m愉情XXXⅩ视频| 无码中文字幕在线播放| 黄色国产av| 操逼视频大全| 婷婷爱要操| 婷婷乱伦| 91久九九| 青青草日逼视频| 国产主播在线播放| 麻豆免费视频| 成人毛片网| 五月花在线视频| 日韩视频中文字幕在线| 久久免费视频,久久免费视频| 亚洲成人一级片| 欧美精品一二三| 91人妻人人澡人人精品| 韩日无码人妻| 日韩三区在线| 伊人久久香蕉网| 嫩BBB槡BBBB槡BBBB视频-百度| 人妻FrXXeeXXee护士| 久热精品视频| 久久日av| 亚洲国产综合AV在线| 99精品国自产在线| 久久精品www人人爽人人| 欧美视频免费在线观看| 波多野结衣黄色| 一本色道精品久久一区二区三区| 色综合99久久久无码国产精品 | 国产成人a亚洲精品无码| 久久99精品视频| 天堂无码| 西西4444www大胆无吗| 欧美亚洲视频在线观看| 亚洲a√| 亚洲中文字幕免费| 天天操B| 三级久久网| 日韩A毛片| 麻豆md0049免费| 国产精品无码专区| 国产精品不卡| 女人的天堂网| 香蕉成人网站| 无套免费视频欧美| 久久成人免费| MAD033_后宫秘密陶子.| 凸凹翔田千里无码| 亚洲视频无码在线| 在线视频免费观看| 日韩91视频| 永久免费不卡在线观看黄网站| 高潮毛片| 91丨九色丨熟女新版| 在线观看欧美日韩| 一级操逼视频| 亚洲最新AV在线| 久久精品久| 狼人狠狠干| 高清一区二区三区| 特级毛片av| 四川少妇搡bbw搡bbbb| 激情国产视频| 豆花视频| 国产精品色婷婷99久久精品| 操老女人逼视频| 国产三级AV在线| 久久久九九九| 婷婷九月色| 欧美久久一区| 国产一级二级在线观看| 青娱乐自拍视频| 就爱操逼网| 天天天日天天天天天天天日歌词| 九九九免费| 操逼视频在线免费观看| 亚洲AV无码日韩AV无码导航| 日本精品视频一区二区| a视频免费| 91黄色视频在线观看| 色99在线视频| 99视频在线精品| 成人无码三级| 欧美成人A片| 亚洲一级一级黄色| 人人妻人人要| 亚洲AV无码成人精品涩涩麻豆| A一级黄色| 18禁网站免费观看| 视频在线一区| 伊人网成人| 亚洲色视频在线| 中国特级毛片| 17c.白丝喷水自慰| 亚洲猛男操逼欧美国产视频 | 成人久久久久一级大黄毛片中国| 一级生活片| 99久久99久久99久久久99国产| 免费成人视频在线观看| 日韩经典无码| 大乳奶一级婬片A片| 一本久久综合亚洲鲁鲁五月天| 在线日韩AV| 东京热精品视频| 靠逼国产| 天天射天天操天天干| 影音先锋男人资源站| 一区二区三区视屏| 成人做爰黄片视频免费| 中文字幕免费视频在线观看| 日本操逼在线播放| 一区二区三区四区无码在线| 成人在线不卡| 国产在线一区二区| 福利视频免费观看| 日本九九视频| 91人妻成人精品一区二区| 大鸡巴操小逼视频| 97超碰伊人| 91Av视频| 日韩无码中文字| 欧美操BB| 亚洲在线免费视频| h在线网站| 久热精品免费| 亚洲色图15P| 日韩综合精品| 亚洲免费观看高清完整版在va线| 亚洲不卡视频| 麻豆秘在线观看国产| 无码777| 女同二人91| 久久AA| 久久国产性爱| 欧美日韩在线观看一区| 国产99精品视频| 淫揉BBB揉揉揉BBBBB| 在线亚洲日韩| AV先锋资源| 黄色片在线看| www.jiujiujiu| 国产激情啪啪| 国产在线观看免费成人视频| 操逼123首页| 一区二区三区成人电影| 91人妻人人澡人人爽人人精吕| 作爱网站| 国产免费操逼视频| 精品久久久999| 俺来操| 欧美黄网站| 中文字幕AV在线免费观看| 中文字幕不卡一区| 亚洲秘AV无码一区二区qq群| 中文字幕欧美激情|