衆所周知,Javascript是一個單線程的語言。這意味着,在Javascript中,同一時間只能作一件事情。javascript
這樣的設計有一些優勢,例如簡單,避免了多線程中複雜的狀態同步,寫程序時不用考慮併發訪問。但同時也帶來了一些其餘問題,其中比較突出的一個問題是:代碼邏輯不直觀。因爲Javascript是單線程的,其中只有一個執行序列。因此,在執行異步操做(例如定時,網絡請求這些不能當即完成的操做)時,Javascript運行時不可能在那裏等着操做完成。不然整個運行時都被阻塞在那裏了,致使其餘全部的操做都沒法進行,例如網頁渲染,用戶點擊、滾動頁面等操做。這樣的用戶體驗是很是糟糕的。java
正由於如此,Javascript使用回調函數處理異步操做結果。進行異步操做時傳入一個回調,操做完成以後由Javascript引擎執行這個回調,將結果傳入。慢慢地,Javascript中充斥着大量的回調。過多的使用回調讓一段完整的邏輯被拆分紅了不少片斷,很是不利於閱讀與維護。回調過多的問題在NodeJS中更爲突出,故而出現了Promise
(見個人前一篇博客)和async/await
。git
那麼異步操做完成時,Javascript運行時是怎樣感知到並調用對應的回調函數的呢?github
答案是EventLoop(事件循環)。編程
要了解EventLoop是怎樣運做的,咱們首先須要瞭解Javascript是怎樣處理一個個任務,調用一個個函數的。這就是CallStack(調用棧)所作的事。promise
相信有過其餘語言編程經驗的讀者都據說過CallStack的概念。Javascript中的CallStack相似。瀏覽器
CallStack是一個棧結構,棧的特色是LIFO(後入先出),出棧入棧只會在一端(也就是棧頂)進行。bash
CallStack是用來處理函數調用與返回的。每次調用一個函數,Javascript運行時會生成一個新的調用結構壓入CallStack。而函數調用結束返回時,JavaScript運行時會將棧頂的調用結構彈出。因爲棧的LIFO特性,每次彈出的必然是最新調用的那個函數的結構。網絡
Javascript啓動時,從文件或標準輸入加載程序。加載完成時,Javascript運行時會生成一個匿名的函數,函數體就是輸入的代碼。這個函數就有點相似於C/C++中的main()
函數,是咱們的入口函數。咱們姑且稱之爲<main>
函數。Javascript啓動時,首先調用就是<main>
函數。看下面代碼:多線程
function func1() {
console.log('in function1');
}
function func2() {
func1();
console.log('in function2');
}
function func3() {
func2();
console.log('in function3');
}
func3();
複製代碼
上面代碼很好理解,咱們來看看Javascript是如何運行這段代碼的。
Javascript首先加載代碼,建立一個匿名<main>
包裹這段代碼並調用該函數。<main>
函數執行,依次定義函數func1
、func2
、func3
,而後調用函數func3
。爲func3
建立調用結構並壓棧。函數func3
中調用func2
,爲func2
建立調用結構並壓棧。函數func2
中調用func1
,爲func1
建立調用結構並壓棧。這個過程當中,CallStack的變化以下。
而後,函數func1
執行完成,從棧頂彈出調用結構。而後func2
繼續執行,func2
執行完成後從棧頂彈出其調用結構。而後func3
繼續執行,func3
執行完成後從棧頂彈出其調用結構。這個過程當中,CallStack的變化以下。
固然,我這裏有一個地方不太嚴謹。不知道讀者有沒有注意到,console.log
也是函數函數哦。因此在func1
中調用console.log
時,CallStack上也會有對應的調用堆棧。func2
,func3
中的console.log
調用一樣如此。有興趣的話,能夠本身畫一畫完整的調用流程,這樣能夠加深理解😀。
這裏我推薦你們使用Google Chrome的開發者工具來幫助咱們理解CallStack。下圖是上面代碼在開發者工具中的一步步執行的結果:
Chrome中<main>
稱爲<anonymous>
。
單步執行時,重點觀察右側工具欄中Call Stack一欄的變化。
在CallStack中執行的函數,咱們稱之爲一個task(任務)。
接下來,咱們來思考這樣一個問題:setTimeout
、setInterval
、AJAX
請求這些功能是怎麼實現的?
一位牛人Philip Roberts曾經將V8引擎(Chrome內置的Javascript引擎)源碼下載下來,而後用grep
查找,發現源碼中並無實現這些函數的代碼😂。
那麼這些函數究竟是如何實現的,又是如何與Javascript引擎交互的呢?
答案是:宿主(網頁中指的是瀏覽器,Node中指的是Node引擎)提供實現,並在操做完成時將結果(異步的,會有延遲)放入Javascript引擎的task隊列,由Javascript引擎處理。
EventLoop顧名思義,其實就是Javascript引擎中的一個循環,它就是一個不停地從任務隊列(task queue)中取出任務執行的過程。
咱們前面詳細瞭解了CallStack以及Javascript啓動時是如何處理的。可是<main>
退出後,Javascript引擎就沒事作了嗎?固然不是,有不少任務會不定時的觸發須要Javascript引擎去處理。例如,用戶點擊按鈕,定時器,頁面渲染等。
其實,Javascript引擎中維護着一個任務隊列。當CallStack中沒有任務在執行時,引擎會從任務隊列中取出任務壓入CallStack處理。咱們經過代碼來具體看看(引用jesstelford):
setTimeout(() => {
console.log('hi');
}, 1000);
複製代碼
咱們的Js代碼,call stack,task queue和Web APIs(瀏覽器中實現)關係以下:
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
複製代碼
開始時,代碼未執行,全部都是空的。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <main> | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
複製代碼
開始執行代碼,壓入咱們的<main>
函數。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
> setTimeout(() => { | <main> | | | |
console.log('hi') | setTimeout | | | |
}, 1000) | | | | |
| | | | |
複製代碼
執行第一行代碼,調用函數setTimeout
。咱們前面說過,每一個函數調用都會建立一個新的調用記錄壓到棧上。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | <main> | | | timeout, 1000 |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
複製代碼
setTimeout
執行完成,從棧中移除對應調用記錄。Web APIs
記錄超時和回調,超時機制由瀏覽器實現。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | timeout, 1000 |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
複製代碼
代碼中沒有其餘邏輯,<main>
函數結束,從棧移除調用信息。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | function <-----timeout, 1000 |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
複製代碼
超時時間到了,Web APIs
將回調放入task queue中。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function <---function | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
複製代碼
EventLoop檢測到Javascript沒有任務處理,從task queue中取出任務執行。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function | | | |
> console.log('hi') | console.log | | | |
}, 1000) | | | | |
| | | | |
複製代碼
執行該回調函數,回調函數中又調用了console.log
函數。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | function | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
> hi
複製代碼
console.log
執行完成,輸出"hi"。
[code] | [call stack] | [task queue] | | [Web APIs] |
--------------------|-------------------|--------------| |---------------|
setTimeout(() => { | | | | |
console.log('hi') | | | | |
}, 1000) | | | | |
| | | | |
> hi
複製代碼
回調函數執行完成,CallStack再次爲空。
上面就是setTimeout
的執行過程,從中開始看出EventLoop在幕後作的工做。
下面咱們再來看一段代碼:
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 0);
Promise.resolve()
.then(() => {
console.log("promise1");
})
.then(() => {
console.log("promise2");
});
console.log("end");
複製代碼
這段程序的輸出是什麼?建議你們先思考一下,最好能動筆畫一畫圖😎。
接着上一節的代碼,符合標準(不少舊版本的瀏覽器實現都是不符合標準的,具體參見參考連接)的輸出應該是:
start
end
promise1
promise2
timeout
複製代碼
可是,爲?什?麼?
實際上,Javascript中有另一種隊列。Promise
的回調是被放入這個隊列的。這個隊列叫作microtask queue(微任務隊列),ES6標準中叫Job Queue。microTask的優先級是比task高的,也就是說microtask隊列中的任務要先處理。
EventLoop檢測到當前沒有任務在執行,首先檢查microtask隊列中有沒有須要處理的任務。若是有那麼一個個執行,直到microtask隊列爲空。
microtask隊列中沒有任務了,執行task隊列中的任務。這裏須要注意,**每執行一個task隊列中的任務,就檢查一下microtask隊列狀態。將microtask隊列中全部任務都執行完成以後,再從task隊列中取出任務執行。
下面咱們看看上面那段代碼是怎麼一步步執行的:
(爲方便起見咱們稱setTimeout
回調爲timeoutcb
,稱第一個then
成功回調爲promisecb1
,第二個then
回調爲promisecb2
)
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
--------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | | | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10.}) | | | | | | |
11..then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13.}); | | | | | | |
14. | | | | | | |
15.console.log("end"); | | | | | | |
複製代碼
開始時,call stack、task queue、microtask queue都爲空。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
> 1. console.log("start"); | <main> | | | | | |
2. | console.log | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
複製代碼
程序開始運行,壓入<main>
函數。首先執行第一行代碼,console.log("start")
,將console.log
壓棧。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
> 1. console.log("start"); | <main> | | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
複製代碼
console.log
執行完成,輸出"start"。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | <main> | | | | | |
2. | setTimeout | | | | | |
> 3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
複製代碼
第二行爲空跳過,開始執行第三行代碼,setTimeout
壓棧。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | <main> | timeoutcb <------------------------~~timeoutcb, 0~~|
2. | | | | | | |
> 3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
複製代碼
setTimeout
執行完成,因爲超時是0,因此當即回調當即進入task queue中。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | <main> | timeoutcb | | promisecb1 | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
> 7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
複製代碼
代碼執行到第7行:
首先Promise.resolve
壓棧,執行完成後返回一個Promise
對象。
而後調用該對象的then
方法,該方法壓棧,執行完成後返回一個全新的Promise
對象,咱們稱該對象爲promise1。因爲Promise.resolve
返回對象的狀態爲resolved
,因此promise1
回調直接進入microtask隊列。
接着又執行新對象的then
方法,咱們稱該對象爲promise2。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | <main> | timeout | | promisecb1 | | |
2. | console.log | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
> 15. console.log("end"); | | | | | | |
> start
複製代碼
代碼執行到第15行,console.log
壓棧。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | <main> | timeout | | promisecb1 | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
> 15. console.log("end"); | | | | | | |
> start
> end
複製代碼
console.log("end")
執行完成,輸出"end",出棧。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | | timeout | | promisecb1 | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
> 15. console.log("end"); | | | | | | |
> start
> end
複製代碼
<main>
函數沒有邏輯須要執行了,出棧。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | promisecb1 | timeout | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
> 15. console.log("end"); | | | | | | |
> start
> end
複製代碼
EventLoop檢查到microtask隊列中有任務須要執行,將promisecb1
取出壓入call stack。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | | timeout | | promisecb2 | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
> end
> promise1
複製代碼
promisecb1
執行完成,輸出"promise1",而且返回undefined
。(其實在這裏還有一個console.log
壓棧出棧的過程,我就不畫了,下同)
在深刻理解Javascript之Promise中看到,若是返回一個值,那麼對象馬上變爲resolved
。因此第二個then
的回調須要安排執行,進入microtask隊列。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | promisecb2 | timeout | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
> end
> promise1
複製代碼
EventLoop檢測到call stack中沒有正在執行的任務,同時microtask隊列不爲空。從microtask隊列取出任務壓入call stack。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | | timeout | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
> end
> promise1
> promise2
複製代碼
promisecb2
執行完成,輸出"promise2"。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | timeoutcb | | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
> end
> promise1
> promise2
複製代碼
接着,EventLoop檢測到call stack和microtask隊列都爲空,從task隊列中取出timeoutcb
壓入棧。
[code] | [call stack] | [task queue] | | [microtask queue] | | [Web APIs] |
---------------------------------|-------------------|--------------| |-------------------| |---------------|
1. console.log("start"); | | | | | | |
2. | | | | | | |
3. setTimeout(() => { | | | | | | |
4. console.log("timeout"); | | | | | | |
5. }, 0); | | | | | | |
6. | | | | | | |
7. Promise.resolve() | | | | | | |
8. .then(() => { | | | | | | |
9. console.log("promise1"); | | | | | | |
10. }) | | | | | | |
11. .then(() => { | | | | | | |
12. console.log("promise2"); | | | | | | |
13. }); | | | | | | |
14. | | | | | | |
15. console.log("end"); | | | | | | |
> start
> end
> promise1
> promise2
> timeout
複製代碼
timeoutcb
執行完成,輸出"timeout"。
經過這篇文章,咱們瞭解到Javascript時如何經過call stack來處理函數的調用與返回的。setTimeout
等異步機制實際上是宿主提供實現,並在異步操做完成負責將回調放入任務隊列,最後由EventLoop在適合的時機取出壓入call stack實際執行。
咱們還看到了另一種隊列——microtask隊列。該隊列中存放的通常是優先級較高的任務,例如Promise
的回調處理函數。
每當call stack中沒有正在執行的任務時,EventLoop會優先從microtask隊列中取出任務執行,當該隊列爲空時纔會從task隊列取。
在使用一門框架或語言時,對因而否須要瞭解底層運做機制和原理,每每會有比較大的爭論。有人說,我不瞭解內部原理一樣能夠寫出好程序,那爲何還須要花時間去研究呢? 對此,我以爲了解底層原理仍是很是有必要的。有下面幾個好處:
可讓咱們看到全貌,瞭解整個系統是如何運做的。
底層原理大可能是相通的,例如幾乎全部語言的函數調用底層都是利用CallStack來實現的。學會了Javascript的CallStack運做機制,在學習其餘語言的相關概念時每每能事半功倍。
瞭解底層可讓咱們心中有數,明白什麼事情能作,什麼事情不能作。例如Javascript是單線程的,咱們寫代碼時必定不能讓線程阻塞了😀。
瞭解底層可讓咱們更好的優化代碼。當程序性能出現瓶頸時,能夠更快地定位問題。