解释这个图片
浏览器中的事件循环就是这个图片。事件循环可视化展示
JavaScript 中的事件循环指的是浏览器处理并发模型(管理多个任务的的方法),包括了执行栈(call stack)、收集和处理任务(web APIs),任务队列(callback queue,也可称为回调队列、消息队列)、event loop 这些部分。event loop 会监控 call stack 和 queue,当 stack 为空的时候,将从 callback queue 中获取任务并放到 stack 中执行。这是一个无限循环的过程。
为什么 JS 是单线程的
JavaScript 是单线程的,因为它本来是作为浏览器执行脚本,主要用途是和用户互动、操作 DOM。如果是多线程的,比如一个线程在某个 DOM 节点上添加内容,另一个线程删除这个节点,这时候浏览器以哪个为准呢?为了避免这样复杂的问题,JavaScript 就被设计成了单线程的。
什么是任务(Tasks)
一个 任务 就是由执行诸如从头执行一段程序、执行一个事件回调或一个 interval/timeout 被触发之类的标准机制而被调度的任意 JavaScript 代码。这些都在 **任务队列(task queue)**上被调度。
以下时机(这里宿主环境是浏览器),任务会被添加到任务队列:
- 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个
<script>
元素中运行代码)。- 触发了一个事件,将其回调函数添加到任务队列时。
- 执行到一个由
setTimeout()
或setInterval()
创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时
简单来说可以把下面这些看做任务(宏任务)
- 定时器
- IO操作(读写文件)
- DOM 事件
什么是微任务(Microtasks)
- Promise 有结果的时候产生微任务
- 通过
queueMicrotask()
想队列加入微任务 MutationObserver
监控DOM节点,产生的微任务
简单来说就是 JS 发起的任务
判断打印顺序的练习
其他相关问题
定时器的时间间隔准吗
只能确保回调函数不会再指定的事件间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。——《你不知道的 JavaScript》 p143
用 setTimeout 代替 setInterval
- 为什么要替代?
搞清楚了事件循环,就会知道 setInterval 也是等待 call stack 为空的时候,才会执行回调,如果前面的代码执行太久了,超出了给 setInterval 设定的时间间隔,此时回调函数已经进入队列,等 stack 终于为空的时候就会立即执行队列中的回调函数,这时候 web APIs 中的 setInterval 计时也已经在计时了一段时间了,很快又会把回调函数放入队列,就会发现怎么上一次执行完后,没到以为的事件间隔又执行了······· 另外,如果 setInterval 本身给定的回调函数执行的时间比设定的时间间隔长,也会带来这样的问题,失去了设置时间间隔的意义。
- 如何代替?
思路
可以参考这个视频,思路说得很详细JavaScript用setTimeout模拟实现setInterval ,不过视频中没有说如何清除定时器,下面记录下实现清除定时器的思考过程。
设计接口:调用方式和 setInterval 一样
使用:timer = mySetInterval(fn, delay)
清除:myClearInterval(timer)
// 方法一(这种不好想清除定时器,因为不好设置全局的 timer,视频中没有说这个方法)
function mySetInterval(fn, delay) {
setTimeout(() => {
console.log(new Date().toLocaleString()) // 可以打印时间看看
fn()
mySetInterval(fn, delay)
}, delay)
}
// 测试
function testmySetInterval() {
console.log('testmySetInterval')
}
mySetInterval(testmySetInterval, 1000)
复制代码
方法一不好清除定时器,因为 mySetInterval 肯定要范围一个 timer 才能清除对不对?来看方法二。
// 方法二(视频的方法,没有写怎么清除定时器)
function mySetInterval(fn, delay) {
function inside() {
console.log(new Date().toLocaleString()) // 可以打印时间看看
fn()
setTimeout(inside, delay)
}
setTimeout(inside, delay)
}
// 开始想:
// 假如 mySetInterval 需要返回一个 timer,因为使用方式是 timer = mySetInterval(fn, delay),这样一写 timer 就被固定了对不对?
function mySetInterval(fn, delay) {
let timer = null
function inside() {
clearTimeout(timer)
fn()
timer = setTimeout(inside, delay)
}
timer = setTimeout(inside, delay)
return timer // timer = mySetInterval(fn, delay) 的时候 timer 被固定
}
// mySetInterval 只调用了一次,这样的直接返回的永远都是第一个 setTimeout 的 timer。如何让 timer 不固定呢?对象!返回一个对象 clearTimeout() 作为属性值返回!属性值是个方法,清除定时器,就是让myClearInterval 调用这个方法!这样写:
function mySetInterval(fn, delay) {
let timer = null
function inside() {
console.log(new Date().toLocaleString()) // 打印看看时间
clearTimeout(timer) // 把上一次的 timer 掉,这里使用了闭包, inside 访问了不属于自己作用域的变量,也就是 mySetInterval 下的 timer
fn()
timer = setTimeout(inside, delay)
}
timer = setTimeout(inside, delay)
return { // 返回一个对象 clearTimeout() 作为属性值返回!
clear() {
clearTimeout(timer)
}
}
}
// 清除定时器
function myClearInterval(flagTimer) {
flagTimer.clear()
}
// 测试
function testmySetInterval() {
console.log('testmySetInterval')
}
const timer = mySetInterval(testmySetInterval, 1000)
// 控制台直接调用 myClearInterval(timer)
复制代码
封装
function mySetInterval(fn, delay) {
let timer = null
function inside() {
console.log(new Date().toLocaleString()) // 打印看看时间
clearTimeout(timer)
fn()
timer = setTimeout(inside, delay)
}
timer = setTimeout(inside, delay)
return {
clear() {
clearTimeout(timer)
}
}
}
// 清除定时器
function myClearInterval(flagTimer) {
flagTimer.clear()
}
// 测试
function testmySetInterval() {
console.log('testmySetInterval')
}
const timer = mySetInterval(testmySetInterval, 1000)
// 控制台直接调用 myClearInterval(timer)
复制代码
requestAnimationFrame 代替定时器
const RAF = {
intervalTimer: null,
timeoutTimer: null,
setTimeout(cb, interval) { // 实现setTimeout功能
let now = Date.now
let stime = now()
let etime = stime
let loop = () => {
this.timeoutTimer = requestAnimationFrame(loop)
etime = now()
if (etime - stime >= interval) {
cb()
cancelAnimationFrame(this.timeoutTimer)
}
}
this.timeoutTimer = requestAnimationFrame(loop)
return this.timeoutTimer
},
clearTimeout() {
cancelAnimationFrame(this.timeoutTimer)
},
setInterval(cb, interval) { // 实现setInterval功能
let now = Date.now
let stime = now()
let etime = stime
let loop = () => {
this.intervalTimer = requestAnimationFrame(loop)
etime = now()
if (etime - stime >= interval) {
stime = now()
etime = stime
cb()
}
}
this.intervalTimer = requestAnimationFrame(loop)
return this.intervalTimer
},
clearInterval() {
cancelAnimationFrame(this.intervalTimer)
}
}
let count = 0
function a() {
console.log(count)
count++
}
RAF.setTimeout(a, 1000)
复制代码
参考
Concurrency model and the event loop
What is the Event Loop in JavaScript?