settimeout 延时不生效_第3部分-异步使用1:定时任务setTimeout和任务队列

异步
任务队列
定时器
网络请求
事件绑定

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)//通过使用并调用执行该匿名函数

如:

d61c9503361d4f62815897c58f2687e0.png

使用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)

933fc0c8a56af24d95ceaca78855b0c1.gif

2、可用来做一个时钟:

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

c85948bfa3304ee6a48e69b501d3523e.gif

四、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运行环境

edfe0cbb79b4f3c32144f2a030cf6c88.png

主线程运行的时候,产生堆(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请求线程等)接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,将完成消息放到任务(消息)队列,主线程通过事件循环过程去取任务(消息),然后执行一定的动作(调用回调函数)。看此图可视化描述:

300e33c43d17b651f2256799f96c405b.png

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 线程)是如何工作:

cde484ffd181e9c17cfc25980a3f85ad.png

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)
} 

执行结果为:

4667c224d9cf53790f76da9502114d96.png

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

六、任务队列异步与回调

先看一下下面这段代码:

function f1(callback){
    setTimeout(function(){
        //做某件事,可能很久
        console.log('别急,开始执行f1')
        for(var i=0;i< 100000;i++){

        }
        console.log('f1执行完了')
    }, 0);

}
function f2(){
    console.log('执行f2');
}
function f3(){
    console.log('执行f3');
}
f1()
f2() 
f3()

f2,f3为同步,先执行完f2,f3,再执行f1中的任务队列函数

输出结果为:

2ac3a4df8c9851a9a6fcff69214f5098.png

在f1中加入一个回调函数,进而调用:

function f1(callback){
    setTimeout(function(){
        //做某件事,可能很久
        console.log('别急,开始执行f1')
        for(var i=0;i< 100000;i++){

        }
        console.log('f1执行完了')
        callback(i)
    }, 0);

}
f1(function(value){
    console.log(value)
})

在f1中使用一个回调函数callback(实质上就是f2),在保证f1的异步操作获取数据完成之后,调用这个回调函数。执行结果如下:

884fb4410b4b2dac0e5da6016bd1ba34.png

七、函数节流

设置一个定时器,将多次执行的功能及时清除,待定时器时间到达之后,输出最后一次执行的结果

    var timer
  function hiFrequency(){
      if(timer){
          clearTimeout(timer)
      }
        timer = setTimeout(function(){
             console.log('do something')
        }, 300)
  }
//多次执行直到定时器执行结果do something为止
  hiFrequency()
  hiFrequency()
  hiFrequency()

进行改造,将函数封装起来,连续去执行fn(实质就是throttle返回的结果),到了1s之后,则输出最后结果

function throttle(fn, delay) {
    var timer = null
    return function(){
        clearTimeout(timer)
        timer = setTimeout(function(){ 
            fn(arguments)
        }, delay)
    }
}

function fn(){
    console.log('hello ')
}

var fn2 = throttle(fn, 1000)

//表示多次执行,直至fn函数输出hello为止
fn2()
fn2()
fn2()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值