簡單案例淺析JS執行緒機制

YZcxy發表於2019-02-19

故事背景

故事的開始是這樣的,有一個需求,需要將一個List的資料載入到頁面上展示。

需求看上去很簡單對吧,但是由於List資料量巨大,並且需要對List裡的每個物件進行一定的操作。

所以呢,每次都會造成幾秒鐘的瀏覽器假死,這對使用者的體驗簡直是殺傷性的。

//最初的方法
function original(myList){
    for (var i; i < myList.length; i++) {
        //1.myList[i]物件的一系列操作
        doSomething(myList[i]);
        //2.將物件進行DOM載入 addChild(myList[i])
        addChild(myList[i]);
    }
}
複製程式碼

一號案發現場

通過除錯,發現addChild(myList[i])DOM載入是一個非常消耗效能的方法,小弟不才,想到的解決方案就是將List裡面的物件分批進行載入,所以寫下了如下解決方案。

function solutionOne(myList){
    var tempList = new Array();
    for (var i=0; i < myList.length; i++) {
        //1.myList[i]物件的一系列操作
        doSomething(myList[i]);
        tempList.push(myList[i]);
        //2.當積累到100個物件或者遍歷結束的時候進行DOM載入
        if (tempList.length == 100 || (i + 1) == myList.length) {
            //3.將臨時陣列一起進行DOM載入 
            addChilds(tempList);
            tempList = new Array();
        }
    }
}
複製程式碼

但是現在問題就來了,頁面一如既往的假死,好像我的解決方案並沒有起任何的作用。 :cookie:

這是為什麼呢,明明已經分批載入頁面,講道理的話,頁面會一部分一部分的渲染才對呀。

二號案發現場

這時候呢,通過google大佬,我瞭解了JS的執行緒機制的大致原理。 :lollipop:

  • javascript是一門單執行緒語言,任何的併發都是單執行緒的偽裝。

敲黑板敲黑板,牢記這句話就是分析JS執行緒機制的核心。下面看看單執行緒是如何實現併發操作的。

JS與JAVA併發區別

由上圖可以得知,JS在巨集觀角度是多執行緒併發操作,但是微觀角度在一個時間永遠只有一個執行緒在執行。

  • 瀏覽器核心是多執行緒,常駐三大執行緒為:JavaScript引擎執行緒、GUI渲染執行緒、瀏覽器事件觸發執行緒。
JS多執行緒執行機制

簡單描述一下上圖,瀏覽器事件觸發執行緒 會把觸發的事件(例如click,keydown等)新增到 Event Queue 的隊尾,JavaScript引擎執行緒 會通過 Event Loop 不斷處理 Event Queue 裡面的任務,並且可以通過 setTimeout 等方法產生一些非同步任務加入隊尾。然而 JavaScript引擎執行緒GUI渲染執行緒 是互斥的,所以當一個執行緒在執行,另一個會被掛起。

有了上面的理論基礎,理所當然,我將使用setTimeout來優化我的程式碼,於是我寫下了如下解決方案。

function solutionTwo(myList){
    var tempList = new Array();
    for (var i = 0; i < myList.length; i++) {
        //1.myList[i]物件的一系列操作
        doSomething(myList[i]);
        tempList.push(myList[i]);
        //2.當積累到100個物件的時候進行DOM載入
        if (tempList.length == 100 || (i + 1) == myList.length) {
            //3.將臨時陣列一起進行DOM載入 
            setTimeout(function(){addChilds(tempList);},0);
            tempList = new Array();
        }
    }
}
複製程式碼

根據我的推理,陣列會很快遍歷完,然後會有很多回撥函式加入佇列,然後分批渲染。

然而案發現場是慘重的,最後什麼都沒有載入上。 :cake:

找出真凶

是誰造成了二號案發現場呢,如果你是合格的前端工程師,可能很快就找到了真凶。

沒錯,疑犯就是setTimeout。 :candy:

因為setTimeout在將回撥函式加入任務佇列時會檢查任務佇列是否已經包含了這個回撥函式,如果包含則放棄新增,因此就造成了在執行回撥函式的時候tempList已經被重新初始化了,所以什麼都載入不了。

舉個例子:
下面兩個方法將模擬類似進條度0-100的載入過程。

//方法1的顯示結果為:直接顯示100
function myFun1(){
    for (var i = 0; i < 100; i++) {
        setTimeout(function(){
            document.getElementById("messages").innerHTML = i;},0);
    }
}
//方法2的顯示結果為:從0開始顯示,直到100結束
function myFun2(i){
    if (i <= 100){
        document.getElementById("messages").innerHTML = i;
        setTimeout(function(){myFun2(i+1);},0);
    } else {
        return;
    }
}
複製程式碼

故事講到這裡就該結尾了,凶手找到了,也得到了最終的解決方案,對JS的執行緒機制也有了一個粗略的理解,對於JAVA出身的小菜鳥,向全棧webdev又邁出了一步。

最後附上最終解決方案:

function solutionFinally(myList,listIndex){
    if(listIndex < myList.length) {
        var tempList = new Array();
        var number = 0;
        while(listIndex < myList.length && number <= 100) {
            //1.myList[i]物件的一系列操作
            doSomething(myList[i]);
            tempList.push(myList[i]);
            listIndex++;
            number++;
        }
        //2.當積累到100個物件的時候進行DOM載入
        addChilds(tempList);
        setTimeout(function(){solutionFinally(myList,listIndex);},0);
    } else {
        return;
    }
}
複製程式碼

相關文章