單線程的js是如何工做的

JavaScript的任務隊列

js在誕生初期就肯定了其單線程的定位,也就是說,全部任務須要排隊,前一個行行執行,前面的執行完,纔會執行後面的。若是前一個任務耗時很長,後一個任務就不得不一直等着。
在討論單線程的js時,咱們先來看看爲何js要是單線程的。讓js變成多線程的不行麼🙄~~
css

爲何js是單線程

  1. js誕生初只是爲了實現一些簡單的表單驗證,也就不必太強大。也許一個項目也就幾十行js代碼。那爲啥如今js愈來愈龐大,瀏覽器廠商不給改改呢?讓咱們來看第二點。
  2. 做爲一個腳本語言,JavaScript的主要用途是與用戶互動,好比操做DOM,那麼問題來了,若是有JavaScript有兩個線程,這裏兩個線程同時改變一個div的背景色,那瀏覽器聽誰的,好像兩個線程都沒問題...
  3. 雖然js是線程的,但瀏覽器是多線程

有個叫 Web Worker 的東西,可讓瀏覽器開出一個Worker 線程,經過接受發送消息和主線程交互,而且和主線程不衝突的執行。web worker的定位是負責須要大量計算的代碼執行線程。它限制了咱們不能訪問 DOM 對象。
html

寫了半天怎麼能沒有代碼呢..

var a = 0
    while (a < 10000000) {
        a++
    }
    console.log(10000000)
    console.log(123)
    // 123
    // 321
複製代碼

看上面這個代碼,先執行while 循環,在打印 a,最後123,這很好理解,js就是單線程,一行一行解釋執行。就算while 要加一個億,後面的代碼也得等着java

事件監聽

那咱們的代碼是否是就沒辦法異步執行了呢?固然不是。咱們的代碼須要承擔一個很是重要的任務就是,完成和用戶的交互。好比你點擊一個按鈕但願它能給你彈出一個彈窗。web

// 來看個很簡單的例子
    const body = document.querySelector('body')
    console.log(1)
    body.addEventListener('click',function(){
        console.log(body) 
    })
    body.addEventListener('click',function(){
        console.log(body)   
    })
    console.log(2)
    console.log(2)
    console.log(2)
    // 下面能夠有無限代碼
    ...
複製代碼

這段代碼。在咱們不點擊頁面的時候,是不會打印body的。這就是事件監聽經過事件監聽咱們能夠實現,一個異步任務。
ajax

寫着寫着感受仍是須要簡單的說一個ur從輸入框到加載完成的主要流程。瀏覽器

  1. 用戶abcd輸入一個url,而後瀏覽器發起DNS查詢請求(dns就是把abcd和真實的ip地址1234 對應起來的服務器)
  2. 找到了1234,瀏覽器就向1234發起創建TCP鏈接(tcp是一些列協議,其中http是它應用層上的一個,感興趣的同窗能夠搜索一下TCP七層模型,也有叫五層的)
  3. 發送HTTP 請求(http攜帶報文給服務器,請求行、請求頭、請求體)
  4. 瀏覽器解析http返回的東西(咱們就說一個html吧)
  5. 瀏覽器解析html從上到下

這裏每一個展開均可以寫N+1盤文章。咱們重點了解一下最後一個,瀏覽器解析html的時候會從上到下。在沒有碰到js代碼以前,瀏覽器會一邊解析css一邊解析dom,最後把他們合併渲染。若是中途遇到了js,瀏覽器會中斷解析dom,去解析js,而後從新解析dom,渲染。
服務器

咱們再重點瞭解一下瀏覽器對js的解析
網絡

遇到script,開啓一個宏任務吧。解析裏面的js,預解析var和function聲明的東西。值得注意的是JavaScript預解析不僅是在一開始。每一個執行上下文都會進行一次預解析。
多線程

function fun () {
        console.log(a)
        var a = 1
    }
    fun() // undefined
複製代碼

爲啥會是undefined,而不是由於找不到a報錯呢?緣由就是js預解析了一次,實際執行的代碼咱們能夠理解爲是這樣的dom

function fun () {
            var a = undefined
            console.log(a)
            a = 1
    }
    fun() // undefined
複製代碼

這個預解析也是咱們能夠在函數定義前執行function的緣由。

fun() // 1
    function fun () {
        var a = 1
        console.log(a) // 1
    }
複製代碼

思考一下一個需求。讓一些前面的代碼在最後執行。這時小明說,很簡單。把它放在最後不就好了嗎😋。小明你出去🤣...

console.log('翻天')

var a = 100000
for(let i in [123, 123, 123, 123]) {
}
while (a > 0) {
    a--
}
alert(a)
alert(a)

var ajax=new XMLHttpRequest();
ajax.onreadystatechange=function(){
	console.log('ajax', ajax.responseText);
}
ajax.open("GET","https://search-merger-ms.juejin.im/v1/search?query=ajax&page=0&raw_result=false&src=web",true);
ajax.send();

// 翻天
// alert(0)
// alert(0)
// ajax, '******'
...
複製代碼

能夠看到最早打印翻天,而後在一行一行執行下去了,最後打印 這個異步ajax返回的結果。

那怎麼辦呢。咱們找到了小明,小明把cosnole.log('翻天')放到了代碼的最後面。執行一下。發現竟然仍是比ajax先執行...

// 這樣就行啦
    setTimeout(() => {
        console.log('翻天')
    }, 0)
    ...
    ...
    ...
    // alert(0)
    // alert(0)
    // ajax, '****'
    // 翻天
    
複製代碼

你們能夠在控制檯執行一下。

那麼爲何設置一個setTimeou 0 就可讓代碼最後執行呢?這是由於在執行中當js碰到了setTimeout、setInterval、Promise這些東西的時候,瀏覽器爲咱們開啓了一個異步的隊列。

瀏覽器的線程

  1. js引擎線程 (解釋執行js代碼、用戶輸入、網絡請求)

  2. GUI線程 (繪製用戶界面,就是咱們剛剛說的解析css和dom的,它和js主線程是互斥的)

  3. http網絡請求線程 (處理ajax的)

  4. 定時觸發器線程 (setTimeout、setInterval)

  5. 瀏覽器事件處理線程 (將click、touch放入事件隊列中)

那爲何setTimeout 比 ajax還後執行呢?

由於瀏覽器從開始執行遇到script會開啓一個宏任務。遇到setTimeout也會開啓一個宏任務。遇到ajax請求開啓的是一個微任務。只有當一個宏任務全部的同步代碼和因此微任務所有執行完畢後,瀏覽器纔會開始下一個宏任務。這裏的setTimeout 屬於下一個宏任務。

難度升級,咱們再思考下面這些代碼

<script>
        console.log(1)
        setTimeout(() => {
            console.log(2)
        }, 0)
        const prom = new Promise(function (ret, rej) {
            console.log(3)
            const ajax = new XMLHttpRequest();
            ajax.onreadystatechange=function(){
            	ret(4)
            }
            ajax.open("GET","https://search-merger-ms.juejin.im/v1/search?query=ajax&page=0&raw_result=false&src=web",true);
            ajax.send();
            console.log(5)
            setTimeout(() => {
                console.log(6)
            }, 0)
        })
        prom.then(res => {
            console.log(res)
            setTimeout(() => {
                console.log(7)
            })
        })
        setTimeout(() => {
            console.log(8)
        })
        console.log(9)
<script/>
複製代碼

看到這段代碼有沒有感受日了狗🐩。思考一下,會打印啥。

// 1 3 5 9 4 2 6 8 7
複製代碼

那麼你的答案是對的嗎?若是是,恭喜你很牛逼😀...

咱們從1到8開始講講爲何會是這個順序,而不是123456789.依次打印呢?
由於js引擎遇到 setTimeout 會開啓一個宏任務,new Promise().then() 是一個微任務,這裏的Promise是屬於 script 這個宏任務的。 執行順序是先進先出。

一個小需求

經過js事件隊列實現這樣一個需求

obj.eat('a') // 立刻打印'a'
obj.stop(3000).eat('a') // 間隔3秒後打印 'a'
複製代碼

思考一下。


讓咱們來看看一個簡單的實現代碼吧

class laz {
        constructor(name) {
            this.tasks = []
            setTimeout(() => {
                this.next()
            }, 0)
        }
        next () {
            let task = this.tasks.shift()
            task && task()
        }
        eat (val) {
            const task = (val => () => {
                console.log(val)
                this.next()
            })(val)
            this.tasks.push(task)
            return this
        }
        stop (time) {
            const task = (time => () => {
                setTimeout(() => {
                    console.log(time)
                    this.next()
                }, time)
            })(time)
            this.tasks.push(task)
            return this
        }
    }    
    const obj = new laz()
    obj.eat('a') // a
    obj.stop(3000).eat('a') // 三秒後 3000 a
複製代碼

這個功能就是利用了js隊列實現了一個簡單的延遲執行.

其實這樣的例子還有不少.

總結:其實一開始只是想寫幾百個字結束戰鬥...結果一寫發現,就如今仍是沒寫全。每一個知識點都牽扯到一大堆相關聯的知識點。每一個展開說均可以說半天!!
我想說的是什麼呢,就是平時咱們在學習中,不要死記硬背,東西是背不完的,咱們的時間和大腦都是有限的,記住索引便可。

說實話這麼一大片長文字,我本身都不想看
最後送上一句話

吾生也有涯,而知也無涯

相關文章
相關標籤/搜索