您好,登錄后才能下訂單哦!
棧(Stack)
棧是一種遵循后進先出(LIFO)的數據集合,新添加或待刪除的元素都保存在棧的末尾,稱作棧頂,另一端稱作棧底。在棧里,新元素都靠近棧頂,舊元素都接近棧底
感覺說起來并不是很好理解,我們舉個例子,比如有一個乒乓球盒,我們不停的向球盒中放進乒乓球,那么最先放進去的乒乓球一定是在最下面,最后放進去的一定是在最上面,那么如果我們想要把這些球取出來是不是就必須依次從上到下才能拿出來,這個模型就是后進先出,就是我們后進入球盒的球反而最先出來。
棧的概念其實在我們js中十分的重要,大家都知道我們js是一個單線程語言,那么他單線程在哪里呢,就在他的主工作線程,也就是我們常說的執行上下文,這個執行上下文就是棧空間,我們來看一段代碼:
console.log('1');
function a(){
console.log('2');
function b(){
console.log('3')
}
b()
}
a()
復制代碼我們知道函數執行的時候會將這個函數放入到我們的執行上下文中,當函數執行完畢之后會彈出執行棧,那么根據這個原理我們就能知道這段代碼的運行過程是
首先我們代碼執行的時候會有一個全局上下文,此時代碼運行,全局上下文進行執行棧,處在棧底的位置
我們遇到console.log('1'),這個函數在調用的時候進入執行棧,當這句話執行完畢也就是到了下一行的時候我們console這個函數就會出棧,此時棧中仍然只有全局上下文
接著運行代碼,這里注意的是我們遇到的函數聲明都不會進入執行棧,只有當我們的函數被調用被執行的時候才會進入,這個原理和我們執行棧的名字也就一模一樣,接著我們遇到了a();這句代碼這個時候我們的a函數就進入了執行棧,然后進入到我們a的函數內部中,此時我們的函數執行棧應該是 全局上下文 —— a
接著我運行console.log('2'),執行棧變成 全局上下文——a——console,接著我們的console運行完畢,我們執行棧恢復成全局上下文 —— a
接著我們遇到了b();那么b進入我們的執行棧,全局上下文——a——b,
接著進入b函數的內部,執行console.log('3')的時候執行棧為全局上下文——a——b——console,執行完畢之后回復成全局上下文——a——b
然后我們的b函數就執行完畢,然后就被彈出執行棧,那么執行棧就變成全局上下文——a
然后我們的a函數就執行完畢,然后就被彈出執行棧,那么執行棧就變成全局上下文
然后我們的全局上下文會在我們的瀏覽器關閉的時候出棧
我們的執行上下文的執行過程就是這樣,是不是清楚了很多~
通過上面的執行上下文我們可以發現幾個特點:
執行上下文是單線程
執行上下文是同步執行代碼
當有函數被調用的時候,這個函數會進入執行上下文
代碼運行會產生一個全局的上下文,只有當瀏覽器關閉才會出棧
隊列(Queue)
隊列是一種遵循先進先出(FIFO)的數據集合,新的條目會被加到隊列的末尾,舊的條目會從隊列的頭部被移出。
這里我們可以看到隊列和棧不同的地方是棧是后進先出類似于乒乓球盒,而隊列是先進先出,也就是說最先進入的會最先出去。
同樣我們舉個例子,隊列就好比是我們排隊過安檢,最先來到的人排在隊伍的首位,后來的人接著排在隊伍的后面,然后安檢員會從隊伍的首端進行安檢,檢完一個人就放行一個人,是不是這樣的一個隊伍就是先進先出的一個過程。
隊列這里我們就要提到兩個概念,宏任務(macro task),微任務(micro task)。
任務隊列
Js的事件執行分為宏仁務和微任務
宏仁務主要是由script(全局任務),setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
微任務主要是process.nextTick, Promise.then, Object.observer, MutationObserver.
瀏覽器事件環
js執行代碼的過程中如果遇到了上述的任務代碼之后,會先把這些代碼的回調放入對應的任務隊列中去,然后繼續執行主線程的代碼知道執行上下文中的函數全部執行完畢了之后,會先去微任務隊列中執行相關的任務,微任務隊列清空之后,在從宏仁務隊列中拿出任務放到執行上下文中,然后繼續循環。
執行代碼,遇到宏仁務放入宏仁務隊列,遇到微任務放入微任務隊列,執行其他函數的時候放入執行上下文
執行上下文中全部執行完畢后,執行微任務隊列
微任務隊列執行完畢后,再到宏仁務隊列中取出第一項放入執行上下文中執行
接著就不停循環1-3的步驟,這就是瀏覽器環境中的js事件環
//學了上面的事件環 我們來看一道面試題
setTimeout(function () {
console.log(1);
}, 0);
Promise.resolve(function () {
console.log(2);
})
new Promise(function (resolve) {
console.log(3);
});
console.log(4);
//上述代碼的輸出結果是什么???
復制代碼思考思考思考思考~~~
正確答案是3 4 1,是不是和你想的一樣?我們來看一下代碼的運行流程
// 遇到setTimeout 將setTimeout回調放入宏仁務隊列中
setTimeout(function () {
console.log(1);
}, 0);
// 遇到了promise,但是并沒有then方法回調 所以這句代碼會在執行過程中進入我們當前的執行上下文 緊接著就出棧了
Promise.resolve(function () {
console.log(2);
})
// 遇到了一個 new Promise,不知道大家還記不記得我們上一篇文章中講到Promise有一個原則就是在初始化Promise的時候Promise內部的構造器函數會立即執行 因此 在這里會立即輸出一個3,所以這個3是第一個輸入的
new Promise(function (resolve) {
console.log(3);
});
// 然后輸入第二個輸出4 當代碼執行完畢后回去微任務隊列查找有沒有任務,發現微任務隊列是空的,那么就去宏仁務隊列中查找,發現有一個我們剛剛放進去的setTimeout回調函數,那么就取出這個任務進行執行,所以緊接著輸出1
console.log(4);
復制代碼看到上述的講解,大家是不是都明白了,是不是直呼簡單~
那我們接下來來看看node環境中的事件執行環
NodeJs 事件環
瀏覽器的 Event Loop 遵循的是 HTML5 標準,而 NodeJs 的 Event Loop 遵循的是 libuv標準,因此呢在事件的執行中就會有一定的差異,大家都知道nodejs其實是js的一種runtime,也就是運行環境,那么在這種環境中nodejs的api大部分都是通過回調函數,事件發布訂閱的方式來執行的,那么在這樣的環境中我們代碼的執行順序究竟是怎么樣的呢,也就是我們不同的回調函數究竟是怎么分類的然后是按照什么順序執行的,其實就是由我們的libuv所決定的。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
復制代碼我們先來看下這六個任務是用來干什么的
timers: 這個階段執行setTimeout()和setInterval()設定的回調。
pending callbacks: 上一輪循環中有少數的 I/O callback會被延遲到這一輪的這一階段執行。
idle, prepare: 僅內部使用。
poll: 執行 I/O callback,在適當的條件下會阻塞在這個階段
check: 執行setImmediate()設定的回調。
close callbacks: 執行比如socket.on('close', ...)的回調。
我們再來看網上找到的一張nodejs執行圖,我們能看到圖中有六個步驟 ,當代碼執行中如果我們遇到了這六個步驟中的回調函數,就放入對應的隊列中,然后當我們同步人物執行完畢的時候就會切換到下一個階段,也就是timer階段,然后timer階段執行過程中會把這個階段的所有回調函數全部執行了然后再進入下一個階段,需要注意的是我們在每次階段發生切換的時候都會先執行一次微任務隊列中的所有任務,然后再進入到下一個任務階段中去,所以我們就能總結出nodejs的事件環順序
同步代碼執行,清空微任務隊列,執行timer階段的回調函數(也就是setTimeout,setInterval)
全部執行完畢,清空微任務隊列,執行pending callbacks階段的回調函數
全部執行完畢,清空微任務隊列,執行idle, prepare階段的回調函數
全部執行完畢,清空微任務隊列,執行poll階段的回調函數
全部執行完畢,清空微任務隊列,執行check階段的回調函數(也就是setImmediate)
全部執行完畢,清空微任務隊列,執行close callbacks階段的回調函數
然后循環1-6階段
那我們來練練手~~~
// 我們來對著我們的執行階段看看
let fs = require('fs');
// 遇到setTimeout 放入timer回調中
setTimeout(function(){
Promise.resolve().then(()=>{
console.log('then1');
})
},0);
// 放入微任務隊列中
Promise.resolve().then(()=>{
console.log('then2');
});
// i/o操作 放入pending callbacks回調中
fs.readFile('./text.md',function(){
// 放入check階段
setImmediate(()=>{
console.log('setImmediate')
});
// 放入微任務隊列中
process.nextTick(function(){
console.log('nextTick')
})
});
復制代碼首先同步代碼執行完畢,我們先清空微任務,此時輸出then2,然后切換到timer階段,執行timer回調,輸出then1,然后執行i/o操作回調,然后清空微任務隊列,輸出nextTick,接著進入check階段,清空check階段回調輸出setImmediate
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。