同步异步,宏任务微任务,事件循环机制,带你一文全部弄清楚!

784 阅读5分钟

这是我参与 8 月更文挑战的第 11 天,活动详情查看: 8月更文挑战

宏任务、微任务、同步任务、DOM渲染执行顺序

image.png

  1. 程序先程序 Call Stack 里面的内容,待 Call Stack 清空时,执行当前的微任务;

  2. 程序找到微任务队列的任务,执行微任务;

  3. 待微任务执行完毕后,尝试执行DOM渲染;

  4. DOM 渲染结束后,触发 event loop ,执行宏任务。

名称举例(常用)
宏任务script、setTimeout 、setInterval 、setImmediate、Ajax、DOM事件、I/O、UI Rendering
微任务process.nextTick()、Promise、async/await

单线程和异步

  • js是单线程语言,只能同时做一件事
  • 浏览器合nodejs已支持js 启动进程,如web worker
  • js和DOM渲染共用同一个线程,因为JS可修改DOM结构
  • 遇到等待(网络请求,定时任务)不能卡住
  • 需要异步
  • 回调callback函数形式

异步使用场景

  • 网络请求,如ajax、图片加载
  • 定时任务,如setTimeout

同步和异步的区别是什么?

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

回调地狱 callback hell

Promise

解决callback hell,可链式调用

function getData(url){
   return new Promise((resolve,reject)=>{
       $.ajax({
           url,
           success(data){
               resolve(data)
           },
           error(err){
               reject(err)
           }
       })
   })
}
let url1 = '/data1.json'
let url2 = '/data2.json'
let url3 = '/data3.json'
getData(url1).then(data1=>{
   console.log(data1)
   return getData(url2) // promise实例
}).then(data2=>{
   console.log(data2)
   return getData(url3)
}).then(data3=>{
   console.log(data3)
}).catch(err=>{
   console.error(err)
})

Promise有哪三种状态?如何变化?

  • 三种状态:pending resolved rejected
    • pending-> resolved或者pending->rejected
    • 变化不可逆
  • 状态的表现和变化
    • pending状态,不会触发then和catch
    • resolved状态,会触发后续的then回调函数
    • rejected状态,会触发后续的catch回调函数
  • then 和 catch对状态的影响
    • then 正常返回 resolved,里面有报错则返回rejected
    • catch 正常返回 resolved,里面有报错则返回rejected
//code 1
Promise.resolve().then(()=>{
    console.log(1)
}).catch(()=>{
    console.log(2)
}).then(()=>{
    console.log(3)
})
// 返回 1 3

// code2
Promise.resolve().then(()=>{
    console.log(1) 
    throw new Error('error1')
}).catch(()=>{
    console.log(2)
}).then(()=>{
    console.log(3)
})
//返回 1 2 3

//code3
Promise.resolve().then(()=>{
    console.log(1) 
    throw new Error('error1')
}).catch(()=>{
    console.log(2)
}).catch(()=>{
    console.log(3)
})
//返回1 2

手写用Promise加载一张图片

const url = 'https://inews.gtimg.com/newsapp_bt/0/11327712791/641'

function loadImg(src){
    return new Promise((resolve,reject)=>{
        const img = document.createElement('img')
        img.onload = ()=>{
            resolve(img)
        }
        img.onerror = ()=>{
            const err = new Error(`图片加载失败${src}`)
            reject(err)
        }
        img.src = src
    })
}

loadImg(url).then(img=>{
    console.log(img.width)
    return img //普通对象
}).then(img=>{
    console.log(img.height)
}).catch(ex=>console.error(ex))

event loop事件循环/事件轮询

  • js是单线程运行的
  • 异步要基于回调来实现
  • event loop就是异步回调的实现原理

js是如何执行的?

  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步

event loop过程

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

DOM事件和event loop

  • js是单线程的
  • 异步(setTimeout,ajax等)使用回调,基于event loop
  • DOM事件也使用回调,基于event loop,不是异步

async/await

  • 异步回调 callback hell
  • Promise then catch链式调用,但也是基于回调函数
  • async/await是同步语法,彻底消灭回调函数
// loadImg的方法见上方Promise
!(async function(){
    //同步的语法
    
    const img1 = await loadImg(src1)
    console.log(img1.height,img1.width)
    
    const img2 = await loadImg(src2)
    console.log(img2.height,img2.width)
})()

async/await 和 Promise的关系

  • async/await是消灭异步回调的终极武器
  • 和Promise并不互斥
  • 两者相辅相成
  • 执行async函数,返回的Promise对象
  • await相当于Promise的then
  • try...catch可捕获异常,代替了Promise的catch
!(async function(){
   const p4 = Promise.reject('err1') // rejected状态
   
   const res = await p4  // await -> then
   console.log('res',res) //不执行
   
   try{
       const res = await p4
       consle.log(res)
   }catch(ex){
       console.log(ex) // try...catch相当于promise catch
   }
   //err1
   
})()

异步的本质

  • async/await是消灭异步回调的终极武器
  • js还是单线程,还是得有异步,还得是基于event loop
  • async/await只是一个语法糖
  • await的后面,都可以看作是 callback里的内容,即异步
async function fn(){
    return 100
}
!(async function(){
    const a = fn() // Promise
    const b= await fn() //100
})()
async function async1(){
    console.log('async1 start') // 2 重要
    await async2() //undefined
    //await的后面,都可以看作是 callback里的内容,即异步
    //类似,event loop,setTimeout(cb1)
    // setTimeout(function(){console.log('async1 end')})
    //Promise.resolve().then(()=> console.log('async1 end') )
    console.log('async1 end') //5
}
async function async2(){
    console.log('async2') // 3 重要
}
console.log('script start') //1
async1()
console.log('script end') //4
async function async1(){
    console.log('async1 start')  //2
    await async2() 
    
    //下面3行都是异步回调 callback的内容    
    console.log('async1 end') //5
    await async3()
        //下面1行是异步回调的内容
        console.log('async1 end2') //7
}
async function async2(){
    console.log('async2') //3
}
async function async3(){
    console.log('async3') //6
}
console.log('script start') //1
async1()
console.log('script end') //4 同步代码执行完,event loop执行异步

for ... of异步遍历

  • for...in (及forEach for)是常规的同步遍历
  • for...of常用于异步的遍历
function muti(num){
    return new Promise(resolve=>{
        setTimeout(()=>{
            resolve(num*num)
        },1000)
    })
}

const nums = [1,2,3]

nums.forEach(async(i)=>{
    const res =await muti(i)
    console.log(res) // 一次性输出 1 4 9 
})

!(async function(){
    for(let i of nums){
        const res =await muti(i)
        console.log(res) // 每隔1s输出 1 4 9 
    }
})()

宏任务 macroTash 和微任务 microTask

什么是宏任务?什么是微任务?

  • 宏任务:setTimeout, setInterval, Ajax , Dom事件
  • 微任务:Promise, async/await
  • 微任务执行时机比宏任务要早

event loop和DOM渲染

  • js是单线程的,而且和DOM渲染共用一个线程
  • js执行的时候,得路一些时机供DOM渲染

执行顺序

  1. Call Stack空闲
  2. 执行当前的微任务
  3. 尝试DOM渲染
  4. 触发Event loop

微任务为什么比宏任务执行更早

  • 每次Call Stack清空(即每次轮询结束),即同步任务执行完
  • 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
  • 然后再去触发下一次Event loop

alert会阻断js执行,也会阻断DOM渲染

微任务和宏任务的区别

  • 宏任务: DOM渲染后触发, 如setTimeout; 是由浏览器规定的
  • 微任务: DOM渲染前触发,如Promise;是ES6规定的
async function async1(){
    console.log('async1 start') // 2
    await async2()
    //await后面的都作为回调内容- 微任务
    console.log('async1 end') //6
}
async function async2(){
    console.log('async2') // 3
}
console.log('script start') //1

setTimeout(function(){ //宏任务
    console.log('setTimeout') //8
},0)

async1()

// 初始化Promise时,传入的函数会立即执行
new Promise(function(resolve){
    console.log('promise1')  // 4
    resolve()
}).then(function(){ //微任务
    console.log('promise2')  // 7
})
console.log('script end') //5

//执行到5,同步代码执行完毕(event loop - call stack被清空)
// 执行微任务
// (尝试触发DOM渲染)
// 触发event loop,执行宏任务