前端中的事件队列

为什么js是单线程

js之所以采用单线程,原因是一开始设计的时候不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。比如,假定js同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?在Java中会使用锁来解决这种竞态条件,而js并不想这样来解决。
当然,单线程模型带来了一些问题,主要是新的任务被加在队列的尾部,只有前面的所有任务运行结束,才会轮到它执行。如果有一个任务特别耗时,后面的任务都会停在那里等待,造成浏览器失去响应,又称“假死”。为了避免“假死”,当某个操作在一定时间后仍无法结束,浏览器就会跳出提示框,询问用户是否要强行停止脚本运行。

事件队列

我们先来看下面的代码

   function fn(){
       var a = 1;
       setTimeout(function(){
           var b = 2;
           console.log('b', b);
       }, 0)
       console.log('a', a);
   }
   fn();
   var c = 3;
   console.log('c', c);

按照正常对单线程的理解,输出应该是

   b 2
   a 1
   c 3

然而,代码的执行结果竟然是

   a 1
   c 3
   b 2

为什么setTimeout里面的代码到了最后才执行,原因就是,js的执行是单线程的。而当它遇到了window的setTimeout和setInterval这样的异步任务,js都默默地先不执行这些回调,而是继续向下执行其他js脚本,等到所有js脚本都解析执行完了,再执行回调。
那么有多个回调的时候执行顺序是怎么样的呢?浏览器是多线程的,js执行线程只是它多个线程中的一个。当js的执行线程看到了setTimeout,浏览器马上会调用其他线程把这个函数中的回调扔到浏览器的事件队列中,事件队列是先入先出的队列。那么在js执行线程执行完所有脚本空闲的时候,事件队列中的事件回调,会一个一个被拿出来执行。浏览器有一个内部大消息循环Event Loop(事件循环),会轮询事件队列并处理事件。例如,浏览器当前正在忙于处理onclick事件,这时另外一个事件发生了(如:input onchange),这个异步事件就被放入事件队列等待处理,只有前面的处理完毕了才能执行下一个。

    setTimeout(function(){
        setTimeout(function(){
            console.log(1);
        }, 1000);
        setTimeout(function(){
            console.log(1);
        }, 2000);
        setTimeout(function(){
            console.log(1);
        }, 3000);
        var time = new Date().getTime();
        while(true){
            //这里模拟一个6s的任务
            if(new Date().getTime() - time > 6000){
                break;
            }
        }
    }, 0)

这里会在6秒任务后看到连续的3个1。
而setInterval有所不同,虽然也不是抢占了当前任务放到队首,但是会在当前线程结束后开始添加时间间隔队列:

    setTimeout(function(){
        setInterval(function(){
            var div = document.createElement('div');
            div.innerHTML =  'I am a interval';
            document.body.appendChild(div);
        }, 1000);
        var time = new Date().getTime();
        while(true){
            //这里模拟一个3s的任务
            if(new Date().getTime() - time > 3000){
                var div = document.createElement('div');
                div.innerHTML =  'three seconds task ends';
                document.body.appendChild(div);
                break;
            }
        }
    }, 0);

这里是先进行3秒计时,再每隔1秒在DOM中输出了 ‘I am a interval’。说明setInterval没有抢占当前的线程,在线程结束后开始计时。(这里非常感谢@伊优01的指正)。

ajax

那么ajax呢。ajax的原理完全一样,当js的执行线程发出了ajax请求后,会继续往下执行js脚本。当有响应返回时,注册在xhr对象上的监听事件中的回调处理函数被放进了任务队列中。等当脚本全部执行完了之后,放在事件队列里的回调处理函数才会执行。每次发送ajax,浏览器都会新开一个线程处理,这些线程之间会共享数据。所以当我们想并发发三个ajax请求的时候,在处理回调函数中如果想操作同一个变量,我们并不知道哪个请求会先返回,那么最好借助promise对象来决定回调函数的顺序。

js下载

js在浏览器中需要被下载、解释并执行这三步。在html body标签中的script都是阻塞的,也就是说,顺序下载、解释、执行。比如script1在script2的前面,但script2先下载完了也没有用,因为全部下载完才会开始解释和执行。
尽管Chrome可以实现多线程并行下载外部资源,例如:script file、image、frame等(css比较复杂,在IE中不阻塞下载,但Firefox阻塞下载)。但是,由于js是单线程的,所以尽管浏览器可以并发加快js的下载,但必须依次执行。
还有一点需要注意,因为浏览器在遇到 < body>才会开始呈现内容,所以js脚本最好不放在< head>标签中,因为js的下载,解释和执行都会阻塞住html的解析,页面可能在一开始出现一片空白

你可能感兴趣的:(前端中的事件队列)