解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!
解決異步問題——promise、async/await
- 一、單線程和異步
- 1、單線程是什么
- 2、為什么需要異步
- 3、使用異步的場景
- 二、promise
- 1、promise的三種狀態(tài)
- 2、三種狀態(tài)的表現(xiàn)和變化
- (1)狀態(tài)的變化
- (2)狀態(tài)的表現(xiàn)
- 3、then和catch對狀態(tài)的影響(重要)
- 4、then和catch的鏈式調(diào)用(常考)
- 三、async/await
- 1、引例闡述
- 2、async和await
- 3、async/await和promise的關(guān)系
- 4、異步的本質(zhì)
- 5、場景題
- (1)async/await語法
- (2)async/await的順序問題
- 四、寫在最后
我們平常在寫 js 中,經(jīng)常只管程序能跑就行,但很少去深究它的原理。更別說還有一個同步和異步的問題了。因此,程序往往有時候出現(xiàn)莫名其妙的卡死或者有時候執(zhí)行順序達不到我們想要的結(jié)果時自己都不知道往哪里找錯。下面的這篇文章中,將講解同步和異步的問題,以及如何解決異步問題的promise、async/await方法。
在看本文之前,建議大家先對 event loop 有一個了解。可以先看我的上一篇文章了解event loop以及微任務宏任務的相關(guān)知識,這樣到后面看本文的 async/await 的時候會更友好一些。
下面開始進行本文的講解。
一、單線程和異步
1、單線程是什么
(1) JS 是單線程語言,只能同時做一件事情
- 所謂單線程,就是只能同時做一件事情,多一件都不行,這就是單線程。
(2) 瀏覽器和 nodejs 已支持 JS 啟動進程,如 Web Worker
(3) JS 和 DOM 渲染共用同一個線程,因為 JS 可修改 DOM 結(jié)構(gòu)
- JS 可以修改 DOM 結(jié)構(gòu),使得它們必須共用同一個線程,這間接算是一件迫不得已的事情。
- 所以當 DOM 在渲染時 JS 必須停止執(zhí)行,而 JS 執(zhí)行過程 DOM 渲染也必須停止。
2、為什么需要異步
當程序遇到網(wǎng)絡(luò)請求或定時任務等問題時,這個時候會有一個等待時間。
假設(shè)一個定時器設(shè)置10s,如果放在同步任務里,同步任務會阻塞代碼執(zhí)行,我們會等待10s后才能看到我們想要的結(jié)果。1個定時器的等待時間可能還好,如果這個時候是100個定時器呢?我們總不能等待著1000s的時間就為了看到我們想要的結(jié)果吧,這幾乎不太現(xiàn)實。
那么這個時候就需要異步,通過異步來讓程序不阻塞代碼執(zhí)行,靈活執(zhí)行程序。
3、使用異步的場景
(1)異步的請求,如ajax圖片加載
//ajax console.log('start'); $.get('./data1.json', function (data1) {console.log(data1); }); console.log('end');(2)定時任務,如setTimeout、setInterval
//setTimeout console.log(100); setTimeout(fucntion(){console.log(200); }, 1000); console.log(300); //setInterval console.log(100); setInterval(fucntion(){console.log(200); }, 1000); console.log(300);二、promise
早期我們在解決異步問題的時候,基本上都是使用callback回調(diào)函數(shù)的形式 來調(diào)用的。形式如下:
//獲取第一份數(shù)據(jù) $.get(url1, (data1) => {console.log(data1);//獲取第二份數(shù)據(jù)$.get(url2, (data2) => {console.log(data2);//獲取第三份數(shù)據(jù)$.get(url3, (data3) => {console.log(data3);//還可以獲取更多數(shù)據(jù)});}); });從上述代碼中可以看到,早期在調(diào)用數(shù)據(jù)的時候,都是一層套一層, callback 調(diào)用 callback ,仿佛深陷調(diào)用地獄一樣,數(shù)據(jù)也被調(diào)用的非常亂七八糟的。所以,因為 callback 對開發(fā)如此不友好,也就有了后來的 promise 產(chǎn)生, promise 的出現(xiàn)解決了 callback hell 的問題。
用一段代碼先來了解一下 Promise 。
function getData(url){return new Promise((resolve, reject) => {$.ajax({url,success(data){resolve(data);},error(err){reject(err);}});}); }const url1 = '/data1.json'; const url2 = '/data2.json'; const url3 = './data3.json'; getData(url1).then(data1 => {console.log(data1);return getData(url2); }).then(data2 => {console.log(data2);return getData(url3); }).then(data3 => {console.log(data3); }).catch(err => console.error(err));大家可以看到,運用了 promise 之后,代碼不再是一層套一層,而是通過 .then 的方式來對數(shù)據(jù)進行一個獲取,這在寫法上就已經(jīng)美觀了不少。那 promise 究竟是什么呢?接下來開始進行講解。
1、promise的三種狀態(tài)
- pending :等待狀態(tài),即在過程中,還沒有結(jié)果。比如正在網(wǎng)絡(luò)請求,或定時器沒有到時間。
- fulfilled :滿足狀態(tài),即事件已經(jīng)解決了,并且成功了;當我們主動回調(diào)了 fulfilled 時,就處于該狀態(tài),并且會回調(diào) then 函數(shù)。
- rejected :拒絕狀態(tài),即事件已經(jīng)被拒絕了,也就是失敗了;當我們主動回調(diào)了 reject 時,就處于該狀態(tài),并且會回調(diào) catch 函數(shù)。
總結(jié):
- 只會出現(xiàn)pending → fulfilled,或者pending → rejected 狀態(tài),即要么成功要么失敗;
- 不管是成功的狀態(tài)還是失敗的狀態(tài),結(jié)果都不可逆。
2、三種狀態(tài)的表現(xiàn)和變化
(1)狀態(tài)的變化
promise 主要有以上三種狀態(tài), pending 、 fulfilled 和 rejected 。當返回一個 pending 狀態(tài)的 promise 時,不會觸發(fā) then 和 catch 。當返回一個 fulfilled 狀態(tài)時,會觸發(fā) then 回調(diào)函數(shù)。當返回一個 rejected 狀態(tài)時,會觸發(fā) catch 回調(diào)函數(shù)。那在這幾個狀態(tài)之間,他們是怎么變化的呢?
1)演示1
先來看一段代碼。
const p1 = new Promise((resolved, rejected) => {});console.log('p1', p1); //pending在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p1 函數(shù)里面沒有內(nèi)容可以執(zhí)行,所以一直在等待狀態(tài),因此是 pending 。
2)演示2
const p2 = new Promise((resolved, rejected) => {setTimeout(() => {resolved();}); });console.log('p2', p2); //pending 一開始打印時 setTimeout(() => console.log('p2-setTimeout', p2)); //fulfilled在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p2 一開始打印的是 pending 狀態(tài),因為它沒有執(zhí)行到 setTimeout 里面。等到后續(xù)執(zhí)行 setTimeout 時,才會觸發(fā)到 resolved 函數(shù),觸發(fā)后返回一個 fulfilled 狀態(tài) promise 。
3)演示3
const p3 = new Promise((resolved, rejected) => {setTimeout(() => {rejected();}); });console.log('p3', p3); setTimeout(() => console.log('p3-setTimeout', p3)); //rejected在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p3 一開始打印的是 pending 狀態(tài),因為它沒有執(zhí)行到 setTimeout 里面。等到后續(xù)執(zhí)行 setTimeout 時,同樣地,會觸發(fā)到 rejected 函數(shù),觸發(fā)后返回一個 rejected 狀態(tài)的 promise 。
看完 promise 狀態(tài)的變化后,相信大家對 promise 的三種狀態(tài)分別在什么時候觸發(fā)會有一定的了解。那么我們接下來繼續(xù)看 promise 狀態(tài)的表現(xiàn)。
(2)狀態(tài)的表現(xiàn)
- pending 狀態(tài),不會觸發(fā) then 和 catch 。
- fulfilled 狀態(tài),會觸發(fā)后續(xù)的 then 回調(diào)函數(shù)。
- rejected 狀態(tài),會觸發(fā)后續(xù)的 catch 回調(diào)函數(shù)。
我們來演示一下。
1)演示1
const p1 = Promise.resolve(100); //fulfilled console.log('p1', p1); p1.then(data => {console.log('data', data); }).catch(err => {console.error('err', err); });在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p1 調(diào)用 promise 中的 resolved 回調(diào)函數(shù),此時執(zhí)行時, p1 屬于 fulfilled 狀態(tài), fulfilled 狀態(tài)下,只會觸發(fā) .then 回調(diào)函數(shù),不會觸發(fā) .catch ,所以最終打印出 data 100 。
2)演示2
const p2 = Promise.reject('404'); //rejected console.log('p2', p2); p2.then(data => {console.log('data2', data); }).catch(err => {console.log('err2', err); })在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p2 調(diào)用 promise 中的 reject 回調(diào)函數(shù),此時執(zhí)行時, p1 屬于 reject 狀態(tài), reject 狀態(tài)下,只會觸發(fā) .catch 回調(diào)函數(shù),不會觸發(fā) .then ,所以最終打印出 err2 404 。
對三種狀態(tài)有了基礎(chǔ)了解之后,我們來深入了解 .then 和 .catch 對狀態(tài)的影響。
3、then和catch對狀態(tài)的影響(重要)
- then 正常返回 fulfilled ,里面有報錯則返回 rejected ;
- catch 正常返回 fulfilled ,里面有報錯則返回 rejected 。
我們先來看第一條規(guī)則: then 正常返回 fulfilled ,里面有報錯則返回 rejected 。
1)演示1
const p1 = Promise.resolve().then(() => {return 100; }) console.log('p1', p1); //fulfilled狀態(tài),會觸發(fā)后續(xù)的.then回調(diào) p1.then(() => {console.log('123'); });在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p1 調(diào)用 promise 中的 resolve 回調(diào)函數(shù),此時執(zhí)行時, p1 正常返回 fulfilled , 不報錯,所以最終打印出 123 。
2)演示2
const p2 = Promise.resolve().then(() => {throw new Error('then error'); }); console.log('p2', p2); //rejected狀態(tài),觸發(fā)后續(xù).catch回調(diào) p2.then(() => {console.log('456'); }).catch(err => {console.error('err404', err); });在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p2 調(diào)用 promise 中的 resolve 回調(diào)函數(shù),此時執(zhí)行時, p2 在執(zhí)行過程中,拋出了一個 Error ,所以,里面有報錯則返回 rejected 狀態(tài) , 所以最終打印出 err404 Error: then error 的結(jié)果。
我們再來看第二條規(guī)則: catch 正常返回 fulfilled ,里面有報錯則返回 rejected 。
1)演示1(需特別謹慎! !)
const p3 = Promise.reject('my error').catch(err => {console.error(err); }); console.log('p3', p3); //fulfilled狀態(tài),注意!觸發(fā)后續(xù).then回調(diào) p3.then(() => {console.log(100); });在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p3 調(diào)用 promise 中的 rejected 回調(diào)函數(shù),此時執(zhí)行時, p3 在執(zhí)行過程中,正常返回了一個 Error ,這個點需要特別謹慎!!這看起來似乎有點違背常理,但對于 promise 來說,不管時調(diào)用 resolved 還是 rejected ,只要是正常返回而沒有拋出異常,都是返回 fulfilled 狀態(tài)。所以,最終 p3 的狀態(tài)是 fulfilled 狀態(tài),且因為是 fulfilled 狀態(tài),之后還可以繼續(xù)調(diào)用 .then 函數(shù)。
2)演示2
const p4 = Promise.reject('my error').catch(err => {throw new Error('catch err'); }); console.log('p4', p4); //rejected狀態(tài),觸發(fā).catch回調(diào)函數(shù) p4.then(() => {console.log(200); }).catch(() => {console.log('some err'); });在以上的這段代碼中,控制臺打印結(jié)果如下。
在這段代碼中, p4 依然調(diào)用 promise 中的 reject 回調(diào)函數(shù),此時執(zhí)行時, p4 在執(zhí)行過程中,拋出了一個 Error ,所以,里面有報錯則返回 rejected 狀態(tài) , 此時 p4 的狀態(tài)為 rejected ,之后觸發(fā)后續(xù)的 .catch 回調(diào)函數(shù)。所以最終打印出 some err 的結(jié)果。
4、then和catch的鏈式調(diào)用(常考)
學習完以上知識后,我們通過幾道題來再總結(jié)回顧一下。
第一題:
Promise.resolve().then(() => {console.log(1); }).catch(() => {console.log(2); }).then(() => {console.log(3); });這道題打印的是 1 和 3 ,因為調(diào)用的是 promise 的 resolve 函數(shù),所以后續(xù)不會觸發(fā) .catch 函數(shù)。
第二題:
Promise.resolve().then(() => {console.log(1);throw new Error('error'); }).catch(() => {console.log(2); }).then(() => {console.log(3); });這道題打印的是 1 和 2 ,雖然調(diào)用的是 promise 的 resolve 函數(shù),但是中間拋出了一個異常,所以此時 promise 變?yōu)?rejected 狀態(tài),所以后續(xù)不會觸發(fā) .then 函數(shù)。
第三題:
Promise.resolve().then(() => {console.log(1);throw new Error('error'); }).catch(() => {console.log(2); }).catch(() => { //這里是catchconsole.log(3); });這道題打印的是 1 和 2 和 3 ,跟第二題一樣,中間拋出了一個異常,所以此時 promise 變?yōu)?rejected 狀態(tài),所以后續(xù)只觸發(fā) .catch 函數(shù)。
三、async/await
現(xiàn)代 js 的異步開發(fā),基本上被 async 和 await 給承包和普及了。雖然說 promise 中的 .then 和 .catch 已經(jīng)很簡潔了,但是 async 更簡潔,它可以通過寫同步代碼來執(zhí)行異步的效果。如此神奇的 async 和 await 究竟是什么呢?讓我們一起來一探究竟吧!
1、引例闡述
先用一個例子來展示 promise 和 async/await 的區(qū)別。假設(shè)我們現(xiàn)在要用異步來實現(xiàn)加載圖片。
(1) 如果用 promise 的 .then 和 .catch 實現(xiàn)時,代碼如下:
function loadImg(src){const picture = new Promise((resolve, reject) => {const img = document.createElement('img');img.onload = () => {resolve(img);}img.onerror = () => {const err = new Error(`圖片加載失敗 ${src}`);reject(err);}img.src = src;})return picture; }const url1 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58'; const url2 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=614367910,2483275034&fm=58';loadImg(url1).then(img1 => {console.log(img1.width);return img1; //普通對象 }).then(img1 => {console.log(img1.height);return loadImg(url2); //promise 實例 }).then(img2 => {console.log(img2.width);return img2; }).then(img2 => {console.log(img2.height); }).catch(ex => console.error(ex));(2) 如果用 async 實現(xiàn)時,代碼如下:
function loadImg(src){const picture = new Promise((resolve, reject) => {const img = document.createElement('img');img.onload = () => {resolve(img);}img.onerror = () => {const err = new Error(`圖片加載失敗 ${src}`);reject(err);}img.src = src;})return picture; }const url1 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58'; const url2 = 'https://dss2.bdstatic.com/8_V1bjqh_Q23odCf/pacific/1990552278.jpg';!(async function () {// img1const img1 = await loadImg(url1);console.log(img1.width, img1.height);// img2const img2 = await loadImg(url2);console.log(img2.width, img2.height); })();大家可以看到,如果用第二種方式的話,代碼要優(yōu)雅許多。且最關(guān)鍵的是,通過 async 和 await ,用同步代碼就可以實現(xiàn)異步的功能。接下來我們開始來了解 async 和 await 。
2、async和await
- 背景:解決異步回調(diào)問題,防止深陷回調(diào)地獄 Callback hell ;
- Promise :Promise then catch 是鏈式調(diào)用,但也是基于回調(diào)函數(shù);
- async/await :async/await 是同步語法,徹底消滅回調(diào)函數(shù),是消滅異步回調(diào)的終極武器。
3、async/await和promise的關(guān)系
(1) async/await 與 promise 并不互斥,兩者相輔相成。
(2) 執(zhí)行 async 函數(shù),返回的是 promise 對象。
(3) await 相當于 promise 的 then 。
(4) try…catch 可以捕獲異常,代替了 promise 的 catch 。
接下來我們來一一(2)(3)(4)演示這幾條規(guī)則。
第一條規(guī)則: 執(zhí)行 async 函數(shù),返回的是 promise 對象。
async function fn1(){return 100; //相當于return promise.resolve(100); }const res1 = fn1(); //執(zhí)行async函數(shù),返回的是一個Promise對象 console.log('res1', res1); //promise對象 res1.then(data => {console.log('data', data); //100 });在以上的這段代碼中,控制臺打印結(jié)果如下。
大家可以看到,第一個 res1 返回的是一個 promise 對象,且此時 promise 對象的狀態(tài)是 fulfilled 狀態(tài),所以可以調(diào)用后續(xù)的 .then 并且打印出 data 100 。
第二條規(guī)則: await 相當于 promise 的 then 。
!(async function (){const p1 = Promise.resolve(300);const data = await p1; //await 相當于 promise的thenconsole.log('data', data); //data 300 })();!(async function () {const data1 = await 400; //await Promise.resolve(400)console.log('data1', data1); //data1 400 })();在以上的這段代碼中,控制臺打印結(jié)果如下。
大家可以看到, p1 調(diào)用 resolve 回調(diào)函數(shù),所以此時 p1 屬于 fulfilled 狀態(tài),之后 const data = await p1 中的await,相當于 promise 的 then ,又因為此時 p1 屬于 fulfilled 狀態(tài),所以可以對 .then 進行調(diào)用,于是輸出 data 300 。同理在第二段代碼中, await 400 時, 400 即表示 Promise.resolved(400) ,因此屬于 fulfilled 狀態(tài),隨后調(diào)用 .then ,打印出 data1 400 結(jié)果。
再來看一段代碼:
!(async function (){const p2 = Promise.reject('err1');const res = await p4; //await 相當于 promise的thenconsole.log('res', res); //不打印 })();在以上的這段代碼中,控制臺打印結(jié)果如下。
大家可以看到, p2 調(diào)用 reject 回調(diào)函數(shù),所以此時 p2 屬于 reject 狀態(tài)。但因為await是觸發(fā) promise 中的 .then ,所以此時 res 不會被觸發(fā),于是后續(xù)不會對await進行操作,控制臺也就不對 console.log('res', res); 進行打印。
第三條規(guī)則: try…catch 可以捕獲異常,代替了 promise 的 catch 。
!(async function () {const p3 = Promise.reject('err1'); //rejected 狀態(tài)try{const res = await p3;console.log(res);}catch(ex){console.error(ex); //try…catch 相當于 promise的catch} })();在以上的這段代碼中,控制臺打印結(jié)果如下。
大家可以看到, p3 調(diào)用 reject 回調(diào)函數(shù),所以此時 p3 屬于 rejected 狀態(tài),因此它不會執(zhí)行 try 的內(nèi)容,而是去執(zhí)行 catch 的內(nèi)容, try…catch 中的 catch 就相當于 promise 中的 catch ,且此時 p3 屬于 rejected 狀態(tài),因此執(zhí)行 catch ,瀏覽器捕獲到異常,報出錯誤。
4、異步的本質(zhì)
從上面的分析中,不管是 promise 還是 async/await ,都是解決異步問題。但是呢,異步的本質(zhì)還是解決同步的問題,所以,異步的本質(zhì)是:
- async/await 是消滅異步回調(diào)的終極武器;
- JS 是單線程的,需要有異步,需要基于 event loop ;
- async/await 是一個語法糖,但是這顆糖非常好用!!
我們來看兩道 async/await 的順序問題,回顧 async/await 。
第一題:
async function async1(){console.log('async start'); // 2await async2();//await 的后面,都可以看做是callback里面的內(nèi)容,即異步。//類似event loop//Promise.resolve().then(() => console.log('async1 end'))console.log('async1 end'); // 5 }async function async2(){console.log('async2'); //3 }console.log('script start'); // 1 async1(); console.log('script end'); // 4在以上的這段代碼中,控制臺打印結(jié)果如下。
從上面這段代碼中可以看到,先執(zhí)行同步代碼 1 ,之后執(zhí)行回調(diào)函數(shù) async1() ,在回調(diào)函數(shù) async1() 當中,先執(zhí)行同步代碼 2 ,之后遇到 await ,值得注意的是, await 的后面,都可以看作是 callback 里面的內(nèi)容,即異步內(nèi)容,所以,先執(zhí)行 await 中對應的 async2() 里面的內(nèi)容,之后把 await 后面所有的內(nèi)容放置到異步當中。繼續(xù)執(zhí)行 4 ,等到 4 執(zhí)行完時,整個同步代碼已經(jīng)執(zhí)行完,最后,再去執(zhí)行異步的代碼,最終輸出 5 的內(nèi)容。
同樣的方式來來分析第二題。
第二題:
async function async1(){console.log('async1 start'); //2await async2();// 下面三行都是異步回調(diào),callback的內(nèi)容console.log('async1 end'); //5await async3();// 下面一行是回調(diào)的內(nèi)容,相當于異步回調(diào)里面再嵌套一個異步回調(diào)。console.log('async1 end 2'); //7 }async function async2(){console.log('async2'); //3 }async function async3(){console.log('async3'); //6 }console.log('script start'); //1 async1(); console.log('script end'); //4在以上的這段代碼中,控制臺打印結(jié)果如下。
這里就不再進行分析啦!大家可以根據(jù)第一個案例的步驟進行分析。
5、場景題
最后的最后,我們再來做兩道題回顧我們剛剛講過的 async/await 知識點。
(1)async/await語法
async function fn(){return 100; } (async function(){const a = fn(); //?? Promiseconst b = await fn(); //?? 100 })(); (async function(){console.log('start');const a = await 100;console.log('a', a);const b = await Promise.resolve(200);console.log('b', b);const c = await Promise.reject(300); //出錯了,再往后都不會執(zhí)行了console.log('c', c);console.log('end'); })(); //執(zhí)行完畢,打印出哪些內(nèi)容? //start //100 //200(2)async/await的順序問題
async function async1(){console.log('async1 start'); // 2await async2();//await后面的都作為回調(diào)內(nèi)容 —— 微任務console.log('async1 end'); // 6 }async function async2(){console.log('async2'); // 3 }console.log('script start'); // 1setTimeout(function(){ //宏任務 —— setTimeoutconsole.log('setTimeout'); // 8 }, 0);//遇到函數(shù),立馬去執(zhí)行函數(shù) async1();//初始化promise時,傳入的函數(shù)會立刻被執(zhí)行 new Promise(function(resolve){ //promise —— 微任務console.log('promise1'); // 4resolve(); }).then(function(){ //微任務console.log('promise2'); // 7 });console.log('script end'); // 5//同步代碼執(zhí)行完畢(event loop —— call stack 被清空) //執(zhí)行微任務 //(嘗試觸發(fā) DOM 渲染) // 觸發(fā)event loop,執(zhí)行宏任務這里就不再進行一一解析啦!大家可以前面知識點的學習總計再回顧理解。
四、寫在最后
關(guān)于 js 的異步問題以及異步的解決方案問題就講到這里啦!u1s1, promise 和 async/await 在我們?nèi)粘5那岸碎_發(fā)中還是蠻重要的,基本上寫異步代碼時候都會用到 async/await 來解決。啃了16個小時總結(jié)了event loop 和 promise 、async/await 問題,希望對大家有幫助。
同時,里面可能有一些講的不好或者不容易理解的地方也歡迎評論區(qū)評論或私信我交流~
關(guān)注公眾號 星期一研究室 ,不定期分享學習干貨~
如果這篇文章對你有用,記得點個贊加個關(guān)注哦~
總結(jié)
以上是生活随笔為你收集整理的解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在拼多多查看好友列表
- 下一篇: 怎么清理手机里的大文件?