setTimeout&Promise&Async直接的愛恨情仇
定義:setTimeout() 方法用於在指定的毫秒數後呼叫函式或計算表示式。 複製程式碼
語法:
setTimeout(code, milliseconds, param1, param2, ...) setTimeout(function, milliseconds, param1, param2, ...) 複製程式碼
引數 | 描述 |
---|---|
code/function | 必需。要呼叫一個程式碼串,也可以是一個函式 |
milliseconds | 可選。執行或呼叫 code/function 需要等待的時間,以毫秒計。預設為 0。 |
param1, param2, ... | 可選。 傳給執行函式的其他引數(IE9 及其更早版本不支援該引數)。 |
二、setTimeout 初識
第一種
setTimeout(fn1, 1000); setTimeout(fn2, 2000); setTimeout(function fn3(){console.log(3);}, 3000); setTimeout(function (){console.log(4);}, 4000); function fn1(){ console.log(1); } var fn2 = function(){ console.log(2); } //輸出結果如下: // 分別延遲1,2,3,4秒之後輸出 1 2 3 4 複製程式碼
第二種
setTimeout(fn1(), 1000); setTimeout(fn2(), 2000); setTimeout(function fn3(){console.log(3);}(), 3000); setTimeout(function (){console.log(4);}(), 4000); function fn1(){ console.log(1); } var fn2 = function(){ console.log(2); } //輸出結果如下: //直接輸出 1 2 3 4,沒有延遲 複製程式碼
按照定義:setTimeout() 方法用於在指定的毫秒數後呼叫函式或計算表示式。第一種方法在指定毫秒數之後執行,第二種方法沒有在指定毫秒數後執行,而是立刻執行。所以我個人將其分成正規軍setTimeout和雜牌軍setTimeout,方便後面記憶。 正規軍我們在後面詳細講,現在先了解下雜牌軍: 由於setTimeout()的第一個引數是**直接可執行的程式碼**,所以它沒有任何延遲效果,如下: 複製程式碼
setTimeout(console.log(1), 1000); //輸出結果如下: //直接輸出 1 ,沒有延遲 複製程式碼
三、setTimeout 再遇
setTimeout(function(a,b){ console.log(a+b); },1000,4,5); //輸出結果如下: //9 複製程式碼
從第三個引數開始,依次用來表示第一個引數(回撥函式)傳入的引數 一些古老的瀏覽器是不支援,可以用bind或apply方法來解決,如下: 複製程式碼
setTimeout(function(a,b){ console.log(a+b); }.bind(this,4,5),1000); //輸出結果如下: //9 複製程式碼
第一個引數表示將原函式的this繫結全域性作用域,第二個引數開始是要傳入原函式的引數 當呼叫繫結函式時,繫結函式會以建立它時傳入bind()方法的第一個引數作為 this 複製程式碼
四、setTimeout 相知
對於setTimeout()的this問題,網上有很多的文章,我就不班門弄斧了,後面若總結的夠到位了就寫一篇文章介紹下。
console.log(1); setTimeout(function (){ console.log(2); },3000); console.log(3); //輸出結果如下: //1 3 2 複製程式碼
console.log(1); setTimeout(function (){ console.log(2); },0); console.log(3); //輸出結果如下: //1 3 2 複製程式碼
這裡有些同學可能會疑惑,第一段程式碼延遲三秒之後執行輸出1,3,2可以理解,但是第二段程式碼延遲0秒執行為什麼也是會輸出1,3,2呢? 這裡就需要提到“任務佇列”這個概念了,由於JavaScript是一種單執行緒的語言,也就是說同一時間只能做一件事情。但是HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制。 單執行緒意味著,所有的任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等待。 所以設計者將任務分成兩種,一種 **同步任務** ,另一種是 **非同步任務** 。 同步任務是,在主執行緒上排隊執行的任務,只有前一個執行完,才能執行後一個; 非同步任務是,不進入主執行緒,而是進入“任務佇列”的任務,只有“任務佇列”通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。 “任務佇列”除了放置任務事件,還可以放置定時事件。即指定某些程式碼在多少時間之後執行。知道了這些我們基本上就可以解釋上面兩段程式碼為什麼會輸出這樣的結果了。 第一段程式碼,因為setTimeout()將回調函式推遲了3000毫秒之後執行。如果將setTimeout()第二個引數設定為0,就表示當前程式碼執行完以後,立刻執行(0毫秒間隔)指定的回撥函式。所以只有在打印出1和3之後,系統才會執行“任務佇列”中的回撥函式。 總之,setTimeout(fn,0)的含義是,指定某個任務在主執行緒最早可得的空閒時間執行,也就是說,儘可能早得執行。它在"任務佇列"的尾部新增一個事件,因此要等到同步任務和"任務佇列"現有的事件都處理完,才會得到執行。強調一遍:它在"任務佇列"的尾部新增一個事件,記住是尾部,新增到"任務佇列"尾部,所以後最後執行。 HTML5標準規定了setTimeout()的第二個引數的最小值(最短間隔),不得低於4毫秒,如果低於這個值,就會自動增加。在此之前,老版本的瀏覽器都將最短間隔設為10毫秒。 setTimeout()只是將事件插入了"任務佇列",必須等到當前程式碼(執行棧)執行完,主執行緒才會去執行它指定的回撥函式。要是當前程式碼耗時很長,有可能要等很久,所以並沒有辦法保證,回撥函式一定會在setTimeout()指定的時間執行。所以他們有時候不一定會守時的。守時的都是好孩子! 阮一峰老師對任務佇列有詳細的介紹,詳情[戳這裡](http://www.ruanyifeng.com/blog/2014/10/event-loop.html) 複製程式碼
五、setTimeout 相熟
瞭解了上面的內容,我們得拉出來溜溜了,直接上測試題:
console.log(1); setTimeout(fn1, 1000); setTimeout(function(){ console.log(2); },0); setTimeout(console.log(3),2000); function fn1(){ console.log(4); } console.log(5); //輸出結果: //1 3 5 2 4(4會延遲一秒) 複製程式碼
1.先執行主執行緒,打印出1; 2.遇到第一個setTimeout,1秒後執行回撥函式,所以新增到任務佇列; 3.遇到第二個setTimeout,0秒後執行回撥函式,再次新增到任務佇列; 4.遇到第三個setTimeout,這個第一個引數不是回撥函式,而是一個直接可執行的語句,記得我前面講過的這個是個雜牌軍,它不會新增到任務佇列也不會延遲執行而是直接執行,所以打印出3; 5.繼續執行打印出5; 6.第二個setTimeout,由於是0秒延遲所以主執行緒任務結束立刻執行,所以打印出2; 7.最後執行第一個setTimeout,一秒後打印出4. 複製程式碼
上面的試題明白之後我們就可以明白下面的程式碼了:
var timeoutId = setTimeout(function (){ console("hello World"); },1000); clearTimeout(timeoutId); //輸出結果: //不會打印出hello World 複製程式碼
1.先執行主執行緒,遇到setTimeout並且第一個引數是回撥函式,新增到任務佇列,1秒後執行; 2.執行clearTimeout,則還未等到程式碼執行就 取消了定時器,所以不會打印出任何內容。 複製程式碼
下面我們學習下promise
promise
一、promise 初現
ES6 將promise寫進了語言標準,統一了用法,原生提供了Promise物件。 詳細介紹戳這裡阮一峰老師進行了詳細的說明;
這裡我簡單的說下,我後面會使用到的內容: Promise 新建後就會立即執行,然後,then方法接受兩個回撥函式作為引數,將在當前指令碼所有同步任務執行完才會執行。記住這裡then之後的回撥函式才非同步執行的,所以會新增到任務佇列中。 第一個回撥函式是Promise物件的狀態變為resolved時呼叫,第二個回撥函式是Promise物件的狀態變為rejected時呼叫。其中,第二個函式是可選的,不一定要提供。 複製程式碼
二、promise 初識
下面我將以程式碼片段的方式,逐漸看出現的各種面試題,加深大家的理解
console.log(1); new Promise((resolve,reject)=>{ console.log(2); resolve() }).then( ()=>{ console.log(3) },()=>{ console.log(4); }); console.log(5); //輸出結果: //1 2 5 3 複製程式碼
1.先執行主執行緒,打印出1; 2. Promise 新建後就會立即執行,所以打印出2,執行resolve表明執行成功回撥; 3. then的成功執行的是回撥函式,所以是非同步執行,新增到任務佇列之中,暫不執行; 4. 繼續執行主執行緒,打印出5; 5. 主執行緒結束之後執行任務佇列中的回撥函式打印出3 複製程式碼
console.log(1); new Promise((resolve,reject)=>{ console.log(2); reject() }).then( ()=>{ console.log(3) },()=>{ console.log(4); }); console.log(5); //輸出結果: //1 2 5 4 複製程式碼
這個例子同上,只是執行的是非同步的失敗的回撥函式,所以最後一個打印出的是4 複製程式碼
console.log(1); new Promise((resolve,reject)=>{ console.log(2); }).then( ()=>{ console.log(3) }); console.log(4); //輸出結果: //1 2 4 複製程式碼
這個例子中打印出4之後沒有列印3,是因為promise中沒有指定是執行成功回撥還是失敗的回撥所以不會執行then的回撥函式 複製程式碼
console.log(1); new Promise((resolve,reject)=>{ console.log(2); }).then(console.log(3)); console.log(4); //輸出結果: //1 2 3 4 複製程式碼
看到這個有同學可能就懵了,怎麼回事怎麼是1234而不是1243呢,這需要考察同學們是否細心呢,看這裡then中的直接是可執行的語句而不是回撥函式,所以會出現這種情況,非同步任務必須是回撥函式如果不是回撥函式就是同步的了 1.先執行主執行緒,打印出1; 1. Promise 新建後就會立即執行,所以打印出2; 2. then中不是回撥函式而是直接可執行的語句,所以直接執行打印出3; 3. 繼續執行主執行緒,打印出4; 複製程式碼
嘻嘻,看了上面的這些例子相信大家已經對promise理解了不少,所以我們繼續深入看看下面這個例子,輸出的結果是什麼呢?
console.log(1); new Promise((resolve,reject)=>{ console.log(2); resolve(); console.log(3); }).then( ()=>{ console.log(4) }); console.log(5); //輸出結果: //1 2 3 5 4 複製程式碼
大家有沒有寫對呢? 這裡大家的疑問估計就是resolve()之後的console.log(3);這個地方咯 這是因為上面程式碼中,呼叫resolve()以後,後面的console.log(3)還是會執行,並且會首先打印出來。因為立即 resolved 的 Promise 是在本輪事件迴圈的末尾執行,總是晚於本輪迴圈的同步任務。 所以如果想讓,呼叫resolve或reject以後,Promise 的使命完成,後繼操作應該放到then方法裡面,而不應該直接寫在resolve或reject的後面。所以,最好在它們前面加上return語句,這樣就不會有意外。如下: 複製程式碼
console.log(1); new Promise((resolve,reject)=>{ console.log(2); return resolve(); console.log(3); }).then( ()=>{ console.log(4) }); console.log(5); //輸出結果: //1 2 3 5 4 複製程式碼
這樣console.log(3);是不會執行的。 複製程式碼
三、promise&setTimeout
下面我們在來看如果promise&setTimeout同時出現會發生什麼樣的情況呢?如下:
console.log('a'); setTimeout(function() {console.log('b')}, 0); new Promise((resolve, reject) => { for(let i=0; i<10000000; i++) { if(i==999999) { console.log('c'); resolve(); } } console.log('d'); }).then(() => { console.log('e'); }); console.log('f'); //輸出結果: // a c d f e b 複製程式碼
大家是不是有些暈,哈哈哈,彆著急這裡我們得在拓展一點新概念,方便我們理解:事件迴圈、巨集任務和微任務
JavaScript的一大特點就是單執行緒,而這個執行緒中擁有唯一的一個事件迴圈。 一個執行緒中,事件迴圈是唯一的,但是任務佇列可以擁有多個。 任務佇列又分為macro-task(巨集任務)與micro-task(微任務),它們又被稱為task與jobs。 巨集任務(macro-task)大概包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。 微任務(micro-task)大概包括: process.nextTick, Promise, MutationObserver(html5新特性) 事件迴圈的順序,決定了JavaScript程式碼的執行順序。 它從script(整體程式碼)開始第一次迴圈。之後全域性上下文進入函式呼叫棧。 直到呼叫棧清空(只剩全域性),然後執行所有的微任務(micro-task)。 當所有可執行的微任務(micro-task)執行完畢之後。 迴圈再次從巨集任務(macro-task)開始,找到其中一個任務佇列執行完畢,然後再執行所有的微任務(micro-task),這樣一直迴圈下去。 注:本篇使用的巨集任務(macro-task):script(整體程式碼), setTimeout, setInterval;微任務(micro-task): Promise。至於其他的瀏覽器沒有,引用了node.js的API,如: setImmediate、 process.nextTick等,至於他們的執行順序可參考[這篇文章](https://www.jianshu.com/p/12b9f73c5a4f) 複製程式碼
比如上述例子,不同型別的任務會分別進入到他們所屬型別的任務佇列,比如所有setTimeout()的回撥都會進入到setTimeout任務佇列,既巨集任務(macro-task);所有then()回撥都會進入到then佇列,既微任務(micro-task)。
當前的整體程式碼我們可以認為是巨集任務。事件迴圈從當前整體程式碼開始第一次事件迴圈,然後再執行佇列中所有的微任務,當微任務執行完畢之後,事件迴圈再找到其中一個巨集任務佇列並執行其中的所有任務,然後再找到一個微任務佇列並執行裡面的所有任務,就這樣一直迴圈下去。這就是我所理解的事件迴圈。
分析上面例子:
1.首先執行整體程式碼,第一個打印出來a 2.執行到第一個setTimeout時,發現它是巨集任務,此時會新建一個setTimeout型別的巨集任務佇列並派發當前這個setTimeout的回撥函式到剛建好的這個巨集任務佇列中去 3.再執行到new Promise,Promise建構函式中的第一個引數在new的時候會直接執行,因此不會進入任何佇列,所以第三個輸出是c 4.執行完resolve()之後,繼續向後執行,打印出d 5.上面有說到Promise.then是微任務,那麼這裡會生成一個Promise.then型別的微任務佇列,這裡的then回撥會被push進這個佇列中 6.再向後走打印出f 7.第一輪事件迴圈的巨集任務執行完成(整體程式碼看做巨集任務)。此時微任務佇列中只有一個Promise.then型別微任務佇列。巨集任務佇列中也只有一個setTimeout型別的巨集任務佇列。 8.下面執行第一輪事件迴圈的微任務,很明顯,會打印出e,至此第一輪事件迴圈完成 9.開始第二輪事件迴圈:執行setTimeout型別佇列(巨集任務佇列)中的所有任務,只有一個任務,所以打印出b 10.第二輪事件的巨集任務結束,這個事件迴圈結束。 複製程式碼
再來一個你中有我我中有你的超級例子,體驗下到處是坑的試題,嘿嘿;-)
console.log('a'); setTimeout(function () { console.log('b') new Promise(resolve=> { console.log('c') resolve() }).then(()=> { console.log('d') }) },2000); new Promise((resolve,reject)=>{ console.log('e'); resolve(); console.log('f'); }).then(()=>{ console.log('g') }); console.log('h'); new Promise((resolve,reject)=>{ setTimeout(function () { console.log('i'); },0); }).then(console.log('j')); setTimeout(function () { console.log('k') new Promise(resolve=>{ console.log('l') return resolve() console.log('m') }).then(()=>{ console.log('n') }) },1000); console.log('p'); //輸出結果: //a e f h j p g i //延遲1s 輸出:k l n //再延遲1s 輸出:b c d 複製程式碼
1.首先執行整體程式碼,第一個打印出來"a"; 2.執行到第一個setTimeout時,發現它是巨集任務,此時會新建一個setTimeout型別的巨集任務佇列並派發當前這個setTimeout的回撥函式到剛建好的這個巨集任務佇列中去,並且輪到它執行時要延遲2秒後再執行; 3.執行到第一個new Promise,Promise建構函式中的第一個引數在new的時候會直接執行,因此不會進入任何佇列,所以第二個輸出是"e",resolve()之後的語句會繼續執行,所以第三個輸出的是"f",Promise.then是微任務,那麼這裡會生成一個Promise.then型別的微任務佇列,這裡的then回撥會被push進這個佇列中; 4.再執行整體程式碼,第四個打印出來"h"; 5.執行到第一個new Promise,Promise建構函式中的第一個引數在new的時候會直接執行,但是這個是一個setTimeout,發現它是巨集任務,派發它的回撥到上面setTimeout型別的巨集任務佇列中去。後面Promise.then中是一個可執行的程式碼,並不是回撥函式,所以會直接的執行,並不會新增到微任務中去,所以第五個輸出的是:"j"; 6.執行到第二個setTimeout時,發現它是巨集任務,派發它的回撥到上面setTimeout型別的巨集任務佇列中去,但是會延遲1s執行; 7.執行整體程式碼,第六個輸出的是"p"; 8.第一輪事件迴圈的巨集任務執行完成(整體程式碼看做巨集任務)。此時微任務佇列中只有一個Promise.then型別微任務佇列,它裡面有一個任務;巨集任務佇列中也只有一個setTimeout型別的巨集任務佇列; 9.下面執行第一輪事件迴圈的微任務,很明顯,第七個輸出的是:"g"。此時第一輪事件迴圈完成; 10.開始第二輪事件迴圈:執行setTimeout型別佇列(巨集任務佇列)中的所有任務。發現有的有延時有的沒有延時,所以先執行延時最短的巨集任務; 11.執行setTimeout,第八個輸出的是"i"; 12.緊接著執行延遲1s的setTimeout,所以延遲一秒之後第九個輸出的是:"k"; 13.之後遇到new Promise,Promise建構函式中的第一個引數在new的時候會直接執行,因此不會進入任何佇列,所以第十個輸出是"l",之後是一個return語句,所以後面的程式碼不會執行,"m"不會被輸出出來; 14.但這裡發現了then,又把它push到上面已經被執行完的then佇列中去,這裡要注意,因為出現了微任務then佇列,所以這裡會執行該佇列中的所有任務(此時只有一個任務),所以第十一個輸出的是"n"; 15.再延遲1s執行setTimeout,所以延遲二秒之後第十二個輸出的是:"b"; 16.之後遇到new Promise,Promise建構函式中的第一個引數在new的時候會直接執行,因此不會進入任何佇列,所以第十三個輸出是"c",; 17.但這裡又發現了then,又把它push到上面已經被執行完的then佇列中去,這裡要注意,因為出現了微任務then佇列,所以這裡會執行該佇列中的所有任務(此時只有一個任務),所以第十四個輸出的是"d"; 複製程式碼
噗,終於完了,不知道大家有沒有理解呢? 生活就是這樣,你以為度過了一個難關前面就是陽光大道,但現實就是這樣,他會給你再來一個難題,接著看下面的程式碼,嘿嘿嘿~~~
async function async1() { console.log("a"); awaitasync2(); console.log("b"); } asyncfunction async2() { console.log( 'c'); } console.log("d"); setTimeout(function () { console.log("e"); },0); async1(); new Promise(function (resolve) { console.log("x"); resolve(); }).then(function () { console.log("y"); }); console.log('z'); //輸出結果: // d a c x z y b e 複製程式碼
是不是有點傻了,怎麼又出現了async了,別慌別慌且聽我慢慢道來,在說之前還得大家瞭解async,阮一峰老師對此有詳細的介紹,詳情戳這裡
Async
一、async
async的用法,它作為一個關鍵字放到函式前面,用於表示函式是一個非同步函式,因為async就是非同步的意思, 非同步函式也就意味著該函式的執行不會阻塞後面程式碼的執行。
我們先來觀察下async的返回值,請看下面的程式碼:
async function testAsync() { return "hello async"; } const result = testAsync(); console.log(result); //輸出結果: // Promise { 'hello async' } 複製程式碼
看到這裡我們知道了,saync輸出的是一個promise物件
async 函式(包含函式語句、函式表示式)會返回一個 Promise 物件,如果在函式中 return 一個直接量,async 會把這個直接量通過 Promise.resolve() 封裝成 Promise 物件。 複製程式碼
那我們試下沒有返回值會是怎麼樣呢?
async function testAsync() { console.log("hello async"); } const result = testAsync(); console.log(result); //輸出結果: // hello async // Promise { undefined } 複製程式碼
會返回一個為空的promis物件
二、await
從字面意思上看await就是等待,await 等待的是一個表示式,這個表示式的返回值可以是一個promise物件也可以是其他值。
注意到 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 是個運算子,用於組成表示式,await 表示式的運算結果取決於它等的東西,如果它等到的不是一個 Promise 物件,那 await 表示式的運算結果就是它等到的東西。
內容 | 描述 |
---|---|
語法 | [return_value] = await expression; |
表示式(expression) | 一個 Promise 物件或者任何要等待的值。 |
返回值(return_value) | 返回 Promise 物件的處理結果。如果等待的不是 Promise 物件,則返回該值本身 |
但是當遇到await會怎麼執行呢?
async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。 當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句. 複製程式碼
即,
當遇到async函式體內的 `await test();`時候,執行test(),然後得到返回值value(可以是promise也可以是其他值),組成`await value;`,若 value是promise物件時候,此時返回的Promise會被放入到任務佇列中等待,await會讓出執行緒,跳出 async函式,繼續執行後續程式碼;若 value是其他值,只是不會被新增到任務佇列而已,await也會讓出執行緒,跳出 async函式,繼續執行後續程式碼。 複製程式碼
明白了這些,我們分析上面最難的那部分程式碼:
1.首先執行整體程式碼,遇到兩個saync函式,沒有呼叫所以繼續向下執行,所以第一個輸出的是:"d"; 2.執行到第一個setTimeout時,發現它是巨集任務,此時會新建一個setTimeout型別的巨集任務佇列並派發當前這個setTimeout的回撥函式到剛建好的這個巨集任務佇列中去,並且輪到它執行時要立刻執行; 3.遇到async1(), async1函式呼叫,執行async1函式,第二個輸出的是:"a"; 4.然後執行到 await async2(),發現 async2 也是個 async 定義的函式,所以直接執行了“console.log('c')”。所以第三個輸出的是:"c"; 5.同時async2返回了一個Promise,請注意:此時返回的Promise會被放入到任務佇列中等待,await會讓出執行緒,接下來就會跳出 async1函式,繼續往下執行!!! 6.執行到 new Promise,前面說過了promise是立即執行的,所以第四個輸出的是:"x"; 7.然後執行到 resolve 的時候,resolve這個任務就被放到任務佇列中等待,然後跳出Promise繼續往下執行,所以第五個輸出的是:"z"; 8.現在呼叫棧空出來了,事件迴圈就會去任務佇列裡面取任務繼續放到呼叫棧裡面; 9.取到的第一個任務,就是前面 async1 放進去的Promise,執行Promise時候,遇到resolve或者reject函式,這次會又被放到任務佇列中等待,然後再次跳出 async1函式 繼續下一個任務!!! 10.接下來取到的下一個任務,就是前面 new Promise 放進去的 resolve回撥,執行then,所以第六個輸出的是:"y"; 11.呼叫棧再次空出來了,事件迴圈就取到了下一個任務,async1 函式中的 async2返回的promise物件的resolve或者reject函式執行,因為 async2 並沒有return任何東西,所以這個resolve的引數是undefined; 12.此時 await 定義的這個 Promise 已經執行完並且返回了結果,所以可以繼續往下執行 async1函式 後面的任務了,那就是“console.log('b')”,所以第七個輸出的是:"b"; 13.呼叫棧再次的空了出來終於執行setTimeout的巨集任務,所以第八個輸出的是:"e" 複製程式碼
哇(@ο@ ) 哇~,解決了小夥伴們明白沒有,希望大家瞭解了就再也不怕面試這種題目啦! 本想著簡單的寫下面試題的解決步驟沒想到一下子寫了這麼多,耐心讀到這裡的小夥伴都是非常棒的,願你在技術的路上越走越遠!