笔记:事件循环及相关问题

Event Loop

解释这个图片

浏览器中的事件循环就是这个图片。事件循环可视化展示

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?

深入:微任务与Javascript运行时环境

在 JavaScript 中通过 queueMicrotask() 使用微任务

深入理解JS的事件循环

第 133 题:用 setTimeout 实现 setInterval,阐述实现的效果与setInterval的差异

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享