#hello,JS:08 setTimeout和任务队列
前言:
之前写过一次被不小心删掉了,幸好思路和参考资料还在,所以赶快写下来。里面涉及了一点点dom事件的操作(但不影响学习)。
一、什么是定时器
JS提供定时执行代码功能,叫做定时器(timer),主要由setTimeout( )和setInterval( )这两个函数来完成。setTimeout( )和setInterval( )是windows的两个全局属性。
二、setTimeout( )
setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
var timerId = setTimeout(func|code, delay)//括号里代表(函数|代码字符串,延迟的时间毫秒数)
先写一个函数,在通过setTimeout调用函数,如:
function f(){
console.log(2)
}
setTimeout(f,1000) //表示在1s之后执行这个函数
或者
setTimeout(function (){console.log(2)},1000)//通过使用并调用执行该匿名函数
如:
image
使用setTimeout,连续几次之后,发现一个现象,返回了类似于有序的编号整数。这是由于setTimeout本身执行的时候,里面的函数返回值可认为返回的是一个定时器的id(或编号),当我们执行setTimeout,浏览器则会创建一个延时器(即一个对象),该延时器的返回则是一个编号。那么,这样的话,我们可以通过编号找到相对应的定时器
续上面例子,如:
var timer = setTimeout(function(){
console.log('wangxiaoqin')
},10000)
--> undefined
timer
--> 419
wangxiaoqin //若不做任何操作,1s后返回这个字符串
clearTimeout(timer) //表示还未执行,该定时器就被取消操作
clearTimeout(422) //表示提前取消编号为XXX的定时器
三、setInterval( )
用法与setTimeout一样,区别在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。即间隔执行任务。
1、如每隔1s执行一次
var i = 1
var timer = setInterval(function() {
console.log(i++);
}, 1000)
image
2、可用来做一个时钟:
var i = 1
var timer = setInterval(function(){
console.log(new Date());
},1000)
image
四、clearTimeout(),clearInterval()
setTimeout和setInterval函数,都返回一个表示计数器编号的整数值,将该整数传入clearTimeout和clearInterval函数,就可以取消对应的定时器。
var id1 = setTimeout(f,1000);
var id2 = setInterval(f,1000);
clearTimeout(id1);
clearInterval(id2);
五、从setTimeout(f,0)引发的关于JS运行环境的探究
我们先看这样一个例子:
setTimeout(function() {
console.log(1);
},0);
console.log(2)
-->2
1 //0s后,返回
为什么会先返回2,再返回1呢?(先留着疑问)
var isOk = true //第1:首先声明变量isOk,默认为true
setTimeout(function(){
console.log(1)
isOk = false
},1000) //异步回调:需要1秒之后,才能将 isOk 设为 false(1s后才执行,所以暂不执行)
while(isOk){
console.log(2)
} //第2:先进行while循环判断,isOk是否为true,如果是,那么就是返回console(2)的结果
//第4:当过了1s后,代码执行,isOK = false,就会停止执行
是不是稍微有点明白?
简单的例子里其实涉及到了JS运行中的很多方面,让我们详细看看
关键词:JS运行环境、事件循环、异步回调
1、单线程模型
这里截取阮一峰老师的JavaScript的教程中的单线程描述
单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。
JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。为了避免复杂性,JavaScript 一开始就是单线程,已成为这门语言的核心特征,将来也不会改变。
2、线程
涉及到单、多线程,这里截取李佳怡专栏文章中关于线程的描述
(1)定义
浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器通常由以下常驻线程组成:GUI 渲染线程,javascript 引擎线程,浏览器事件触发线程,定时触发器线程,异步 http 请求线程。
(2)常驻线程
-
GUI 渲染线程:负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在 Javascript 引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”。即 GUI 渲染线程与 JS 引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
-
javascript 引擎线程:也可以称为 JS 内核,主要负责处理 Javascript 脚本程序,例如 V8 引擎。Javascript 引擎线程理所当然是负责解析 Javascript 脚本,运行代码。浏览器无论什么时候都只有一个 JS 线程在运行 JS 程序。
-
浏览器事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待 JS 引擎处理。
-
定时触发器线程:浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 javaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
-
异步 http 请求线程:在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。
3、了解一下JS的V8运行环境
image主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
注:假设这段话暂时看不明白,暂时放掉,先了解下面的其他知识,完毕之后再回看这段话,就能明白。
说说图中的几个关键名词
(1)堆(heap)
对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。
(2)栈(stack)
函数调用形成了一个栈帧。
而通过使用js 调用栈(call stack)则能更清晰地了解单线程的执行过程。
js 调用栈(call stack):
函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数,这种数据结构的关键在于【后进先出】,即 LIFO(last-in,first-out)。
第一个例子:
function multiply(a,b){
return a*b
}
function square(n){
return multiply(n,n)
}
function printSquare(n){
var squared = square(n);
console.log(squared);
}
printSquare(4)
//一个将两个数字相乘的函数multiply,一个调用了前者的平方函数square,
//一个打印函数printSquare,它调用了square,然后将结果用console.log打印出来
//然后最后我们调用了printSquare
//运行
//调用栈(callback),基本上是一个记录当前程序所在位置的数据结构。如果当前进入了某个函数,
//这个函数就会被放在栈里面。如果当前离开了某个函数,这个函数就会被弹出栈外,这是栈所做的事。
//如果你运行这个文件,将会有一个类似main的函数,指代文件本身,首先,把它放进栈中。
//接着,我们从上到下查看了声明的函数,看到了最后是printSquare,知道了它被调用了,
//那么我们把它推进栈里;它调用了square,所以也把square推进栈里;square也调用了mulitiply,
//同样把mulitiply推进栈中,最后,我们得到了mulitiply的返回值
//那么这之后,我们把multiply弹出栈,然后square也得到了返回值,再把square弹出栈,
//最后到了printSquare,它调用了console.log,到这里已经没有返回值。我们到了函数的最后部分,
//然后我们完成了。
第二个例子:
function f(b) {
var a = 12;
return a + b + 35;
}
function g(x) {
var m = 4;
return f(m * x);
}
g(21);
调用 g 函数 的时候,创建了第一个 堆( Heap ) 栈(stack) 帧 ,包含了 g 的参数和局部变量。当 g 调用 f 的时候,第二个 堆栈帧 就被创建、并置于第一个 堆栈帧 之上,包含了 f 的参数和局部变量。当 f 返回时,最上层的 堆栈帧 就出栈了(剩下 g 函数调用的 堆栈帧 )。当 g 返回的时候,栈就空了。
第三个例子:
function test() {
setTimeout(function() {
alert(1)
},1000);
alert(2);
}
test();
在执行函数 test 的时候,test 先入栈,如果不给 alert(1)加 setTimeout,那么 alert(1)第 2 个入栈,最后是 alert(2)。但现在给 alert(1)加上 setTimeout 后,alert(1)就被加入到了一个新的堆栈中等待,并1s后执行,因此实际的执行结果就是先 alert(2),再 alert(1)。
(3)队列(queue)
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都有一个为了处理这个消息相关联的函数。
任务队列(消息队列):
任务(消息)队列是一个先进先出的队列,它里面存放着各种任务(消息)
A、同步任务VS异步任务
console.log('Hi')
setTimeout(function(){
console.log('There')
},1000)
console.log('wangxiaoqin')
- 同步函数:如果在函数A返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。如:
console.log('Hi’); //函数返回时,就看到了预期的效果:在控制台打印了一个字符串
- 异步函数:即如果在函数A返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。如:
setTimeout(fn, 1000);//setTimeout是异步过程的发起函数,fn是回调函数
-
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
-
异步任务:主线程发起一个异步请求(即执行异步函数),相应的工作线程(浏览器事件触发线程、异步http请求线程等)接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,将完成消息放到任务(消息)队列,主线程通过事件循环过程去取任务(消息),然后执行一定的动作(调用回调函数)。看此图可视化描述:
image
B、事件循环(Event loop)
事件循环,指主线程重复从任务(消息)队列中取任务(消息)、执行的过程。取一个任务(消息)并执行的过程叫做一次循环。
即:
while (queue.waitForMessage()) {
queue.processNextMessage();
} //如果当前没有任何消息queue.waitForMessage 会等待同步消息到达
事件循环中有事件两个字的原因:任务(消息)队列中的每条消息实际上都对应着一个事件——dom事件。如:
var button = document.getElement('#btn');
button.addEventListener('click',function(e) {
console.log();
});
从异步过程的角度看,addEventListener 函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。那么添加的这个任务(消息)事实上就是任务注册异步任务时添加的回调函数。如果 一个异步函数没有回调,那么它就不会放到任务(消息)队列里。
总结:主线程在执行完当前循环中的所有代码后,就会到任务(消息)队列取出一条消息,并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,工作线程就没必要通知主线程,从而也没必要往消息队列放消息。如图:
image了解一下工作线程(即异步 http 请求线程,即 Ajax 线程)是如何工作:
4、再来看setTimeout(f,0)所带来的零延迟与事件循环、任务队列的联系
setTimeout和setInterval的运行机制是,将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。
什么意思呢?
setTimeout的作用是,将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f,0),那么不会立刻执行。这里则涉及到了零延迟。
**零延迟 (Zero delay) **并不是意味着回调会立即执行。在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数。其等待的时间基于队列里正在等待的消息数量。也就是说,setTimeout()只是将事件插入了任务队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。
setTimeout(function() {
console.log(1);
},0);
console.log(2)
-->2
1 //0s后,返回
现在我们知道为什么返回结果是2,1。因为只有在执行完主线程的所有代码之后,主线程空了,才会去任务队列中取任务执行回调函数,去执行回调函数。
总结: setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到主线程把同步任务和"任务队列"现有的事件都处理完,才会得到执行。
for(var i=0; i<10; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
执行结果为:
image
相当于for(var i=0; i<10; i++)这个同步代码执行完之后,i的值变为10 。此时(1s后),执行回调函数,在同步任务中创建了10个定时器均在1s中之后执行,则返回了10
由此看来,在某种程度上,我们可以利用setTimeout(fn,0)的特性,修正浏览器的任务顺序。
参考、学习并感谢:
1.MDN:并发模型与事件循环
2.阮一峰JavaScript参考教程:异步操作概述
3.李佳怡专栏:【 js 基础 】 setTimeout(fn, 0) 的作用