# JS 异步进阶(大厂)

代码笔记 (opens new window)

# 1. 简述

  • 本章主要讲JS异步的原理和进阶
  • 符合一线互联网公司基层员工必须要求掌握的知识点
  • 几乎也算是面试必考内容之一

# 2. 题目

# 2.1. 问答题

  • 请描述event loop(事件循环/事件轮询)的机制,可画图
    • 什么叫事件循环
    • event loop在异步中起到什么作用,和异步有什么关系
  • 什么是宏任务和微任务,两者有什么区别
  • Promise有哪三种状态?如何变化

# 2.2. 场景题

  • promise then和catch的连接(经常考)
    • 经常考到,业务中可能会用,答不出来会减分很多
    // 第一题
    Promise.resolve().then(() => {
      console.log(1)
    }).catch(() => {
      console.log(2)
    }).then(() => {
      console.log(3)
    })
    
    // 第二题
    Promise.resolve().then(() => {  
      console.log(1)
      throw new Error('erro1')
    }).catch(() => {
      console.log(2)
    }).then(() => {
      console.log(3)
    })
    
    // 第三题
    Promise.resolve().then(() => {
      console.log(1)
      throw new Error('erro1')
    }).catch(() => {
      console.log(2)
    }).catch(() => {
      console.log(3)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
  • async/await语法问题
    // 题目一
    async function fn() {
      return 100
    }
    (async function() {
      const a = fn()  // ??
      const b = await fn()  // ??
    })
    
    // 题目二
    (async function() {
      console.log('start')
      const a = await 100
      console.log('a', a)
      const b = await Promise.resolve(200)
      console.log('b', b)
      const c = await Promise.reject(300)
      console.log('c', c)
      console.log('end')
    })  // 执行完毕,打印出哪些内容
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  • promise和setTimeout的顺序
    • 经典题
    • 常考,答不出来有些危险
    console.log(100)
    setTimeout(()=> {
      console.log(200)
    })
    Promise.resolve().then(()=> {
      console.log(300)
    })
    console.log(400)
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 外加async/await的顺序问题
    • 最难的一个题,据说是头条面试题
    • 很综合的一个题目
    • 已经达到一线互联网公司对高级前端对异步广度和深度理解的难度
    async function async1() {
      console.log('async1 start')
      await async2()
      console.log('async1 end')
    }
    async function async2 () {
      console.log('async2')
    }
    console.log('script start')
    setTimeout(function() {
      console.log('setTimeout');
    }, 0)
    
    async1()  // async1 start - async2 - async1 end - script start - setTimeout ?
    
    new Promise(function(resolve) {
      console.log('promise1')
      resolve()
    }).then(function() {
      console.log('promise2')
    })
    
    console.log('script end');
    
    // 依次打印什么内容
    // 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

# 3. 知识点

# 3.1. event loop(事件循环/事件轮询)

# 3.1.1. 基础概念

  • 简述
    • 偏原理,几乎必问
    • JS是单线程运行的
    • 异步要基于回调来实现
    • event loop就是异步回调的实现原理
  • JS如何执行 => 有助于梳理event loop过程
    • JS代码它是从前到后,一行一行执行
    • 如果某一行执行报错,则停止下面代码的执行
    • 先把同步代码执行完,再执行异步
      • 通过回调执行异步
      • 回调如何实现 => 事件循环机制
  • 示例
    • event loop的流程比较复杂,我们可以通过简单的示例来进行梳理
    console.log('Hi')
    
    setTimeout(function cb1() {
      console.log('cb1')  // cb即callback
    }, 5000)
    
    console.log('Bye');
    
    1
    2
    3
    4
    5
    6
    7
  • 方法
    • 不要扣细节,不要扩大范围,核心是event loop的过程
  • 开始讲解事件循环过程
    • 图示1 => 最初的状态 event-loop1
      • Browser console => 浏览器环境console界面
      • Call Stact => js运行中重要的模块
        • 调用栈
      • Web Apis => => js运行中重要的模块
        • ECMA规范之外,浏览器定义的api相关(DOM/BOM)
      • Callback Queue => js运行中重要的模块
        • 回调函数的队列
      • Event Loop
        • 一遍一遍循环事件
    • 开始执行第一行代码
      • console.log('Hi')
      • js会把代码推入调用栈中,Call Stact会去执行这个代码
      • 调用完之后在浏览器环境console界面打印hi
      • 代码执行完毕之后清空调用栈
    • 执行第二行代码
      • setTimeout cb1
      • setTimeout是一个函数,我们执行这个函数的时候
      • 即,将这个setTimeout cb1函数(三行)放到调用栈中
      • 但控制台不打印,因为暂时还执行不到setTimeout里面的console
      • 执行setTimeout函数的时候,由于setTimeout是浏览器定义的
      • setTimeout的第一个参数cb1(异步方法)被放到了Web Apis中的定时器里面
        • 其实遇到setTimeout他的产出就是把方法放到定时器里面,里面是什么方法不管
      • 定时器的时间是5s,5s之后他把这个cb1放到Callback Queue(回调函数队列)中
      • 此时setTimeout cb1这个方法就结束了
      • 代码执行完毕之后清空调用栈
    • 执行最后一行代码
      • 和执行第一行代码一样
    • 执行完最后一行代码之后
      • 此时这个timer定时器还在web apis里面
      • 此时代码已经全部执行完了,即调用栈中没有东西可以执行了
      • 一旦调用栈空了,这时候会启动Event Loop机制
        • 即所有的同步代码执行完,调用栈空了
        • 浏览器内核启动事件循环机制
      • 启动事件循环机制
        • 事件循环机制会一遍一遍地进行循环
        • 每次循环他会从Callback Queue(回调函数队列)中去找有没有函数,有函数的话就拿过来
        • 拿过来之后再放到Call Stact(调用栈)中执行
        • 5s还没到,回调函数队列中是空的,调用栈里面就没东西
        • 5s到了之后,定时器把cb1函数推到回调函数队列,此时回调函数队列中有函数,事件循环机制把函数拿过来,再推到调用栈中
        • 此时调用栈中他是一个函数体(cb1函数),此时再一行行执行代码
        • console.log('cb1')
          • 此时和第一行代码一样,将console放到调用栈中,打印
          • 打印完之后,当前那行代码在执行栈被清空
          • 当那行代码执行完,方法也被执行完了,整个方法在调用栈中被清空
      • 此时所有代码执行完毕
  • 总结event loop过程
    • 同步代码,一行行放在call stack(调用栈)执行
    • 遇到异步,会先记录下,等待时机(定时、网络请求等)
    • 时机到了,就移动到Callback Queue(回调函数队列)
    • 如果call stack(调用栈)为空(即同步代码执行完),Event Loop开始工作
    • 轮询查找回调函数队列,如果有则移动到执行栈执行
    • 然后继续轮询查找(永动机一样)
      • 这样异步就可以永远执行下去

# 3.1.2. DOM事件和event loop

  • 示例:
<button id="btn1">提交</button>

<script>
$('#btn1').click(e=> {
  console.log('button clicked');
});

console.log('Bye');
</script>
1
2
3
4
5
6
7
8
9
  • 触发回调的时机不一样(事件=> 点击、鼠标移入等用户交互)
    • 页面加载实际上click是立即就执行的($('#btn1').click)
    • 但里面函数不执行,只是将事件放入web api中
    • 其余和上面讲述的相同
    • 触发时机(请求,事件,定时器等)是由浏览器控制的
  • DOM事件和event loop关联性很强
    • DOM事件是基于event loop实现的
  • 注意事项
    • JS是单线程的
    • 异步(setTimeout,ajax等)使用回调,基于event loop
    • DOM事件也使用回调,基于event loop
      • DOM事件虽然是基于event loop实现,但它不是异步
    • 总结来讲,只要是基于回调,就是基于event loop来实现

# 3.2. promise进阶

# 3.2.1. 三种状态

  • pending
    • 过程中(还没有结果)
  • resolved
    • 已解决(成果)
  • rejected
    • 被拒绝(失败)
  • 过程
    • pending => resolved或pending => rejected
    • 变化不可逆(成功或者失败都回不去了)
  • 示例
    // pending
    const p1 = new Promise((resolve, reject)=> {
        
    })
    console.log('p1', p1);  // pending状态
    
    // resolve
    const p2 = new Promise((resolve, reject)=> {
        // 模拟异步
        setTimeout(()=> {
            // 在异步中执行成功回调
            resolve();
        });
    })
    // 异步中的方法块还没有执行
    console.log('p2', p2);  // 一开始打印时,pending状态
    // 在异步中执行成功回调,状态改变
    setTimeout(()=> console.log('p2-setTimeout', p2))
    
    // reject
    const p3 = new Promise((resolve, reject)=> {
        // 模拟异步
        setTimeout(()=> {
            // 在异步中执行失败回调
            reject();
        });
    })
    // 异步中的方法块还没有执行
    console.log('p3', p3);  // 一开始打印时,pending状态
    // 在异步中执行失败回调,状态改变
    setTimeout(()=> console.log('p3-setTimeout', p3))
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

# 3.2.2. 状态的变化和表现

  • pending状态,不会触发then和catch
  • resolved状态,会触发后续的then回调函数
  • rejected状态,会触发后续的catch回调函数
  • Promise使用技巧
    • 直接使用resolve
    const p1 = Promise.resolve(100)
    console.log('p1', p1);
    // 后面加catch不会触发,因为只有resolve状态
    p1.then(data=> {
        console.log("data", data);
    })
    
    1
    2
    3
    4
    5
    6
    • 直接使用reject
    const p2 = Promise.reject('err2')
    console.log('p2', p2);
    p2.catch(err=> {
        console.log("err", err);
    });
    
    1
    2
    3
    4
    5

# 3.2.3. then和catch改变状态

  • then正常返回resolved,里面有报错则返回rejected
  • catch正常返回resolved,里面有报错则返回rejected
  • 代码演示
    // then正常返回resolved,里面有报错则返回rejected
    const p1 = Promise.resolve().then(()=> {
        return 100
    })
    console.log("p1", p1);  // resolve状态 触发后续then的回调
    // 因为p1是resolve状态
    p1.then(()=> {
        console.log('p3 2333');  // 触发后续then的回调
    })
    
    const p2 = Promise.resolve().then(()=> {
        throw new Error('then error p2')
    })
    console.log("p2", p2);  // rejected状态 触发后续catch的回调
    // 因为p1是rejected状态
    p2.then(()=> {
        console.log('24444');  // 无法触发
    }).catch(err=> {
        console.log('err', err);  // 可以触发
    })
    
    // catch正常返回resolved,里面有报错则返回rejected
    const p3 = Promise.reject('my error').catch(err=> {
        console.log('err', err);
    });
    console.log("p3", p3);  // 此时的状态是resolved - 重点!
    // 因为p3是resolve状态
      p3.then(()=> {
        console.log('p3 2333');  // 触发后续then的回调
    })
    
    const p4 = Promise.reject('my error').catch(err=> {
        throw new Error('then error p4')
    });
    console.log("p4", p4);  // 此时的状态是reject
    // 因为p4是rejected状态
    p4.then(()=> {
        console.log('24444');  // 无法触发
    }).catch(err=> {
        console.log('err', err);  // 可以触发
    })
    // p4.catch()里面没有报错,即它的promise状态也是resolved
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
  • 总结
    • 不管是then还是catch,只要是正常返回,promise状态就都是resolved,只要报错,promise状态就都是rejected
  • 面试题解答
    // 第一题
    // Promise.resolve属于resolved状态,会触发then
    Promise.resolve().then(() => {  
        console.log(1)  // 1 resolve状态的promise不会执行catch
    }).catch(() => {
        console.log(2)  // 不执行
    }).then(() => {
        console.log(3)
    })
    // 整个promise执行完之后,返回的也还是resolved状态的promise
    // 1、3
    
    // 第二题
    Promise.resolve().then(() => {  
        console.log(1)  // 1 
        throw new Error('erro1')  // 报错,那么整个then方法中的promise状态就是rejected,会执行catch回调
    }).catch(() => {
        console.log(2)  // 2 无论catch还是then,只要不报错,返回的promise状态就是resolved,执行then回调
    }).then(() => {
        console.log(3)  // 3
    })  // 整个返回的也是resolved状态的promise
    // 1、2、3
    
    // 第三题
    Promise.resolve().then(() => {  // rejected 触发catch回调
        console.log(1)  // 1
        throw new Error('erro1')
    }).catch(() => {
        console.log(2)  // 2 resolve 触发then回调
    }).catch(() => {
        console.log(3)
    })
    // 1、2
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

# 3.2.4. promise总结

  • 三种状态,状态的表现和变化
    • pending、resolved、rejected
    • 变化
      • pending => resolved
      • pending => rejected
    • 表现
      • pending不会触发任何回调
      • resolved会触发then回调
      • rejected会触发catch回调
  • then和catch对状态的影响(重点)
    • 无论是then还是catch,只要里面没有报错,返回的就是resolved状态的promise,只要有报错,返回的就是rejected状态的promise
  • then和catch的链式调用(常考)

# 3.3. async/await

# 3.3.1. 基本使用

  • 背景
    • 异步回调有callback hell风险的
    • 为了解决这个问题,可以使用promise then catch链式调用
      • 但这也是基于回调函数
    • async/await是同步语法,彻底消灭回调函数
  • 代码演示
    function loadImg(src) {
      const p = 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
        }
      )
      return p
    }
    
    // 如果不写分号,后面有括号,他会当做一个函数执行,所以这里不写分号,下面匿名函数会报错
    const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
    const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
    
    // 匿名函数
    // 如果前面不加分号,后面要加!(非),表示和前面隔开
    !(async ()=> {
        // 同步写法,实现异步代码
    
        // 执行await函数必须使用async包裹
        const img1 = await loadImg(url1);  // loadImg不是async,是promise对象
        console.log(img1.height, img1.width)
    
        const img2 = await loadImg(url2);
        console.log(img2.height, img2.width)
    })()
    
    const loadImg1 = async ()=> {
        const img1 = await loadImg(url1);
        
        return img1
    }
    
    const loadImg2 = async ()=> {
        const img2 = await loadImg(url2);
        
        return img2
    }
    
    !(async ()=> {
        // 执行await函数必须使用async包裹
        const img1 = await loadImg1();  // loadImg1是async函数,不是promise对象
        console.log(img1.height, img1.width)
    
        const img2 = await loadImg2();
        console.log(img2.height, img2.width)
    })()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54

# 3.3.2. async/await和Promise的关系

  • 回顾
    • async/await是消灭异步回调的终极武器
    • 和Promise并不互斥
    • 两者相辅相成(必须结合起来用)
  • 关系
    • 执行async函数,返回的是Promise对象
    • await相当于promise的then
    • try...catch可捕获异常,代替了promise的catch
  • 代码演示
    • 执行async函数,返回的是Promise对象
      async function fn1() {
        // 如果返回一个值,他会内部封装成一个promise对象
        // 如果返回promise对象,就按原状返回一个promise对象
        // return Promise.resolve(200)
        return 100  // 相当于return Promise.resolve(100)
      }
      const res1 = fn1();  // 执行async函数,返回的是一个promise对象
      console.log("res1", res1);  // 100 resolved状态 promise对象
      // resolved状态的promise对象,可以执行then回调
      res1.then(data=> {
          console.log("data", data)  // 100
      })
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
    • await相当于promise的then
      !(async ()=> {
        const p1 = Promise.resolve(300)
        const data = await p1  // await相当于promise的then
        console.log('data', data);  // 300
      })()
      
      // 如果await下面只是一个值
      !(async ()=> {
        // 如果await后面是promise,他会当做then使用
        // 如果不是promise,他会进行内部封装 => await Promise.resolve(400)
        const data1 = await 400  
        console.log('data1', data1);  // 400
      })()
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
    • 前面两条规则连起来
      async function fn4() {
        // 返回的都是promise对象
        return 200
        // return Promise.resolve(200)
      }
      
      !(async ()=> {
        // await相当于promise的then
        const data2 = await fn4()  
        console.log('data2', data2);  // 200
      })()
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
    • try...catch可捕获异常,代替了promise的catch
      !(async ()=> {
        const p4 = Promise.reject('err')  // reject状态的promise
      
        // promise如果是reject状态,就可以使用try catch捕获
        try {
            const res = await p4
            console.log("p4", res)
        } catch(ex) {
            console.error(ex);  // try...catch相当于promise的catch
        }
      })()
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
    • await、then、try...catch
      • 面试很可能会考
      !(async ()=> {
        const p5 = Promise.reject('err')  // reject状态的promise
        const res = await p5  // 他是reject状态,不会走then,只会走try catch
        // 下面不会执行
        console.log("p5", res)
      })()
      
      1
      2
      3
      4
      5
      6
  • 总结
    • async函数他是封装promise的,返回的也是promise
    • await相当于promise的then,处理promise成功的情况
    • 在async函数中,promise状态为失败的情况,需要用try...catch捕获

# 3.4. 异步的本质

  • 回顾
    • async/await是消灭异步回调的终极武器
    • JS还是单线程,还是得有异步,还得是基于event loop
    • async/await只是一个语法糖,但这颗糖真香
  • 代码演示
    • 示例1
      const async1 = async ()=> {
        console.log("async1 start");  // 立马执行 重要
        // 它返回的实际上是undefined,await执行不执行其实没有意义
        await async2()  // 先执行async2,再执行await这个操作
        // 执行await之后,下面的都是异步
        // await的后面,都可以看作是callback里面的内容,即异步
        // await下面的方法需要放到callback里面去等待执行
        // 类似event loop,setTimeout(cb1)
        // 也可能是Promise.resolve().then(()=> {console.log('async1 end')})
        // 现在可以把上面两个当作是一个东西,涉及到微任务/宏任务之后进行细分
        console.log('async1 end');
      } 
      
      const async2 = async ()=> {
        console.log("async2");  // 立马执行 重要
      }
      
      // script start
      // async1 start
      // async2
      // !!! 因为await下面是异步
      // script end  // 此时同步代码已经执行完,启动事件循环机制,在回调函数的队列里面去找异步的代码去执行
      // async1 end  // 执行异步代码
      console.log('script start');
      async1();  // 执行该方法,里面的代码立马执行(还没到异步)
      console.log('script end');
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
    • 示例2
      const async3 = async ()=> {
        console.log("async3 start");  // 2
        await async4();
      
        // 下面三行都是异步回调,即callback内容
        // 他们被放到异步队列里面
        console.log('async3 end');  // 5
        await async5();
      
        // 下面一行是异步回调的内容
        // 实际上这是层级嵌套的关系
        console.log('async3 end 2');  // 7
      } 
      
      const async4 = async ()=> {
        console.log("async4");  // 3
      }
      
      const async5 = async ()=> {
        console.log("async5");  // 6
      }
      
      // script2 start
      // async3 start
      // async4
      // script2 end
      // async3 end
      // async5
      // async3 end 2
      console.log('script2 start');  // 1
      async3();
      console.log('script2 end');  // 4
      // 此时同步代码执行完,根据事件循环机制,开始执行异步队列中的代码
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
  • 总结
    • async/await是用同步的代码去写异步
    • 但他改变不了异步的本质
    • await后面的东西,都可以看作是callback里面的内容,即await下面的代码都会被放到异步队列里面
    • 它实际执行的方法和callback是一样的,即也和异步是一样的
    • await下面如果还有await,实际是嵌套关系
    • 同步代码执行完之后,根据事件循环机制,再从异步队列里面拿出方法,放入调用栈去执行

# 3.5. for...of

  • 概念
    • for...in(以及forEach、for)是常规的同步遍历
    • for...of常用于异步的遍历
  • 代码演示
    // 模拟异步
    const muti = num=> {
      return new Promise(resolve=> {
        setTimeout(()=> {
            resolve(num * num);
        }, 1000);
      })
    }
    
    const nums = [1, 2, 3]
    
    // 同步循环
    // 因为要用异步语法,所有必须用async函数
    // 同步的代码不会等待,会一遍一遍往下执行
    // 他会一下子把数组遍历完,然后一下子把数组中的值进行计算,最后再一下子打印出结果
    // 一瞬间执行了三遍,所以一秒钟之后同时打印出来
    nums.forEach(async i=> {
      // 因为muti是异步的,这里使用await实现异步
      let res = await muti(i);
      // 1s之后计算的结果同时打印出来
      console.log(res)
    })
    
    // 如何一个一个数字进行打印呢?
    // 异步循环
    !(async ()=> {
      for(let i of nums) {
        // 先执行第一个
        // 第一个有了结果之后执行第二个
        // 以此类推
        let res = await muti(i);
        console.log(res)
      }
    })()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

# 3.6. async/await总结

  • async/await解决了异步回调,是一个很香的语法糖
  • async/await和promise的关系,很重要
    • 执行async函数,返回的是Promise对象
    • await相当于promise的then
    • try...catch可捕获异常,代替了promise的catch
  • for...of的使用
    • 是一种异步循环

# 3.7. 微任务/宏任务

# 3.7.1. 概念

  • 简述
    • 宏任务 => macroTask
    • 微任务 => microTask
  • 知识点
    • 什么是宏任务,什么是微任务
    • event loop和DOM渲染
      • 只有从这里面才能体现出宏任务和微任务的区别
    • 微任务和宏任务的区别
  • 代码演示
    console.log(100);  // 1
    // 宏任务
    setTimeout(()=> {
      console.log(200);  // 3
    })
    /* setTimeout(()=> {
      console.log(201);  // 4  同类型的异步和出场顺序有关
    }) */
    // 微任务
    Promise.resolve().then(()=> {
      console.log(300)  // 3  不同类型的异步和出场顺序无关
    })
    console.log(400)  // 2  同步代码执行完毕
    
    // 100 400 300 200 201
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 宏任务和微任务的区别
    • 宏任务和微任务其实就相当于所有的异步(DOM事件除外)
      • DOM事件也是基于事件循环机制来执行的
    • 宏任务:setTimeout、setInterval、Ajax、DOM事件
    • 微任务:Promise、async/await
    • 微任务执行时机比宏任务要早(先记住)

# 3.7.2. event loop和DOM渲染

  • 问题
    • 为什么微任务执行时机比宏任务早
  • 简述
    • 需要再次回归一遍event loop的过程
    • JS是单线程的,而且和DOM渲染共用一个线程(资源)
    • JS执行的时候,得留一些时机供DOM渲染
      • js一遍一遍执行,浏览器界面不能一直不变
  • 回顾event loop过程(增加DOM渲染时机)
    • 参考代码
      console.log('Hi')
      
      setTimeout(function cb1() {
        console.log('cb1')  // cb即callback
      }, 5000)
      
      console.log('Bye');
      
      1
      2
      3
      4
      5
      6
      7
    • 名称
      • Browser console => 浏览器console环境
      • Call Stack => 调用栈
      • Web APIs => 浏览器定义的api(DOM/BOM)
      • Callback Queue => 回调函数队列
      • Event Loop => 事件循环机制
    • 执行过程
      • 执行第一行代码,把它推到调用栈,打印,移出调用栈
      • 执行到定时器
        • 把setTimeout cb1放到调用栈
        • 把定时器里面的方法放到Web API中
        • 放到Web API中定时器就执行完了
        • 移出调用栈
      • 继续往下执行
        • 同步代码继续推到调用栈,打印,移出调用栈
      • 最后调用栈清空,没有任何同步代码推到调用栈执行的时候
        • 第一次轮询
      • 启动事件循环机制
        • 一遍一遍地遍历回调函数队列
        • 如果异步回调队列有值,就会将它推到调用栈中执行
      • 最后还漏了一个步骤
        • 在调用栈空闲(没有同步代码推入调用栈)
        • 先尝试DOM渲染
        • 再去触发事件循环机制
      • 将定时器放入调用栈,执行完毕之后移出调用栈
        • 此时调用栈空闲
        • 会再次尝试DOM渲染
        • 再去触发事件循环机制
        • 第二次轮询
    • 补充说明
      • 每次调用栈清空(即每次轮询结束),即同步任务执行完
        • 包括异步代码推到调用栈执行结束
      • 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
      • 然后再去触发下一次事件循环
  • 代码示例
    • 体会dom渲染和js执行的关系
    // event loop和DOM渲染
    // 执行完js代码之后,还需要渲染到页面上
    const $p1 = $('<p>一段文字</p>')
    const $p2 = $('<p>一段文字</p>')
    const $p3 = $('<p>一段文字</p>')
    
    $('#container').append($p1)
                   .append($p2)
                   .append($p3)
    
    console.log("length", $('#container').children().length)
    // alert会阻断js执行,也会阻断DOM渲染,便于查看效果
    // alert之前,js已经执行完成了,但DOM还未渲染
    alert('本次调用栈结束,DOM结构已经更新,但尚未触发渲染')
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 微任务和宏任务的区别
    • 宏任务:DOM渲染后触发,如setTimeout
    • 微任务:DOM渲染前触发,如Promise
    • 微任务 => DOM渲染 => 宏任务
  • 为什么微任务和宏任务执行顺序是这样的
    • 代码示例
      // event loop和DOM渲染
      // 执行完js代码之后,还需要渲染到页面上
      const $p1 = $('<p>一段文字</p>')
      const $p2 = $('<p>一段文字</p>')
      const $p3 = $('<p>一段文字</p>')
      
      $('#container').append($p1)
                     .append($p2)
                     .append($p3)
      
      // 微任务:DOM 渲染前触发
      Promise.resolve().then(()=> {
        console.log("length1", $('#container').children().length)
      
        alert('Promise then')  // 此时dom未渲染
      })
      
      // 宏任务:DOM 渲染后触发
      setTimeout(()=> {
        console.log("length2", $('#container').children().length)
      
        alert('Promise setTimeout')  // 此时dom渲染了
      })
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
    • 原理
      • 角度
        • 从事件循环解释,为何微任务执行更早
        • 即为何微任务在dom前触发
      • setTimeout执行机制 => 宏任务
        • setTimeout实际上就是把callback放到定时器中
        • 定时器等待一个时机 => 定时时间结束
        • 放入异步回调队列中
        • 由事件循环机制,将异步队列中的方法放到调用栈中
        • 当调用栈空闲(每次轮询结束)的时候,会尝试DOM渲染
        • 尝试DOM渲染之后继续触发事件循环
      • Promise机制 => 微任务
        • 执行promise的时候,他会放到micro task queue里面等待时机
        • 他不会经过web apis
        • 因为promise是ES规范,不是w3c规范
        • 他和宏任务队列(callback queue)是分开的
        • 为什么
          • 微任务是ES6语法规定的
          • 宏任务是由浏览器规定的
      • 完善事件循环机制
        • 同步代码执行完之后,即调用栈清空
        • 执行当前的微任务(微任务队列)
        • 尝试DOM渲染
        • 触发事件循环机制
        • 最后执行宏任务
      • 图示 event-loop2
    • 总结(面试题)
      • 宏任务有哪些?微任务有哪些?为什么微任务触发时机更早
      • 宏任务、微任务和DOM渲染的关系
      • 微任务、宏任务和DOM渲染,在event loop的过程

# 4. 回顾面试题

  • 回顾event loop的过程
  • 和DOM渲染的关系
    • 微任务和宏任务在event loop过程中的不同处理
  • 什么是宏任务和微任务,两者区别
    • 宏任务:setTimeout、setInterval、Ajax、DOM
    • 微任务:Promise、async/await
    • 微任务执行时机比宏任务要早
    • 微任务在DOM渲染前触发,宏任务在DOM渲染后触发
  • Promise的三种状态
    • pending、resolved、rejected
    • pending -> resolved 或 pending -> rejected
    • 变化不可逆
  • Promise then和catcha的连接
    • then后面是resolved状态
    • resolved状态后面跟then可以打印
    • 抛出异常后面是rejected状态
    • rejected状态后面跟catch可以打印
  • async/await和promise关系
    • async返回的永远是promise
    • await相当于then
    • 代码示例
      // 示例一
      async function fn() {
        return 100
      }
      
      (async function() {
        const a = fn()  // ?
        const b = await fn()  // ?
      
        console.log(a);  // promise
        console.log(b);  // 100
      })()
      
      // 示例二
      (async function() {
        console.log('start');
        const a = await 100  // 直接返回
        console.log('a', a)
        // await相当于then,resolved后面加then可以获取值,即直接得到Promise里面的返回值(200)
        const b = await Promise.resolve(200)  
        console.log('b', b)
        // await相当于then,rejected后面只能用catch
        const c = await Promise.reject(300)
        // 后面的方法属于回调,上面返回的是rejected,下面没有try catch
        console.log('c', c)
        console.log('end')
      })()
      // start 100 200 
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
  • promise和setTimeout的顺序
    • setTimeout属于宏任务,promise属于微任务
    • 微任务先于宏任务执行
  • async/await的顺序问题
    // script start
    // async1 start
    // async2
    // promise1
    // script end
    // async1 end
    // promise2
    // setTimeout
    
    async function async1() {
      console.log('async1 start')  // 2  同步
      await async2()  // 先执行async2,再执行await
    
      // 执行await,await后面的都是回调 - 微任务
      // 同步代码执行过程中,微任务会被放到micro task queue里面等待时机
      console.log('async1 end')  // 6  第一个微任务
    }
    async function async2 () {
      console.log('async2')  // 3  同步
    }
    
    console.log('script start')  // 1  同步
    
    // 宏任务
    // 同步代码执行过程中,宏任务会被放到Callback Queue里面等待时机
    setTimeout(function() {
      console.log('setTimeout');  // 8  最后执行宏任务
    }, 0)
    
    async1()
    
    // 初始化promise时,传入的函数会立刻被执行
    new Promise(function(resolve) {
      console.log('promise1')  // 4  同步
      resolve()  // 变成了一个resolved状态下的promise
      // 即then会被触发
      // 这里的then是异步,是一个微任务
    }).then(function() {
      console.log('promise2')  // 7  第二个微任务
    })
    
    console.log('script end');  // 5  同步  ->  同步代码执行完毕 => 调用者(call stack)被清空
    // 同步代码执行完毕之后 => 微任务队列执行 => 尝试触发渲染DOM => 触发事件循环机制 => 宏任务队列执行
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

# 5. 回顾知识点

  • 简述
    • event loop
    • promise进阶
    • async/await
    • 微任务/宏任务
  • event loop过程1
    • 同步代码,一行一行放在call stack执行
    • 遇到异步,会先'记录'下,等待时机(定时、网络请求等)
    • 时机到了,就移动到callback queue
  • event loop过程2
    • 如果call stack为空(即同步代码执行完),event loop开始工作
    • 轮询查找callback queue,如有则移动到call stack执行
    • 然后继续轮询查找(永动机一样)
  • DOM事件和event loop
    • JS是单线程的
    • 异步(setTimeout、ajax等)使用回调,基于event loop
    • DOM事件也使用回调,基于event loop
    • DOM事件和异步,他们触发的时机不一样,但本质都是事件循环
  • Promise进阶
    • 三种状态,状态的表现和变化
    • then和catch对状态的影响(重要)
    • then和catch的链式调用(常考)
  • async/await
    • async/await解决了异步回调,我们可以使用同步代码来写异步
    • 是一个语法糖,其本质还是promise
    • async/await和Promise的关系,非常重要
      • 执行async函数,返回的是Promise对象
      • await相当于promise的then
      • try...catch可捕获异常,代替了promise的catch
    • for...of的使用
  • 微任务/宏任务
    • 宏任务有哪些?微任务有哪些?为什么微任务触发时机更早
      • 微任务是ES6语法规定的
        • Promise、async/await
      • 宏任务是由浏览器规定的
        • setTimeout、setInterval、Ajax、DOM事件
    • 宏任务、微任务和DOM渲染的关系
      • 微任务 => DOM渲染 => 宏任务
    • 微任务、宏任务和DOM渲染,在event loop的过程
      • 微任务会被放到micro task queue里面等待时机
      • 宏任务会被放到Callback Queue里面等待时机
      • 调用栈被清空之后(同步代码执行完)
      • 先执行微任务(不会走web api,直接执行)
        • 微任务被一个个拖到调用栈
      • 微任务执行完毕之后,尝试DOM渲染
      • 尝试DOM渲染之后,触发event loop(事件轮询机制)(永动机)
        • 执行宏任务
          • 宏任务被一个个拖到调用栈


~End~