你需要知道的Event Loop

1,039 阅读8分钟

首先我们一开始先思考几道题,在思考题目的时候可以先想一下该题考查的是什么知识点,然后我们再来通过题目理解该知识点

js异步/事件循环/Promise

我们可以先看几个面试题,留着疑问去学习:

  • 同步和异步的区别?
  • 手写Promise加载一张图片?
  • 前端使用异步的场景有哪些?
  • 请描述event loop(事件循环/事件轮询)的机制,可画图
  • 什么是宏任务?什么是微任务,两者有什么区别?
  • Promise有哪三种状态?如何变化?

知识点:

  • 单线程和异步
  • 应用场景
  • callback hell 和Promise
  • event loop
  • promise进阶
  • async/await 的使用
  • 微任务/宏任务的 认知和区别

单线程和异步

因为js的单线程才有的异步

  • JS是单线程语言,只能同时做一件事儿

    比方说,我们弄一个ajax请求,一个定时器先等待一秒钟,
    如果是同步的话,这里在加载图片或者ajax过程中就卡住了,
    鼠标点击不了,拖不动,js也不执行,这就是单线程语言的本质。
    
  • 浏览器和nodejs已支持js启动进程,如Web Worker;但也不能改变js是单线程这种本质

  • 因为JS可修改DOM结构,所以js的DOM渲染共用同一个线程。js执行过程中DOM必须停止,DOM渲染过程中js必须停止

      所以,当我们遇到等待(网络请求,定时任务)的时候不能卡住,
      所以就需要异步,解决js单线程的问题,回调callback函数形式
      
      同步异步的例子:异步可以开一个定时器或者请求数据,
      同步的话可以放一个alert(。。),会发现alert后面的代码被堵塞了。
      
    

异步和同步:

  • 基于JS是单线程语言
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行

应用场景

  • 网络请求,如 ajax,图片加载
  • 定时任务,如 setTimeout 网络请求:
console.log('start')
$.get('./detail.json',function(data1){
    console.log(data1)
})
console.log('end')

图片加载:

console.log('start')
let img = document.createElement('img')
img.onload = function(){
    console.log('loaded')
}
img.src = '/xxx.png'
console.log('end')

定时器~

image.png

callback hell回调地狱

老式写法嵌套一层又一层: image.png 很复杂,于是产生了peomise:

image.png

peomise解决了回调地狱:也就是解决callback嵌套的问题; image.png

手写Promise加载一张图片


function loadImg(src){
    return new Promise(
    // 一开始的时候 是pending状态
        (resolve,reject) => {
            // resolve,reject这两个也是函数,一个成功的执行,一个失败的执行
            const img = document.createElement('img')
            img.onload = () => {
                resolve(img)// 这里是resolved状态
            }
            img.onerror = () => {
                reject(new Error(`图片加载失败${src}`))// 这里是reject状态
            }
            img.src = src
        }
    )
}


loadImg(url)// 返回的是一个promise对象,所以可以then
const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'

loadImg(url1).then(img1 => {
    console.log(img1.width)
    return img1 // 返回一个普通对象
}).then(img1 => { // 这里的参数是一个普通对象
    console.log(img1.height)
    return loadImg(url2) // 这里返回一个Promise实例
}).then(img2 => {
    console.log(img2.width)
    return img2
}).then(img2 => {
    console.log(img2.height) 
}).catch(ex => console.error(ex))

Promise

(一)Promise的状态

  • promise有三种状态
  • 状态的表现和变化
  • then和catch对状态的影响
三种状态和变化:
pending          resovled            rejected
在过程中还没结果   已经解决了          已经失败了

pending-> resolved或pending->rejected
变化不可逆
// 代码演示:
const p1 = new Promise((resolve,reject) =>{})
console.log('p1',p1) // p1 Promise{<pending>}


const p2 = new Promise((resolve,reject) =>{
    setTimeout(()=>{
        resolve()
    })
})
console.log('p2',p2) // p2 Promise{<pending>} 依然是一个pending
// 但我们点开以后,里面的PromiseStatus是一个resolved
// 因为这里是一个异步,先打印,后执行完毕,所以一开始还是一个pending
setTimeout(()=>{console.log('p2-setTimeout',p2)})
// p2-setTimeout Promise{<resolved>:undefined} 我们在异步里打印的话能打印出它真实的状态


const p3 = new Promise((resolve,reject) =>{
    setTimeout(()=>{
        reject()
    })
})
console.log('p3',p3) // p3 Promise{<pending>},点开后,里面的PromiseStatus是一个rejected
setTimeout(()=>{console.log('p3-setTimeout',p3)})
// p3-setTimeout Promise{<rejected>:undefined}
  1. pengding状态,不会触发then和catch
  2. resolved状态,会触发后续的then回调函数
  3. rejected状态,会触发后续的catch回调函数
// 现在我们想直接拿到一个resolved
const p1 = Promise.resolve(100) // resolved
console.log('p1',p1)//p1 Promise{<resolved>:100}
p1.then(data => {
    // resolve只会走.then回调
    console.log('data',data) // data 100 
}).catch(err => {
    console.error('err',err) // resolve不会走这边
})


const p2 = Promise.reject('err') // rejected
// 因为这里不是.catch去接收信息,所以内部抛出错误,直接rejected状态
console.log('p2',p2)//p2 Promise{<rejected>:'err'} 然后报错!
p2.then(data => {
    console.log('data2',data)  // rejected不会走这边
}).catch(err => {
    // rejected只会走.catch回调
    console.error('err2',err) // err2 'err'
})

(二)Promise的then和catch如何影响状态的变化

  • then正常返回resolved,若里面有报错,则返回rejected
  • catch正常也返回resolved,若里面有报错,则返回rejected 这两句话虽然很简单,但我们还是从下面的代码中去理解一下:
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
    // resolved会触发then回调
    return 100
})


// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
    throw new Error('err')
})


// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
    // rejected会触发catch回调
    console.error('catch some error')
})


// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
    throw new Error('err')
})

async/await

  1. 一开始因为有异步回调callback hell(嵌套写法)
  2. 然后我们使用Promise then catch链式调用,但也是基于回调函数的(链式写法)
  3. async/await是同步语法,彻底消灭回调函数(同步写法)
// 我们继续看之前写的这个方法
function loadImg(src) {
    const promise = new Promise((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = () => {
            resolve(img)
        }
        img.onerror = () => {
            reject(new Error(`图片加载失败 ${src}`))
        }
        img.src = src
    })
    return promise
}

// 注意,在控制台中输入
alert
(
)
也是会触发这个方法的,
所以我们在日常书写js中,写自调用的时候如果因为这个符号的问题报错,我们可以在前面加一个!号:
async function loadImg3(){
    const src3 = 'http://www.imooc.com/static/img/index/logo_new.png'
    const img3 = await loadImg(src3)
    return img3
}
!(async function loadImg1() {
    const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
    const img1 = await loadImg(src1) // promise对象
    const src2 = 'http://www.imooc.com/static/img/index/logo_new.png'
    const img2 = await loadImg(src2)
    
    const img3 = await loadImg3() // async函数
    console.log(img1, img2,'同步写法')
})()
// await不仅后面可以追加 还可以追加promise对象

async/await 和 Promise的关系

  • async/await 是消灭异步回调的终极武器
  • 和Promise并不互斥
  • 两者相辅相成
// async函数 返回的是Promise对象
async function fn1() {
    return 100 
    //如果返回的是一个值的话,它会封装成一个Promise对象去返回
    // return Promise.resolve(100) 两者返回的都是Promise对象
    // 如果返回的是一个Promise对象的话,它会原封不动的去返回
}
const res1 = fn1()
console.log(res1) // Promise{<resolved>:100} -> 执行async函数返回的是一个Promise对象


// await后跟promise,值,async函数 这三种情况都可以
!(async function(){
    const p1 = Promise resolve(300)
    const data = await p1 // await 相当于Promise then
    const data1 = await 400 // await 会把400 封装成Promise对象去执行Promise.then
    const data2 = await fn1() // async函数
    console.log('data',data,data1,data2) // data 300  400  100
})()

// try...catch...相当于Promise的catch
!(async function (){
    const p4 = Promise.reject('err1') // rejected
    try {
        const res = await p4
        console.log(res)
        // 这里肯定走不进来,仅为await相当于then,只执行resolve状态的
    } catch (ex){
        console.error(ex) // err1
    }
    
})()

总结,两者相辅相成的原因:

  1. 执行async函数,返回的是Promise对象
  2. await相当于Promise的then
  3. try...catch 可捕获异常,替代了Promise的catch

异步的本质

event loop

  1. 场景题-promise then 和 catch 的连接 image.png
  2. 场景题- async/await 语法 image.png
  3. 场景题- promise/setTimeout 的顺序 image.png
  4. 场景题- 外加async/await 的顺序问题(思考输出什么) image.png

event loop (事件循环/事件轮询)

  • js是单线程运行的
  • 异步要基于回调来实现
  • event loop就是异步回调的实现原理 JS如何执行?
  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步(通过回调实现异步)

讲event loop之前先把这段代码看熟了

console.log('hi')
setTimeout(function cd1(){
    consol.log('cd1')
},5000)
console.log('bye')

然后这张图是我们执行代码的流程图示

image.png

  1. 同步代码的执行。首先,我们第一行代码console.log('Hi')会被推入调用栈(1)中,然后调用栈执行这行代码,会在控制台(2)输出处展示Hi,执行完后调用栈(1)会把第一行代码弹出调用栈
  2. 异步代码的处理。接着第二行代码setTimeout会被放入执行栈中,内部有一个函数cb1.但因为这里是一个异步,会把timer(cb1)放到WebAPIs(3)中,执行栈弹出setTimeout,5秒钟后会把timer(cb1)放到函数队列中。
  3. 剩余同步代码的执行。然后第三部分代码console.log('Bye')被推入调用栈中,调用栈执行该行代码,在控制台输出,执行完毕后弹出调用栈。
  4. 继续处理异步代码。但这个时候第二部分代码还在WebAPI中;因为这个时候我们的调用栈被清空了,里面没有任何事件了,这个时候会启动我们的event loop机制,一旦我们的同步代码被执行完,调用栈被清空,我们的event loop机制会立马启动。event loop会一遍一遍的循环,会从函数队列中去寻找有没有函数需要执行;
  5. 异步代码的执行。等到事件触发的时候,事件会从WebAPI弹到函数队列;eventloop就转呀转发现函数队列中有事件,这个时候eventloop就立马把这个函数拿到执行栈中执行。执行栈会对 timer(cb1)进行分析,分为函数cb1和console.log两部分;先把内部的console执行了,在控制台输出打印内容,然后清掉console的执行,这个时候其实函数内只有一行打印代码,所以相当于该函数执行完了;所以接着把函数cb1弹出执行栈。整个代码执行完毕。

总结

  1. 同步代码,一行一行放在CallStack执行
  2. 遇到异步,会先“记录”下,弹到WebAPIs,等待时机(定时,网络请求等)
  3. 时机一到,就会移动到Callback Queue中
  4. 如果Call Stack 为空(即同步代码执行完),Event Loop开始工作
  5. Event Loop轮询查找Callback Queue,如果有事件,则移动到Call Stack执行
  6. Event Loop继续轮询查找(像永动机一样)

DOM事件和event loop

只要用了回调就基于event loop

  • JS是单线程
  • DOM事件也是基于event loop的,因为其实也使用回调(比如什么时候用户点击啦什么的)
  • 异步(setTimeout,ajax等等)也是基于event loop的