文章目录
  1. 1. 这就涉及到了JavaScript的核心特性:单线程。
  2. 2. 任务队列与异步
  3. 3. JS的运行机制如下:
  4. 4. 疑问
  5. 5. EventLoop
  6. 6. 定时器

从一到面试题引发的追究

1
2
3
for(var j=0;j<10;j++){
setTimeout(function(){console.log(j)},5000)
}

答案显而易见

为什么会循环打印十个10而不是0到9?
从结果来看,for循环执行完跳出之后,才开始执行setTimeout(所以j才等于10),为什么不是每次迭代都执行一次setTimeout呢?

为什么会循环打印十个10

许多人习惯用第二个问题中的执行结果来回答这个问题:“for循环执行完跳出之后,才开始执行setTimeout,所以才打印了十个10”。

这样的答案,只能说是既应付了自己,又应付了别人。其实,要解答第一个问题,首先要解答的就是第二的问题。

大家都知道,JavaScript在ES6出现以前,是没有块状作用域的,这就意味着, 在for循环中用var定义的变量j,其实是属于全局的,即在全局范围内都可以被访问到,既然如此,那其实整个全局作用域中就只有一个j,每次for循环都i是在更新这个j。

那么现在关键的问题在于,为什么整个for循环会先于setTimeout执行,而不是我们正常理解的,一次迭代执行一次。

这就涉及到了JavaScript的核心特性:单线程。

JavaScript设计的初衷,是浏览器用来与用户进行交互和DOM操作的。这就决定了它必须是单线程的,设想JavaScript同事有两个线程,一个线程在DOM节点添加内容,一个线程删除该节点,浏览器就会出现混乱。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

但是浏览器是多线程的,除了js引擎线程,还有UI渲染线程,http请求线程等等。
多线程共享运行资源,浏览器中js可以操作dom,会影响UI渲染,所以js引擎线程和UI渲染线程是互斥的,当js执行时会阻塞UI的渲染,如alert。

任务队列与异步

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

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

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

那么问题来了,这些异步任务完成后,主线程怎么知道呢?答案就是回调函数

例如setTimeout(function(){console.log(1);},50);浏览器异步执行计时操作,当50ms到了后,会触发定时事件,并且执行栈为空这个时候,就会把回调函数放到任务队列里。整个程序就是通过这样的一个个事件驱动起来的。

所以说,js是一直是单线程的,浏览器才是实现异步的那个家伙。

JS的运行机制如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个”任务队列”。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

所以执行栈中的代码(同步任务),总是在读取”任务队列”(异步任务)之前执行。

疑问

我看到这里的时候 有个疑问 真的是在执行栈为空的时候,才会执行任务队里面吗?

1
2
3
4
5
6
7
8
9
10
setTimeout(function (){console.log('计时器====1s')},1000);
console.time();
for(var i= 0;i<= 999999999;i++)
{
if(i == 999999999)
{
console.log('故意延时');
}
}
console.timeEnd();//延时的时间大约是2.5s

运行结果显而易见(很脑残的疑问,是真的菜)

EventLoop

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。

定时器

除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。
定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。
setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

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

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。
如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

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

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行”任务队列”中的回调函数。
总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在”任务队列”的尾部添加一个事件,因此要等到同步任务和”任务队列”现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。
需要注意的是,setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

文章目录
  1. 1. 这就涉及到了JavaScript的核心特性:单线程。
  2. 2. 任务队列与异步
  3. 3. JS的运行机制如下:
  4. 4. 疑问
  5. 5. EventLoop
  6. 6. 定时器