Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setTimeout 倒计时为什么会出现误差? #21

Open
YvetteLau opened this issue Jun 6, 2019 · 25 comments
Open

setTimeout 倒计时为什么会出现误差? #21

YvetteLau opened this issue Jun 6, 2019 · 25 comments

Comments

@YvetteLau
Copy link
Owner

No description provided.

@shenanheng
Copy link

首先setTimeout的执行会出现在事件队列里面,当在这个作用域从上到下,从左到右把当前的代码执行完之后,才会执行事件队列里面的任务,根据先进先出的原则;但是执行这些都需要一定的时间,所以才会导致倒计时会有误差哦,萌萌哒!!!!嘤嘤怪

@AILINGANGEL
Copy link

setTimeout是一个异步的宏任务,当执行setTimeout时是将回调函数在指定的时间之后放入到宏任务队列。但如果此时主线程有很多同步代码在等待执行,或者微任务队列以及当前宏任务队列之前还有很多任务在排队等待执行,那么要等他们执行完成之后setTimeout的回调函数才会被执行,因此并不能保证在setTimeout中指定的时间立刻执行回调函数

@CCZX
Copy link

CCZX commented Jun 6, 2019

这里涉及到JavaScript的Event Loop,首先js引擎在执行代码的时候,会把整个script看作一个宏任务,在执行内部代码的时候如果遇到同步任务就进入主线程等到它执行完成,遇到异步任务就会把它放入事件表注册,当事件满足执行条件的时候就把他的放入任务队列,当主线程空闲的时候就会依次清空任务队列,但是需要注意的是异步任务又微任务和宏任务之分,当主线程空闲时会先清空微任务列表的所有任务,然后再执行宏任务的一个任务。

@riluocanyang
Copy link

setTimeout倒计时为什么会出现误差?

首先,js是单线程,同一时间只能做一件事情。如果前面一个任务执行时间很长(比如网络请求),后面就必须的等待很长时间。为了解决这个问题,js分为同步任务和异步任务。js会先执行同步任务,执行完后,才会去执行异步任务,异步任务一般放在异步队列中。也就是执行完同步任务后,会不断从异步队列中取出要执行的任务放在主栈中执行,这个过程就称为"event-loop"。
异步队列分为宏任务队列和微任务队列,
宏任务队列包括:

setTimeout, setInterval, setImmediate

微任务队列包括:

promise, async/await

微任务队列执行顺序大于宏任务队列。

所以,setTimeout出现误差是因为:

  1. 要先执行同步任务,才会执行异步任务;
  2. 异步任务中,微任务执行顺序大于宏任务执行顺序。

@MissWXiang
Copy link

1.由于js是单线程的,也就是阻塞的,定时可定会不准。无论setTimeout()还是setInterval(),都有问题;
2、打开浏览器,然后切换到其他app,再次回到浏览器,这期间js可能停止执行的问题。

(微信名:RUN)

@lqzo
Copy link

lqzo commented Jun 8, 2019

单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。这与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。
主线程和任务队列的示意图

Event Loop(事件循环)

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

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。
Event Loop
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

定时器

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。(区别在于前者指定的代码是一次性执行,后者则为反复执行。)

为什么使用setTimeout实现倒计时,而不是setInterval?

setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。
为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。

var timer = setTimeout(function f() {
  // ...
  timer = setTimeout(f, 1000);
}, 1000);

上面代码可以确保,下一次执行总是在本次执行结束之后的1000毫秒开始。(当然存在较小误差,不然就没这道题了。。)

setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),那么会立刻执行吗?

答案是不会。因为必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f。也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行。

setTimeout倒计时为什么会出现误差?

setTimeout作为异步任务,在实现倒计时功能的时候,除了执行我们功能的实现代码,还会有主线程对任务队列的读取及执行等过程,这些过程也需要耗费一些时间,所以会因为event loop的机制出现些许误差。

参考文章(搬砖原址):
定时器 - JavaScript教程 - 网道
JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志


虽然楼上几位答得很不错了,但这个问题毫无疑问又是我的知识盲区,还是没有很快的弄明白是咋回事,要继续跟着小夕姐学习,加油!!!

@YvetteLau
Copy link
Owner Author

单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。这与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。
主线程和任务队列的示意图

Event Loop(事件循环)

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

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。
Event Loop
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

定时器

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。(区别在于前者指定的代码是一次性执行,后者则为反复执行。)

为什么使用setTimeout实现倒计时,而不是setInterval?

setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。
为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。

var timer = setTimeout(function f() {
  // ...
  timer = setTimeout(f, 1000);
}, 1000);

上面代码可以确保,下一次执行总是在本次执行结束之后的1000毫秒开始。(当然存在较小误差,不然就没这道题了。。)

setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),那么会立刻执行吗?

答案是不会。因为必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f。也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行。

setTimeout倒计时为什么会出现误差?

setTimeout作为异步任务,在实现倒计时功能的时候,除了执行我们功能的实现代码,还会有主线程对任务队列的读取及执行等过程,这些过程也需要耗费一些时间,所以会因为event loop的机制出现些许误差。

参考文章(搬砖原址):
定时器 - JavaScript教程 - 网道
JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志

虽然楼上几位答得很不错了,但这个问题毫无疑问又是我的知识盲区,还是没有很快的弄明白是咋回事,要继续跟着小夕姐学习,加油!!!

赞赞赞,已经充分补习了 Event-Loop~

@CCZX
Copy link

CCZX commented Jun 9, 2019

setTimeuot是异步的任务,当遇到异步任务的时候会把该任务到事件表注册,当时间到的时候会把setTimeout的回调函数加入事件队列,只有当同步任务执行完成之后才会执行事件队列的任务,所以会有偏差,所以严格来说setTimeout 只是让开始执行时间不小于规定的时间。

@KRISACHAN
Copy link

KRISACHAN commented Jun 9, 2019

setTimeout 倒计时为什么会出现误差?

线程与进程

相信大家经常会听到一句话,就是 “JS是单线程的”,可是什么是 线程,什么又是 单线程,有 多线程 吗?

定义

讲到线程,那么肯定也得说一下进程。其实在本质上,两个名词都是 CPU 工作时间片的一个描述。

进程(process) 指的是CPU 在 运行指令及加载和保存上下文所需的时间,放在应用上是指计算机中已运行的程序。

线程(thread) 是操作系统能够进行运算的最小单位。它被包含在 进程 之中,描述了执行一段指令所需的时间。

  • 单线程:按代码书写顺序从头到尾,一行一行地执行代码,如果其中一行代码报错,那么剩下代码将不再执行。容易阻塞代码。
  • 多线程:代码运行的环境不同,各线程独立,互不影响,避免阻塞。

浏览器中的线程

浏览器中的线程分了以下几类:

  • JS线程
  • UI线程
  • event线程
  • 定时器线程
  • http线程

执行栈

执行栈可以理解为是用来存储函数调用的栈,遵循先进后出的原则。

事件循环

node端

Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

Event Loop 6 个阶段:

  1. timers
  2. I/O callbacks
  3. idle, prepare
  4. poll
  5. check
  6. close callbacks

浏览器端

浏览器端 的情况与 node端 的情况相仿,当我们执行 JS 代码的时候其实就是往执行栈中放入函数,当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行。

  • 微任务(microtask)
    • process.nextTick
    • promise
    • Object.observe(曾经是提案,如今已经废除)
    • MutationOberver
  • 宏任务(macrotask)
    • script
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI渲染

执行顺序如下:

  1. 执行同步代码,这是宏任务
  2. 执行栈为空,查询是否有微任务要执行
  3. 必要时渲染UI
  4. 进行下一轮的 EventLoop ,执行宏任务中的异步代码

setTimeout 误差

上面讲了定时器是属于 宏任务(macrotask) 。如果当前 执行栈 所花费的时间大于 定时器 时间,那么定时器的回调在 宏任务(macrotask) 里,来不及去调用,所有这个时间会有误差。

我们看以下代码:

setTimeout(function () {
	console.log('biubiu');
}, 1000);

某个执行时间很长的函数();

如果定时器下面的函数执行要 5秒钟,那么定时器里的log 则需要 5秒之后再大圆,函数占用了当前 执行栈 ,要等执行栈执行完毕后再去读取 微任务(microtask),等 微任务(microtask) 完成,这个时候才会去读取 宏任务(macrotask) 里面的 setTimeout 回调函数执行。setInterval 同理,例如每3秒放入宏任务,也要等到执行栈的完成。

还有一种情况如下:

setTimeout(function() {
    setTimeout(function() {
        setTimeout(function() {
            setTimeout(function() {
                setTimeout(function() {
                    setTimeout(function() {
                        console.log('嘤嘤嘤');
                    }, 0);
                }, 0);
            }, 0);
        }, 0);
    }, 0);
}, 0);

在最新的规范里有这么一句:
If nesting level is greater than 5, and timeout is less than 4, then increase timeout to 4.

所以意思就是意思就是如果timeout嵌套大于 5层,而时间间隔小于4ms,则时间间隔增加到4ms。

@into-piece
Copy link

into-piece commented Jun 9, 2019

因为setTimeout是异步宏任务,如果执行栈中的执行所用的时间超过了定时器设置的间隔时间,根据事件轮询机制,需清理完执行栈,task队列才会进入主线程执行,执行所有微任务,最后才是执行宏任务,所以setTimeout开始执行时间会被延迟,出现误差。

@yelin1994
Copy link

setTimeout 倒计时为啥会出现误差

说道这个问题,可以先了解下javascript 的事件循环

事件循环

js 是单线程。为了不让某个耗时比较长的任务,比如aajax请求,让引擎一直等待返回才执行其他任务,一般将任务分为同步任务和异步任务。

  • 当任务进入执行栈的时候,同步任务和异步任务会进入不同的执行场所,同步会进入主线程,异步的进入事件表,并注册函数。
  • 当异步任务返回时,该任务会进入事件队列。
  • 当主线程的任务完成时,才会去事件队列读取对应的函数,进入主线程执行
  • 上述过程,不断重复。形成了事件循环

Js 引擎存在管理进程的进程,会一直不停检查主线程。一旦为空就会去读取事件队列的任务。

而setTimeout 刚好是异步进程,他的执行往往要等主线程,才会去从事件队列,调用setTimeout的回调函数,所以也就导致,会与自己写的延迟时间不相符合。

进一步说一下, 任务还可以微任务和宏任务。

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

参考:https://juejin.im/post/59e85eebf265da430d571f89

@MissNanLan
Copy link

MissNanLan commented Jun 9, 2019

下面这这道题有助于我们去理解

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

以上结果输出是什么,答案是
image.png
你想到了吗?

先简单介绍几个概念。

单线程(single thread)

JS的本质核心就是单线程,这也就意味着所有的任务都得排队,前面的必须处理好,后面的才执行

执行栈(execution context stack)

执行栈也称为调用栈,在主线程上执行,形成一个执行栈

任务队列(queues)

任务分为同步任务异步任务,只有在主线程上面执行的任务叫同步任务,即是前一个执行完毕后后面一个才执行。而异步任务不会进入主线程,它会被加入到 任务队列当中,只有任务队列通知主线程,某个异步的任务才开始执行,异步任务分为宏任务微任务

宏任务和微任务(macroTask、MicroTask)

宏任务

setTimeout、 setInterval 、 setImmediate、Dom、Ajax....

微任务

promise、asyc/await、process.nextTick

微任务队列的执行顺序先于宏任务队列,这样就很好解释开文提到的那段代码的结果

console.log('script start'); console.log('script end');都是同步的。promisesetTimout是异步的,而promise是微任务,setTimeout是宏任务,而微任务的执行顺序先于宏任务

一图胜前言。 很容易看出调用栈、宏观任务队列、微观任务队列
image.png

事件循环(Event Loop)

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
image
同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的Event Loop(事件循环)。

定时器

setTimeout(function(){console.log(1);}, 0);
console.log(2);

这样的结果答案总是2、1,这是因为setTimeout被加入任务队列当中去,它必须等到同步任务和任务队列中的现有的是件都处理完,才去执行,还有setTimeout最小的delay时间是4毫秒

为什么setTimeout倒计时会出现误差

如果执行栈的时间大于定时器设置的时间,那么要等到执行栈完毕之后才去执行宏任务,那么这个时候会出现误差。
参考:https://blog.csdn.net/lc237423551/article/details/79902106

@YvetteLau
Copy link
Owner Author

setTimeout 只能保证延时或间隔不小于设定的时间。因为它实际上只是将回调添加到了宏任务队列中,但是如果主线程上有任务还没有执行完成,它必须要等待。

如果你对前面这句话不是非常理解,那么有必要了解一下 JS的运行机制。

JS的运行机制

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在"任务队列"(task queue)。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

setTimeout(()=>{callback();}, 1000) ,即表示在1s之后将 callback 放到宏任务队列中,当1s的时间到达时,如果主线程上有其它任务在执行,那么 callback 就必须要等待,另外 callback 的执行也需要时间,因此 setTimeout 的时间间隔是有误差的,它只能保证延时不小于设置的时间。

如何减少 setTimeout 的误差

我们只能减少执行多次的 setTimeout 的误差,例如倒计时功能。

倒计时的时间通常都是从服务端获取的。造成误差的原因:

1.没有考虑误差时间(函数执行的时间/其它代码的阻塞)

2.没有考虑浏览器的“休眠”

完全消除 setTimeout的误差是不可能的,但是我们减少 setTimeout 的误差。通过对下一次任务的调用时间进行修正,来减少误差。

let count = 0;
let countdown = 5000; //服务器返回的倒计时时间
let interval = 1000;
let startTime = new Date().getTime();
let timer = setTimeout(countDownStart, interval); //首次执行
//定时器测试
function countDownStart() {
    count++;
    const offset = new Date().getTime() - (startTime + count * 1000);
    const nextInterval = interval - offset; //修正后的延时时间
    if (nextInterval < 0) {
        nextInterval = 0;
    }
    countdown -= interval;
    console.log("误差:" + offset + "ms,下一次执行:" + nextInterval + "ms后,离活动开始还有:" + countdown + "ms");
    if (countdown <= 0) {
        clearTimeout(timer);
    } else {
        timer = setTimeout(countDownStart, nextInterval);
    }
}

如果当前页面是不可见的,那么倒计时会出现大于100ms的误差时间。因此在页面显示时,应该重新从服务端获取剩余时间进行倒计时。当然,为了更好的性能,当倒计时不可见(Tab页切换/倒计时内容不在可视区时),可以选择停止倒计时。

为此,我们可以监听 visibityChange 事件进行处理。

@jodiezhang
Copy link

jodiezhang commented Jun 9, 2019

JS语言的一大特点就是单线程,同一个时间只能做一件事情。
任务队列
单线程就意味着,所有的任务需要排队,前一个任务结束,才会执行后一个任务。如果前面一个任务耗时太长,后面的任务就得等着。

所有的任务分成两类,一种是同步任务,一种是异步任务。同步任务指的是,在主线程上排队执行的任务。异步任务是不进入主线程,而进入任务队列(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。

JS中有两种异步任务
宏任务:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering
微任务:process.nextTick(Nodejs),Promises,Object.observe,MutationObserver

事件循环(event-loop)
主线程从任务队列中读取执行事件, 这个过程是不断循环的,这个机制被称为事件循环。
主线程会不断的从任务队列中按顺序取任务执行,每执行完一个任务都会检查微任务队列是否为空,如果不为空则会一次性执行完所有的微任务,然后再进入下一个循环去任务队列取下一个任务执行。

1.选择当前要执行的宏任务队列,选择一个最先进入任务队列的宏任务,如果没有宏任务可以选择,则会跳转至microtask的执行步骤。
2.将事件循环的当前运行宏任务设置为已选择的宏任务。
3.运行宏任务。
4.将事件循环的当前运行任务设置为null。
5.将运行完的宏任务从宏任务队列中移除。
6.microtasks步骤:进入microtask检查点。
7.更新界面渲染。
8.返回第一步。
setTimeout 只能保证延时或间隔不小于设定的时间。因为它实际上只是将回调添加到了宏任务队列中,但是如果主线程上有任务还没有执行完成,它必须要等待。

@Sakura-pgh
Copy link

总结:
这道题其实考的是对JS运行机制的了解,需要了解的知识点有:
1: 进程和线程
2: JS的执行栈、任务队列、微任务和宏任务
3: event-loop 事件循环

@wangjunw
Copy link

因为js是单线程的,所以的要等队列中的同步任务执行完成才会开始执行setTimeout,而不是从你打开网页算起。如果前面队列任务执行的时间较长,自然就会出现延迟。

@Cain-kz
Copy link

Cain-kz commented Jun 10, 2019

1因为js是单线程的解析器,因此一定时间内只能执行一段代码。
2为了控制执行的代码,就有一个js任务队列
3这些任务会按照它们添加到队列的顺序执行
4setTimout()第二参是多长时间后把当前任务议案家到队列中,如果队列为空,立即执行。如果队列不为空,那么要等前面的代码执行完后再执行,所以会出现延迟。

@zhangxianhui
Copy link

js为什么是单线程的

我们都知道 JavaScript 是一门 单线程 语言,也就是说同一时间只能做一件事。这是因为 JavaScript 生来作为浏览器脚本语言,主要用来处理与用户的交互、网络以及操作 DOM。这就决定了它只能是单线程的,否则会带来很复杂的同步问题。
假设 JavaScript 有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

同步和异步

既然 Javascript 是单线程的,它就像是只有一个办业务,客户不得不排队一个一个的等待办理。同理 JavaScript 的任务也要一个接一个的执行,如果某个任务是个耗时任务,那浏览器岂不得一直卡着?为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念

异步

如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。比如说发一个网络请求,我们告诉主程序等到接收到数据后再通知我,然后我们就可以去做其他的事情了。当异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,再去执行。

这也就是定时器并不能精确在指定时间后输出回调函数结果的原因

setTimeout(() => {
console.log('yancey');
}, 1000);

for (let i = 0; i < 100000000; i += 1) {
console.log("会执行很久")
}

js的两种异步任务

宏任务(macrotasks)和微任务(microtasks)

任务队列

单线程就意味着,所有的任务需要排队,前一个任务结束,才会执行后一个任务。如果前面一个任务耗时太长,后面的任务就得等着。
所有的任务分成两类,一种是同步任务,一种是异步任务。同步任务指的是,在主线程上排队执行的任务。
异步任务是不进入主线程,而进入任务队列(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
那我们上面提到的任务队列到底是什么呢?跟macrotasks和microtasks有什么联系呢?
一个事件循环有一个或者多个任务队列;
每个事件循环都有一个microtask队列
macrotask队列就是我们常说的任务队列,microtask队列不是任务队列
一个任务可以被放入到macrotask队列,也可以放入microtask队列
当一个任务被放入microtask或者macrotask队列后,准备工作就已经结束,这时候可以开始执行任务了。

也就是说执行顺序是:

开始 -> 取task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> … 这样循环往复

@luohong123
Copy link

例如:

 setTimeout(function() {
    alert("Hello world!");
}, 1000);

第一个参数是函数,第二个参数表示等待多长时间的毫秒数,但经过该时间后指定的代码不一定会执行。为什么不一定过了1000毫秒马上执行呢?
JavaScript 是一个单线程序的解释器,因此一定时间内只能执行一段代码。为了控制要执行的代码,就 有一个 JavaScript 任务队列。这些任务会按照将它们添加到队列的顺序执行。setTimeout()的第二个 参数告诉 JavaScript 再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即 执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。

@yeyeyess
Copy link

JavaScript是单线程的,代码是从上到下执行的,前一个任务结束,后一个才能开始,这样容易形成任务阻塞。所以在js中可以使用setTimeout、setInterval、promise、async/await等异步处理任务,防止任务阻塞。

任务分成同步任务和异步任务。所有同步任务都会加入主线程,形成一个执行栈。而异步任务就会进入任务队列,异步任务又分为宏任务(macrotask)和微任务(microtask)。当异步任务执行完会通知主线程并等待。

当主线程中的所有同步任务(也属于宏任务)都执行完,等待中的异步任务就会从任务队列中进入主线程,先执行所有的微任务,异步任务中的宏任务再进入主线程执行,依次循环,形成一个event loop(事件循环)。

回到问题本身,setTimeout倒计时为什么会出现误差?

setTimeout属于异步任务中的宏任务,根据JS的运行机制,若设置的延时执行时间到了,主线程中的同步任务还未执行完,setTimeout就无法执行,必须继续等待,那么就会出现误差。

@chongyangwang
Copy link

setTimeout为什么会出现倒计时误差

首先说一下任务队列

js代码在执行的时候,会创建一个执行队列,会将代码区分为同步任务还是异步任务,从而进入不同的空间,同步任务会进入主线程,主线程的事件会立即执行,异步任务会进入任务队列,等待主线程的任务执行完毕之后 ,主线程没有正在执行的任务,那么这时主线程 或执行任务队列(我理解的,大佬称为任务队列)会通知任务队列即(所有异步任务的集合),异步任务又分为微任务与宏任务 , 执行异步任务时会先将微任务执行完毕,在去执行宏任务 依次循环 这个称之为事件循环。

常见的微任务有:

promise , async/await , process.nextTick

常见的宏任务有:

setTimeout、 setInterval

倒计时误差产生的原因

setTimout属于异步任务的宏任务 后面的参数代表延迟执行的时间,如果延迟时间完毕,主线程没有正在执行的任务,那么会立即执行settimeout,如果尚有正在执行的任务,那么会等待主线程任务执行完毕之后在执行,所以会产生误差。

@Diamondjcx
Copy link

Diamondjcx commented Jun 17, 2019

参考以上各位大佬的总结

单线程:所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务不得不一直等着。

任务队列:分为同步任务和异步任务。

              同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务‘
              异步任务:不进入主线程,而进入“任务队列”的任务,只有“任务队列通知主线程,某个异步任 
              务可以执行了,该任务才会进入主线程执行。

(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个“任务队列”。只要异步任务有了运行结果,就在“任务队列”之中放置一个事件
(3)一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

定时器

除了放置异步任务的时间,“任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。
为什么使用setTimeout实现倒计时,而不是setInterval?
setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务指定本身所消耗的时间,因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定妹100ms执行一次,每次执行需要5ms,那么第一次执行结束后95ms,第二次执行就会开始。如果某次执行耗时特别长,比如需要105ms,那么它结束后,下一次执行就会立即开始。
为了确保两次执行之间的固定的间隔,可以不用setIntetrval,二是每次执行结束后,使用setTimeout指定下一次执行的具体时间。

setTimeout倒计时为什么出现误差?

setTimeout作为异步任务,在实现倒计时功能的时候,除了执行我们功能的实现代码,还会有主线程对任务队列的读取及执行等过程,这些过程也需要耗费一些时间,所以因为event loop的机制出现些许误差。

@ZadaWu
Copy link

ZadaWu commented Jun 19, 2019

这个问题要从根本上理解,请先认真读这篇文章,我每次读一遍,都能领悟到一些东西。

倒计时为啥会出现误差

在基于上面的文章的内容,因为可能在它推入事件列表时,主线程还不空闲,正在执行其他的代码,所以自然有误差。

@James-Lam
Copy link

James-Lam commented Jun 23, 2020

浏览器timer的最小时间延迟为什么是4ms,而不是2ms或者3ms?
这是我在Nicholas.c.zakas的博客找到相关历史背景:Chrome 1.0 beta had a one millisecond timer resolution. That seemed okay, but then the team started having bug reports. It turns out that timers cause the CPU to spin, and when the CPU is spinning, more power is being consumed because it can’t go into sleep (low power) mode.3 That caused Chrome to push its timer resolution to 4ms.

但仍然不清楚为何是4ms

@plh97
Copy link

plh97 commented Jan 26, 2024

....这个不是早就有解决方案了嘛. Math.round就已经解决了延误问题. 延误时差往往在4ms左右, 直接Math.round 就改成1秒整就行. 通过对比上一秒和下一秒时间戳差值.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests