www.bifa88.com 4

JavaScript能否三十二线程

从setTimeout/setInterval看JS线程

2018/04/19 · JavaScriptwww.bifa88.com,
· setInterval,
settimeout

原文出处:
PalmerYe   

最近项目中遇到了一个场景,其实很常见,就是定时获取接口刷新数据。那么问题来了,假设我设置的定时时间为1s,而数据接口返回大于1s,应该用同步阻塞还是异步?我们先整理下js中定时器的相关知识,再来看这个问题。

初识setTimeout 与 setInterval

先来简单认识,后面我们试试用setTimeout 实现 setInterval 的功能

setTimeout 延迟一段时间执行一次 (Only one)

setTimeout(function, milliseconds, param1, param2, …) clearTimeout()
// 阻止定时器运行 e.g. setTimeout(function(){ alert(“Hello”); }, 3000);
// 3s后弹出

1
2
3
4
5
setTimeout(function, milliseconds, param1, param2, …)
clearTimeout() // 阻止定时器运行
 
e.g.
setTimeout(function(){ alert("Hello"); }, 3000); // 3s后弹出

setInterval 每隔一段时间执行一次 (Many times)

setInterval(function, milliseconds, param1, param2, …) e.g.
setInterval(function(){ alert(“Hello”); }, 3000); // 每隔3s弹出

1
2
3
4
setInterval(function, milliseconds, param1, param2, …)
 
e.g.
setInterval(function(){ alert("Hello"); }, 3000); // 每隔3s弹出

setTimeout和setInterval的延时最小间隔是4ms(W3C在HTML标准中规定);在JavaScript中没有任何代码是立刻执行的,但一旦进程空闲就尽快执行。这意味着无论是setTimeout还是setInterval,所设置的时间都只是n毫秒被添加到队列中,而不是过n毫秒后立即执行。

进程与线程,傻傻分不清楚

为了讲清楚这两个抽象的概念,我们借用阮大大借用的比喻,先来模拟一个场景:

这里有一个大型工厂
工厂里有若干车间,每次只能有一个车间在作业
每个车间里有若干房间,有若干工人在流水线作业

那么:

一个工厂对应的就是计算机的一个CPU,平时讲的多核就代表多个工厂
每个工厂里的车间,就是进程,意味着同一时刻一个CPU只运行一个进程,其余进程在怠工
这个运行的车间(进程)里的工人,就是线程,可以有多个工人(线程)协同完成一个任务
车间(进程)里的房间,代表内存。

再深入点:

车间(进程)里工人可以随意在多个房间(内存)之间走动,意味着一个进程里,多个线程可以共享内存
部分房间(内存)有限,只允许一个工人(线程)使用,此时其他工人(线程)要等待
房间里有工人进去后上锁,其他工人需要等房间(内存)里的工人(线程)开锁出来后,才能才进去,这就是互斥锁(Mutual
exclusion,缩写 Mutex)
有些房间只能容纳部分的人,意味着部分内存只能给有限的线程

再再深入:

如果同时有多个车间作业,就是多进程
如果一个车间里有多个工人协同作业,就是多线程
当然不同车间之间的工人也可以有相互协作,就需要协调机制

JavaScript 单线程

总所周知,JavaScript
这门语言的核心特征,就是单线程(是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个)。这和
JavaScript 最初设计是作为一门 GUI
编程语言有关,最初用于浏览器端,单一线程控制 GUI
是很普遍的做法。但这里特别要划个重点,虽然JavaScript是单线程,但浏览器是多线程的!!!例如Webkit或是Gecko引擎,可能有javascript引擎线程、界面渲染线程、浏览器事件触发线程、Http请求线程,读写文件的线程(例如在Node.js中)。ps:可能要总结一篇浏览器渲染的文章了。

HTML5提出Web
Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

同步与异步,傻傻分不清楚

之前阮大大写了一篇《JavaScript 运行机制详解:再谈Event
Loop》,然后被朴灵评注了,特别是同步异步的理解上,两位大牛有很大的歧义。

同步(synchronous):假如一个函数返回时,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),这就是同步函数。

e.g. alert(‘马上能看到我拉’); console.log(‘也能马上看到我哦’);

1
2
3
e.g.
alert(‘马上能看到我拉’);
console.log(‘也能马上看到我哦’);

异步(asynchronous):假如一个函数返回时,调用者不能得到预期结果,需要通过一定手段才能获得,这就是异步函数。

e.g. setTimeout(function() { // 过一段时间才能执行我哦 }, 1000);

1
2
3
4
e.g.
setTimeout(function() {
    // 过一段时间才能执行我哦
}, 1000);

异步构成要素

一个异步过程通常是这样的:主线程发起一个异步请求,相应的工作线程(比如浏览器的其他线程)接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。

发起(注册)函数 – 发起异步过程
回调函数 – 处理结果

e.g. setTimeout(fn, 1000); //
setTimeout就是异步过程的发起函数,fn是回调函数

1
2
3
e.g.
setTimeout(fn, 1000);
// setTimeout就是异步过程的发起函数,fn是回调函数

通信机制

异步过程的通信机制:工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。

消息队列 Message Queue

一个先进先出的队列,存放各类消息。

事件循环 Event Loop

主线程(js线程)只会做一件事,就是从消息队列里面取消息、执行消息,再取消息、再执行。消息队列为空时,就会等待直到消息队列变成非空。只有当前的消息执行结束,才会去取下一个消息。这种机制就叫做事件循环机制Event
Loop,取一个消息并执行的过程叫做一次循环。www.bifa88.com 1

工作线程是生产者,主线程是消费者。工作线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。

setTimeout(function, 0) 发生了什么

其实到这儿,应该能很好解释setTimeout(function, 0)
这个常用的“奇技淫巧”了。很简单,就是为了将function里的任务异步执行,0不代表立即执行,而是将任务推到消息队列的最后,再由主线程的事件循环去调用它执行。

HTML5 中规定setTimeout 的最小时间不是0ms,而是4ms。

setInterval 缺点

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

setInterval(function, N)

1
setInterval(function, N)

那么显而易见,上面这段代码意味着,每隔N秒把function事件推到消息队列中,什么时候执行?母鸡啊!www.bifa88.com 2

上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some
event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。

综上所述,setInterval有两个缺点:

使用setInterval时,某些间隔会被跳过;
可能多个定时器会连续执行;

链式setTimeout

setTimeout(function () { // 任务 setTimeout(arguments.callee, interval);
}, interval)

1
2
3
4
setTimeout(function () {
    // 任务
    setTimeout(arguments.callee, interval);
}, interval)

警告:在严格模式下,第5版 ECMAScript (ES5) 禁止使用
arguments.callee()。当一个函数必须调用自身的时候, 避免使用
arguments.callee(), 通过要么给函数表达式一个名字,要么使用一个函数声明.

上述函数每次执行的时候都会创建一个新的定时器,第二个setTimeout使用了arguments.callee()获取当前函数的引用,并且为其设置另一个定时器。好处:

在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)
保证定时器间隔(解决缺点二)

So…

回顾最开始的业务场景的问题,用同步阻塞还是异步,答案已经出来了…

PS:其实还有macrotask与microtask等知识点没有提到,总结了那么多,其实JavaScript深入下去还有很多,任重而道远呀。

 

1 赞 收藏
评论

www.bifa88.com 3

单线程

  • .JavaScript是单线程
    javascript是单线程,无论后面加了什么标准,什么操作,都不能改变javascript单线程的本质。原因就是,如果两个线程同时操控dom,那浏览器应该听谁的呢?为了避免这个问题,javascript只能是单线程。

  • 但是浏览器是多线程的,除了js引擎线程,还有UI渲染线程,http请求线程等等。

  • .多线程共享运行资源,浏览器中js可以操作dom,会影响UI渲染,所以js引擎线程和UI渲染线程是互斥的,当js执行时会阻塞UI的渲染,如alert。

  JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行,
我想不少人都深有同感, 例如 [javascript]

异步任务

  • js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个执行栈排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。

  • 同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,后一个同步任务才能执行。

  • 异步任务是指不在主线程、而是在任务队列中的任务。只有当任务队列通知主线程,并且执行栈为空时,该任务队列中的任务才会进入主线程执行。

  setTimeout( function(){ alert(‘你好!’); } , 0);

注意:那么问题来了,这些异步任务完成后,主线程怎么知道呢?

答案就是回调函数。
例如setTimeout(function(){console.log(1);},50);浏览器异步执行计时操作,当50ms到了后,会触发定时事件,这个时候,就会把回调函数放到任务队列里。整个程序就是通过这样的一个个事件驱动起来的。
所以说,js是一直是单线程的,浏览器才是实现异步的那个家伙。

  setInterval( callbackFunction , 100);

事件循环

JS的运行机制如下:
(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个”任务队列”。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
所以执行栈中的代码(同步任务),总是在读取”任务队列”(异步任务)之前执行。
EventLoop
主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event
Loop(事件循环)。

www.bifa88.com 4

6.jpg

  setTimeout( function(){ alert(’你好!’); } , 0);

定时器:

JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成

  setInterval( callbackFunction , 100);

setTimeout()

setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

setTimeout(function (){console.log(2)},1000);

  认为setTimeout中的问候方法会立即被执行,因为这并不是凭空而说,而是JavaScript
API文档明确定义第二个参数意义为隔多少毫秒后,回调方法就会被执行.
这里设成0毫秒,理所当然就立即被执行了.

setInterval()

setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。

var i = 1
  var timer = setInterval(function() {
    console.log(i++);
  }, 1000);

  同理对setInterval的callbackFunction方法每间隔100毫秒就立即被执行深信不疑!

clearTimeout(),clearInterval()

etTimeout和setInterval函数,都返回一个表示计数器编号的整数值,将该整数传入clearTimeout和clearInterval函数,就可以取消对应的定时器。

var id1 = setTimeout(f,1000);
var id2 = setInterval(f,1000);

clearTimeout(id1);
clearInterval(id2);

  但随着JavaScript应用开发经验不断的增加和丰富,有一天你发现了一段怪异的代码而百思不得其解:

运行机制

下面这段代码输出结果是? 为什么?

var a = 1;
setTimeout(function(){
    a = 2;
    console.log(a);//1
}, 0);
var a ;
console.log(a);//3
a = 3;
console.log(a);//2

定时器为异步任务,先挂起,将代码移出本次执行,放入任务队列,等到下一轮Event
Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;即必须等到本次执行的所有代码(同步任务)都执行完,才会执行setTimeout指定的代码(任务队列),先输出1,3,再执行定时器函数,输出2;

var flag = true;
setTimeout(function(){
    flag = false;
},0)
while(flag){}
console.log(flag);

一直输出true;陷入死循环;
定时器为异步任务,先挂起,将代码移出本次执行,放入任务队列,等到下一轮Event
Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;即必须等到本次执行的所有代码(同步任务)都执行完,才会执行setTimeout指定的代码(任务队列),先执行while语句,flag为真,一直循环输出true;
范例:
实现一个节流函数。
一定时间内,重复执行同一函数,以最后一次为准

    function throttle(delay){
        var timer=null;
        return function(){
            clearTimeout(timer);
            timer=setTimeout(function(){
                console.log("hello");
            },delay);
        };
    }
    var fn=throttle(10000);
    fn();
    fn();
    fn();//hello

  [javascript]

  div.onclick = function(){

  setTimeout(
function(){document.getElementById(‘inputField’).focus();}, 0);

  };

  div.onclick = function(){

  setTimeout(
function(){document.getElementById(‘inputField’).focus();}, 0);

  };

  既然是0毫秒后执行,那么还用setTimeout干什么, 此刻,
坚定的信念已开始动摇.

  直到最后某一天 , 你不小心写了一段糟糕的代码:

  [javascript]

  setTimeout( function(){ while(true){} } , 100);

  setTimeout( function(){ alert(‘你好!’); } , 200);

  setInterval( callbackFunction , 200);

  setTimeout( function(){ while(true){} } , 100);

  setTimeout( function(){ alert(’你好!’); } , 200);

  setInterval( callbackFunction , 200);

  第一行代码进入了死循环,但不久你就会发现,第二,第三行并不是预料中的事情,alert问候未见出现,callbacKFunction也杳无音讯!

  这时你彻底迷惘了,这种情景是难以接受的,因为改变长久以来既定的认知去接受新思想的过程是痛苦的,但情事实摆在眼前,对JavaScript真理的探求并不会因为痛苦而停止,下面让我们来展开JavaScript线程和定时器探索之旅!

  出现上面所有误区的最主要一个原因是:潜意识中认为,JavaScript引擎有多个线程在执行,JavaScript的定时器回调函数是异步执行的.

  而事实上的,JavaScript使用了障眼法,在多数时候骗过了我们的眼睛,这里背光得澄清一个事实:

  JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.

  JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化.

  那么单线程的JavaScript引擎是怎么配合浏览器内核处理这些定时器和响应浏览器事件的呢?

  下面结合浏览器内核处理方式简单说明.

  浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步.假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎
线程,界面渲染线程,浏览器事件触发线程,除些以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件,下面通过一
个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的.虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异.

  JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行,
我想不少人都深有同感, 例如

www.bifa88.com 5

  由图可看出,浏览器中的JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可以源自
JavaScript引擎当前执行的代码块,如调用setTimeout添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发
器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线
程关系,这些任务得进行排队,一个接着一个被引擎处理.
上图t1-t2..tn表示不同的时间点,tn下面对应的小方块代表该时间点的任务,假设现在是t1时刻,引擎运行在t1对应的任务方块代码内,在这个时间点内,我们来描述一下浏览器内核其它线程的状态.

  t1时刻:

  GUI渲染线程:

  该线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释
JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为
JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.

  在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被“冻结”了.

  所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.

  GUI事件触发线程:

  JavaScript脚本的执行不影响html元素事件的触发,在t1时间段内,首先是用户点击了一个鼠标键,点击被浏览器事件触发线程捕捉后形成一个鼠
标点击事件,由图可知,对于JavaScript引擎线程来说,这事件是由其它线程异步传到任务队列尾的,由于引擎正在处理t1时的任务,这个鼠标点击事
件正在等待处理.

  定时触发线程:

  注意这里的浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就计不了时,它必须依赖外部来计时并触发定时,所以队列中的定时事件也是异步事件.

  由图可知,在这t1的时间段内,继鼠标点击事件触发后,先前已设置的setTimeout定时也到达了,此刻对JavaScript引擎来说,定时触发线程产生了一个异步定时事件并放到任务队列中,
该事件被排到点击事件回调之后,等待处理.

  同理,
还是在t1时间段内,接下来某个setInterval定时器也被添加了,由于是间隔定时,在t1段内连续被触发了两次,这两个事件被排到队尾等待处理.

  可见,假如时间段t1非常长,远大于setInterval的定时间隔,那么定时触发线程就会源源不断的产生异步定时事件并放到任务队列尾而不管它们是否
已被处理,但一旦t1和最先的定时事件前面的任务已处理完,这些排列中的定时事件就依次不间断的被执行,这是因为,对于JavaScript引擎来说,在
处理队列中的各任务处理方式都是一样的,只是处理的次序不同而已.

  t1过后,也就是说当前处理的任务已返回,JavaScript引擎会检查任务队列,发现当前队列非空,就取出t2下面对应的任务执行,其它时间依此类推,由此看来:

  如果队列非空,引擎就从队列头取出一个任务,直到该任务处理完,即返回后引擎接着运行下一个任务,在任务没返回前队列中的其它任务是没法被执行的.

  相信您现在已经很清楚JavaScript是否可多线程,也了解理解JavaScript定时器运行机制了,下面我们来对一些案例进行分析:

  案例1:setTimeout与setInterval

  [javascript]

  setTimeout(function(){

  /* 代码块… */

  setTimeout(arguments.callee, 10);

  }, 10);

  setInterval(function(){

  /*代码块… */

  }, 10);

  setTimeout(function(){

  /* 代码块… */

  setTimeout(arguments.callee, 10);

  }, 10);

  setInterval(function(){

  /*代码块… */

  }, 10);

  这两段代码看一起效果一样,其实非也,第一段中回调函数内的setTimeout是JavaScript引擎执行后再设置新的setTimeout
定时,
假定上一个回调处理完到下一个回调开始处理为一个时间间隔,理论两个setTimeout回调执行时间间隔>=10ms.第二段自
setInterval设置定时后,定时触发线程就会源源不断的每隔十秒产生异步定时事件并放到任务队列尾,理论上两个setInterval回调执行时
间间隔<=10.

  案例2:ajax异步请求是否真的异步?

  很多同学朋友搞不清楚,既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?

  其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求(参见上图),当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到
JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行
onreadystatechange所设置的函数.

,
我想不少人都深有同感…