#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 函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。那么添加的这个任务(消息)事实上就是任务注册异步任务时添加的回调函数。如果 一个异步函数没有回调,那么它就不会放到任务(消息)队列里。

总结:主线程在执行完当前循环中的所有代码后,就会到任务(消息)队列取出一条消息,并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,工作线程就没必要通知主线程,从而也没必要往消息队列放消息。如图:

了解一下工作线程(即异步 http 请求线程,即 Ajax 线程)是如何工作:

image

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) 的作用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容