这篇文章大体借鉴 参考链接
为什么想来看看setTimeout的问题,一开始是想看看vue中 $nextTick的实现的,这个问题又涉及到eventLoop的相关知识。在此之前又看过上面这篇文章,就想写一写关于setTimeout的相关的问题。
MDN的setTimeout
var timeoutID = scope.setTimeout(function[ , delay, arg1, arg2, ...]);
var timeoutID = scope.setTimeout(function[, delay]);
var timeoutID = scope.setTimeout(code[, delay]);
上面是MDN给的setTimeot的基本的用法,其中 arg1、arg2是传递给回调函数的参数
setTimeout的常见面试题
setTimeout(function(){console.log(1)},30)
setTimeout(function(){console.log(2)},10)
setTimeout(function(){console.log(3)},0)
let now = new Date();
while(new Date() - now<100){
}
console.log(0);
这个代码最后输出结果是什么样呢?输出结果是什么样的最终结果是直接取决于setTimeout的含义。如果简单的认为setTimeout就是在函数过了delay的时间之后就执行的,那么显然会输出3 2 1 0 。但是际结果并不是这样的,且听我娓娓道来。
eventLoop机制
javascript的单线程机制
要了解事件循环机制,先要来熟悉js和其他语言与众不同的单线程机制。js的单线程机制,是由于js的主要用途是和用户进行互动,操作DOM。如果是多线程的话,会存在什么问题呢?如果多线程,两个线程就可能会同时操作DOM,一个线程删除了当前的DOM,但是另一个线程又要操作DOM,那么会产生很多不必要的问题。当然,这种问题在操作系统中可以用 互斥锁机制等来解决。但是由于单线程的运行效率比较低,所以H5提出了一个web worker的标准,将任务分给子线程去执行。后面将专门写一篇文章来介绍一下H5的web worker chrome v8 内核线程:参考链接
- GUI渲染线程
- javaScript 引擎线程
- 定时触发器线程
- 事件触发线程
- 异步http请求线程
context stack & task queue
执行栈和任务队列可以参考下图
简单分析一下这个图,js的执行栈中有很多函数等待执行,同时执行这些函数的时候会有一些例如 ajax请求、setTimeout函数等。碰到这些函数的时候,V8引擎的执行过程是这样
允许JS遇到异步任务 --> 将异步任务交给对应的线程 --> 继续执行同步任务 --> 其他线程执行完异步任务后,将结果push进事件队列当中 --> 执行栈中的同步任务执行完毕后轮询事件队列,并取出第一个任务执行
主要的过程是分为这三步
- 所有同步任务都在主线程上执行,形成一个执行栈(
execution context stack)。 - 主线程之外,还存在一个"任务队列"(
task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 - 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
js如何清除所有的定时器setInterval
我们在使用 setTimeinterval的时候,会返回一个定时器的值,来标志这个定时器,可以用变量来进行接收这个值。但是如果我们没有接受定时器的值,但是依旧想要清除循环定时器怎么办呢?那么我们就可以用以下的方法来进行解决
let end = setInterval(function () { }, 10000);
for (let i = 1; i <= end; i++) {
clearInterval(i);
}
end就是我们返回的最大的定时器的值。把小于end标志的定时器都clear了,那么就是清除了当前所有的定时器了。
setTimeout函数的理解
js引擎在碰到setTimeout函数的时候,会调用定时触发线程接管setTimeout。定义触发线程定时delay之后将回调函数放入任务队列中等待执行,js引擎和定时触发线程是同步的。所以当js引擎将setTimeout交付给定时触发线程之后,依旧继续往下执行。
所以回到上面所说的这个问题中 可视化eventLoop
setTimeout(function(){console.log(1)},30) // fun1
setTimeout(function(){console.log(2)},10) // fun2
setTimeout(function(){console.log(3)},0) // fun3
let now = new Date();
while(new Date() - now<100){
}
console.log(0);
正确的输出应该是 0 3 2 1 流程应该如下
- js碰到setTimeout函数,将fun1第一个setTimeout函数交付给定时器触发线程。
- js碰到setTimeout函数,将fun2第二个setTimeout函数交付给定时器触发线程。
- js碰到setTimeout函数,将fun3第三个setTimeout函数交付给定时器触发线程。
- 定义now变量
- while 循环
- 因为while循环100ms,所以前面三个 定时器触发线程托管的函数就在这100ms中定时完毕了,交付给 task queue,入队的顺序是fun3 fun2 fun1
- 100ms的循环走完,打印输出 0
- 这时候js引擎的主线程函数执行完毕了。就开始eventLoop机制,取出task queue的队头元素fun3,执行,执行完毕后取出fun2,执行,最后执行fun1
宏任务和微任务
为什么需要微任务和宏任务?如果不将任务分类型,那么所有的任务都统一放到task queue中去了,那么就无法让一些更为重要的优先运行。其实这个思想有点类似于cpu中中断的机制,优先相应内外中断。当然只是一个在分task的思想上的类似。具体的task的运行顺序和中断的运行顺序上,还是有很大的不同的
异步任务被分为 微任务 和 宏任务 宏任务:
- script 代码块
- setTimeout()
- setInterval()
- postMesage()
- I/O(Ajax 网络I/O)
- UI 交互事件
微任务
- new Promise().then(回调);promise这块知识点我也很不会,下一篇文章就来谢谢Promise
- MutationObserver (html5的新特性,也是vue2.0中nextTick的实现方式)
运行机制
异步任务的返回结果会被放到一个task queue中,通过eventLoop机制来执行。根据异步任务的类型,这个事件会被放到对应的微任务和宏任务队列中去。
当前的执行栈为空的时候,主线程会查看微任务队列是否有事件存在
- 微任务队列中有事件存在,那么一次执行队列中的事件对应的回调函数,直到微任务队列为空。然后去宏任务队列中取出最前面的事件,把当前的回调加到当前执行栈。
- 如果当前微任务队列没有事件存在,那么直接去宏任务的队列中出队第一个事件,并把他对应加入到当前的执行栈。 当前任务的执行栈执行完毕后,会先去处理当前微任务的队列中的事件,然后再去宏任务队列中取出事件。所以在一个事件循环中,微任务永远在宏任务之前执行。
在事件循环中,没进行一次循环操作称为tick,每一次的tick的任务处理模型是比较复杂的。但关键的步骤如下。
- 执行一个宏任务(stack 中没有就去 task queue中取)
- 执行宏任务过程中产生微任务(Promise等产生),就将它添加到微任务的任务队列中去
- 宏任务执行完毕,立马执行当前微任务队列中的所有微任务。
- 上述三个步骤执行完毕,才算一个循环完毕,就是一个tick完毕。开始检查渲染,GUI线程接管渲染
- 渲染完毕之后,JS线程接管,开始下一个宏任务(JS线程和渲染线程不能同时开始)
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
参考链接 这边文章中有个特别形象的动画,大家可以看着理解一下。
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
执行的过程
- 全局代码压入栈执行,输出 start
- setTimeout 作为宏任务,交给定时器线程,setTimeout 0ms 其实就是 settimeout 1ms,可以自行验证。 1ms后 定时器线程将function作为宏任务 入队taskqueue
- promise 入队微任务
- 打印输出end,全局代码运行结束(全局代码属于宏任务),接下来执行本次的微任务
- 执行 promise的第一个callback,触发then回调的第二个微任务,执行第二个微任务的callback
- 微任务队列执行完毕,接下来执行 GUI引擎的渲染任务。渲染完毕,开启下一个宏任务,执行setTimeout的宏任务。