简单案例浅析JS线程机制

1,110 阅读4分钟

故事背景

故事的开始是这样的,有一个需求,需要将一个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;
    }
}