美文网首页JS
#hello,JS:08 setTimeout和任务队列

#hello,JS:08 setTimeout和任务队列

作者: 饥人谷_远方 | 来源:发表于2018-08-11 16:47 被阅读0次

    前言:

    之前写过一次被不小心删掉了,幸好思路和参考资料还在,所以赶快写下来。里面涉及了一点点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) 的作用

    相关文章

      网友评论

        本文标题:#hello,JS:08 setTimeout和任务队列

        本文链接:https://www.haomeiwen.com/subject/rimdbftx.html