视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
如何通过setTimeout理解JS运行机制详解
2020-11-27 21:59:54 责编:小采
文档

任务队列

那么单线程的JavasScript是怎么实现“非阻塞执行”呢?

答:异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。
诸如事件点击触发回调函数、ajax通信、计时器这种异步处理是如何实现的呢?

答:任务队列

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

任务队列:一个先进先出的队列,它里面存放着各种事件和任务。

同步任务

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

  • 输出
  • 如:console.log()
  • 变量的声明
  • 同步函数:如果在函数返回的时候,调用者就能够拿到预期的返回值或者看到预期的效果,那么这个函数就是同步的。
  • 异步任务

  • setTimeout和setInterval
  • DOM事件
  • Promise
  • process.nextTick
  • fs.readFile
  • http.get
  • 异步函数:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
  • 除此之外,任务队列又分为macro-task(宏任务)与micro-task(微任务),在ES5标准中,它们被分别称为task与job。

    宏任务

    1. I/O
    2. setTimeout
    3. setInterval
    4. setImmdiate
    5. requestAnimationFrame

    微任务

    1. process.nextTick
    2. Promise
    3. Promise.then
    4. MutationObserver

    宏任务和微任务的执行顺序

    一次事件循环中,先执行宏任务队列里的一个任务,再把微任务队列里的所有任务执行完毕,再去宏任务队列取下一个宏任务执行。

    注:在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

    三、setTimeout运行机制

    setTimeout 和 setInterval的运行机制是将指定的代码移出本次执行,等到下一轮 Event Loop 时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮 Event Loop 时重新判断。

    这意味着,setTimeout指定的代码,必须等到本次执行的所有同步代码都执行完,才会执行。

    优先关系:异步任务要挂起,先执行同步任务,同步任务执行完毕才会响应异步任务。

    四、进阶

    console.log('A');
    setTimeout(function () {
     console.log('B');
    }, 0);
    while (1) {}

    大家再猜一下这段程序输出的结果会是什么?

    答:A

    注:建议先注释掉while循环代码块的代码,执行后强制删除进程,不然会造成“假死”。

    同步队列输出A之后,陷入while(true){}的死循环中,异步任务不会被执行。

    类似的,有时addEventListener()方法监听点击事件click,用户点了某个按钮会卡死,就是因为当前JS正在处理同步队列,无法将click触发事件放入执行栈,不会执行,出现“假死”。

    五、定时获取接口更新数据

    for (var i = 0; i < 4; i++) {
     setTimeout(function () {
     console.log(i);
     }, 1000);
    }

    输出结果为,隔1s后一起输出:4 4 4 4

    for循环是一个同步任务,为什么连续输出四个4?

    答:因为有队列插入的时间,即使执行时间从1000改成0,还是输出四个4。

    那么这个问题是如何产生和解决的呢?请接着阅读

    异步队列执行的时间

    执行到异步任务的时候,会直接放到异步队列中吗?

    答案是不一定的。

    因为浏览器有个定时器(timer)模块,定时器到了执行时间才会把异步任务放到异步队列。
    for循环体执行的过程中并没有把setTimeout放到异步队列中,只是交给定时器模块了。4个循环体执行速度非常快(不到1毫秒)。定时器到了设置的时间才会把setTimeout语句放到异步队列中。

    即使setTimeout设置的执行时间为0毫秒,也按4毫秒算。

    这就解释了上题为什么会连续输出四个4的原因。

    HTML5 标准规定了setTimeout()的第二个参数的最小值,即最短间隔,不得低于4毫秒。如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。

    利用闭包实现 setTimeout 间歇调用

    for (let i = 0; i < 4; i++) {
     (function (j) {
     setTimeout(function () {
     console.log(j);
     }, 1000 * i)
     })(i);
    }

    执行后,会隔1s输出一个值,分别是:0 1 2 3

  • 此方法巧妙利用IIFE声明即执行的函数表达式来解决闭包造成的问题。
  • 将var改为let,使用了ES6语法。
  • 这里也可以用setInterval()方法来实现间歇调用。

    详见:setTimeout和setInterval的区别

    利用JS中基本类型的参数传递是按值传递的特征实现

    var output = function (i) {
     setTimeout(function () {
     console.log(i);
    
     }, 1000 * i)
    }
    for (let i = 0; i < 4; i++) {
     output(i);
    }

    执行后,会隔1s输出一个值,分别是:0 1 2 3

    实现原理:传过去的i值被复制了。

    基于Promise的解决方案

    const tasks = [];
    
    const output = (i) => new Promise((resolve) => {
     setTimeout(() => {
     console.log(i);
     resolve();
     }, 1000 * i);
    
    });
    
    //生成全部的异步操作
    for (var i = 0; i < 5; i++) {
     tasks.push(output(i));
    }
    //同步操作完成后,
    输出最后的i Promise.all(tasks).then(() => { setTimeout(() => { console.log(i); }, 1000) })

    执行后,会隔1s输出一个值,分别是:0 1 2 3 4 5

    优点:提高了代码的可读性。

    注意:如果没有处理Promise的reject,会导致错误被丢进黑洞。

    使用ES7中的async await特性的解决方案(推荐)

    const sleep = (timeountMS) => new Promise((resolve) => {
     setTimeout(resolve, timeountMS);
    });
    
    (async () => { //声明即执行的async
     for (var i = 0; i < 5; i++) {
     await sleep(1000);
     console.log(i);
     }
    
     await sleep(1000);
     console.log(i);
    
    })();

    执行后,会隔1s输出一个值,分别是:0 1 2 3 4 5

    六、事件循环 Event Loop


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

    有时候 setTimeout明明写的延时3秒,实际却5,6秒才执行函数,这又是因为什么?

    答:setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。

    浏览器的JS引擎遇到setTimeout,拿走之后不会立即放入异步队列,同步任务执行之后,timer模块会到设置时间之后放到异步队列中。js引擎发现同步队列中没有要执行的东西了,即运行栈空了就从异步队列中读取,然后放到运行栈中执行。所以setTimeout可能会多了等待线程的时间。

    这时setTimeout函数体就变成了运行栈中的执行任务,运行栈空了,再监听异步队列中有没有要执行的任务,如果有就继续执行,如此循环,就叫Event Loop。

    七、总结

    JavaScript通过事件循环和浏览器各线程协调共同实现异步。同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性。

    知识点梳理:

  • 理解JS的单线程的概念:一段时间内做一件事
  • 理解任务队列:同步任务、异步任务
  • 理解 Event Loop
  • 理解哪些语句会放入异步任务队列
  • 理解语句放入异步任务队列的时机
  • 最后,希望大家阅后有所收获。🤠

    好了,

    下载本文
    显示全文
    专题