JS异步进阶

JS如何执行

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

event loop(事件循环/事件轮询)图示流程

  • JS是单线程运行的
  • 异步基于回调来实现
  • event loop就是异步回调的实现原理
  • 图示流程
    1
    2
    3
    4
    5
    6
    7
    console.log('Hi')

    setTimeout(function cb1() {
    console.log('cb1') // cb 即 callback
    }, 5000)

    console.log('Bye')
    event loop图示流程1
    event loop图示流程2

DOM事件和event loop的关系

  • 首先要明确DOM事件不是异步!
  • JS单线程,异步(定时器,ajax等)都使用回调,基于 event loop
  • DOM 事件也使用回调,基于 event loop
  • 他们都是到了某一时刻才触发的,定时器是到了时间执行回调,ajax是返回请求时执行回调,DOM事件则是触发事件时执行回调,他们都基于 event loop
  • click是立马就执行的,但是绑定的事件函数要等触发以后才执行,虽然不是异步,但也有一个事件循环的过程,触发后的事件函数才被放入 回调队列 中,等待执行栈中清空后进行事件循环。
1
2
3
4
5
6
7
8
9
10
11
<button id="btn1">提交</button>

<script>
console.log('Hi')

$('#btn1').click(function (e) {
console.log('button clicked')
})

console.log('Bye')
</script>

DOM事件的event loop图示流程


Promise

三种状态

  • pending resolved rejected
  • pending-> resolved 或pending-> rejected
  • 变化不可逆
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
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {

})
console.log('p1',p1)// p1 Promise{<pending>}

// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
// 打印是同步的,此时定时器还没结束,所以状态还是pending
console.log('p2',p2)// p2 Promise{<pending>}
setTimeout(() => console.log('p2_setTimeout',p2))// p2_setTimeout Promise{<fulfilled>: undefined}

// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
// 打印是同步的,此时定时器还没结束,所以状态还是pending
console.log('p3',p3)// p3 Promise{<pending>}
setTimeout(() => console.log('p3_setTimeout',p3))// p3_setTimeout Promise{<rejected>: undefined}
1
2
3
4
5
// 直接返回一个 resolved 状态
const p = Promise.resolve(100)

// 直接返回一个 rejected 状态
Promise.reject('some error')

状态和 then catch

  • pengding状态,不会触发then和catch
  • resolved状态,会触发后续的then回调函数
  • rejected状态,会触发后续的catch回调函数(哪怕中间隔着then,也不执行中间的then,直接执行后面的catch)

then和catch如何影响状态的变化

  • 注意:then和catch返回新的Promise实例,状态默认都为resolved!!如手动在then()/catch()中 返回Promise 或 抛出Error(抛出错误),则状态可变。
    • 注意:throw错误才有用,return无效
  • then正常返回resolved,里面有 抛出错误 则返回rejected
  • catch正常返回resolved,里面有 抛出错误 则返回rejected
  • 注意:rejected状态下的then不被调用,所以也不会得到then返回的新的Promise,而catch就算隔着then也会被调用
    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
    const p = Promise.resolve().then(() => {
    return 100;
    })
    setTimeout(() => console.log('p', p)) // p Promise {<fulfilled>: 100}
    // 所以then()返回的Promise默认是resolved状态的

    const p1 = Promise.resolve().then(() => {
    // 注意:要throw出错误才能改变Promise状态,return没用
    throw new Error('then error')
    // return Promise.reject('some error') 也可改变Promise状态
    })
    setTimeout(() => console.log('p1', p1)) // p1 Promise {<rejected>: Error: then error}

    // p1 Promise <rejected>
    p1.then(() => {
    console.log('rejected状态下的then()不被调用')
    // 虽然then()返回的是resolved状态的Promise,但是此时then()没有被调用
    }).catch(() => {
    console.log('rejected状态下隔着then()的catch()仍然被调用')
    }).then(() => {
    console.log('catch()返回的是resolved状态的Promise,此时的then()会被调用')
    })

    const p2 = Promise.reject('some error')
    setTimeout(() => console.log('p2', p2)) // p2 Promise {<rejected>: "some error"}

    const p3 = p2.catch((err) => {
    console.log('err', err) // err some error
    })
    setTimeout(() => console.log('p3', p3)) // p2 Promise {<fulfilled>: undefined}
    // catch()返回的Promise默认也是resolved状态的!

    p3.then(() => { // p3是rejected的then返回的Promise(resolved状态)
    console.log('resolved状态下then被触发')
    })

    const p4 = p2.catch((err) => {
    throw new Error('catch error')
    })
    setTimeout(() => console.log('p4', p4)) // p4 Promise {<rejected>: Error: catch error}

async/await语法

  • 背景:
    • 异步回调会造成callback hell(回调地狱
    • Promise then catch链式调用可解决回调地狱,但也是基于回调函数
    • async/await是同步语法,彻底消灭回调函数
  • async/await是用同步的方式编写异步
  • 例子如下,Promise的例子可参考“JS异步与单线程、Date、Math”中”手写用Promise加载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
    34
    35
    36
    37
    38
    39
    40
    41
    // 手写用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
    }

    async function loadImg1() {
    const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
    const img1 = await loadImg(src1)
    return img1
    }

    async function loadImg2() {
    const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
    const img2 = await loadImg(src2)
    return img2
    }

    (async function () {
    // 注意:await 必须放在 async 函数中,否则会报错
    try {
    // 加载第一张图片
    const img1 = await loadImg1()
    console.log(img1.width,img1.height) // 252,144
    // 加载第二张图片
    const img2 = await loadImg2()
    console.log(img2)
    // <img src="https://avatars3.githubusercontent.com/u/9583120">
    } catch (ex) {
    console.error(ex)
    }
    })()

async/await和Promise的关系

  • async/await是消灭异步回调的终极武器,但和Promise并不互斥,反而,两者相辅相成
  • 执行async函数返回的是Promise对象(如果函数内没返回 Promise ,则自动封装一下)
  • await相当于Promise的then
    • 注意:对rejected状态的Promise使用await时不会执行,会报错,此时可使用try..catch包裹await来捕获异常。例子
  • try..catch可捕获异常,代替了Promise的catchtry..catch例子
1
2
3
4
5
6
7
8
9
10
// async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
async function fn2() {
return new Promise(() => {}) // Promise pending
}
console.log( fn2() )

async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
  • await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
  • await 后续跟非 Promise 对象:会直接返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
(async function () {
const p1 = new Promise(() => {}) // pending状态
await p1 // await -> then
console.log('p1') // 不会执行
})()

(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()

// await 后续跟非 Promise 对象:会直接返回
(async function () {
const res = await 100
console.log(res) // 100
})()

(async function () {
const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不会执行
})()

async/await与异步的本质,执行顺序

  • 执行顺序:async是同步的,调用就立马执行,await这行代码也同步,但其后的代码会被放入 异步微任务队列 中!
  • 本质:async/await是消灭异步回调的终极武器,实现起来是同步的方式,但本质上还是异步(具体看下方例子,从await后的代码被放在微任务中可看出本质是异步)
  • JS还是单线程,还得是有异步,还得是基于event loop
  • async/await只是一个语法糖,async/await本质还是异步,异步的本质还是回调函数,所以await所在那行代码后的代码都放在异步微队列中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function async1 () { // !! async调用就立马执行
console.log('async1 start') // 2
await async2() // async()是调用就执行,故先直接执行async2()。
// await是同步,await undefined得到undefined
// await后的内容(微任务)都放入callback queque中,等同步执行完才执行
console.log('async1 end') // 关键,它相当于放在 callback queque中,最后执行,5
}

async function async2 () {
console.log('async2') // 3,返回 undefined
}

console.log('script start') // 1
async1()
console.log('script end') // 4

/* script start
async1 start
async2
script end
async1 end */

ES6 for-of的应用场景(与async/await)

  • 可参考ES6扩展 对象扩展MDN for-of
  • for-in以及forEach、for是常规的同步遍历
  • for-of常用于异步遍历,例子如下:
    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
    // 定时算乘法
    function multi(num) {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve(num * num)
    }, 1000)
    })
    }

    /*
    // 使用 forEach ,同步情况下不会等待,会直接循环三次,所以3次的1s重合一起,
    // 出来的效果就是 1s 之后打印出所有结果1 4 9,即 3 个值是同时被计算出来的
    function test1 () {
    const nums = [1, 2, 3];
    nums.forEach(async x => {
    const res = await multi(x);
    console.log(res);
    })
    }
    test1();
    */

    // 使用 for...of ,可以让计算挨个串行执行
    // 出来的效果是1s之后打印1,1s后再打印4,1s后再打印9
    async function test2 () {
    const nums = [1, 2, 3];
    for (let x of nums) {
    // 在 for...of 循环体的内部,遇到 await 会挨个串行计算
    const res = await multi(x)
    console.log(res)
    }
    }
    test2()

ES9 for await of

  • 可参考MDN
  • 与上面的例子区分开
    • 注意:上面的例子循环的是同步可迭代对象nums,只是循环体中用了异步操作multi()
  • 与for-of的区别:
    • 针对同步可迭代对象没有区别:
      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
      // 注意: nums属于同步可迭代对象
      // for-of循环体内部存在异步函数时,此时for-of与forEach处理方式有区别,但与for await of没有区别
      function multi(num) {
      return new Promise((resolve) => {
      setTimeout(() => {
      resolve(num * num)
      }, 1000)
      })
      }
      async function test2 () {
      const nums = [1, 2, 3];
      for (let x of nums) {
      // 在 for...of 循环体的内部,遇到 await 会挨个串行计算
      const res = await multi(x)
      console.log(res)
      }
      }
      test2() // 出来的效果是1s之后打印1,1s后再打印4,1s后再打印9
      async function test3 () {
      const nums = [1, 2, 3];
      for await(let x of nums) {
      // 在 for await...of 循环体的内部,遇到 await 也会挨个串行计算
      const res = await multi(x)
      console.log(res)
      }
      }
      test3() // 出来的效果一样是1s之后打印1,1s后再打印4,1s后再打印9
    • 针对异步可迭代对象区别在于for await of会自动给循环的值执行一个await,而for-of不会,所以虽然都会同步执行,但执行效果可能有差异
      • 就像下面例子test4(),执行的是Promise对象,没有async-await,直接就同步返回了
      • 注意:以下例子在f12中执行时可能存在逐步输出变同时输出的情况,这是因为JS引擎在执行异步代码时会利用事件循环机制,允许同时处理多个任务造成的,实际不是这样
        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
        function multi(num) {
        return new Promise((resolve) => {
        setTimeout(() => {
        resolve(num * num)
        }, 1000)
        })
        }
        async function test4 () {
        // 异步可迭代对象asyncNums
        const asyncNums = [multi(1), multi(2), multi(3)];
        for (let x of nums) {
        console.log(res)
        }
        }
        test4()
        // 并不会像for await of一样自动加上await去执行Promise对象
        // 最终会直接返回3个Promise对象
        /* 相当于:
        async function test4() {
        const p1 = multi(1);
        console.log(p1);
        const p2 = multi(1);
        console.log(p2);
        const p3 = multi(1);
        console.log(p3);
        }
        test4();
        */
        async function test5 () {
        // 异步可迭代对象asyncNums
        const asyncNums = [multi(1), multi(2), multi(3)];
        for await(let x of nums) {
        console.log(res)
        }
        }
        test5()
        // 1秒后打印1,1s后再打印4,1s后再打印9
        /* 相当于:
        async function test5() {
        const p1 = await multi(1);
        console.log(p1);
        const p2 = await multi(1);
        console.log(p2);
        const p3 = await multi(1);
        console.log(p3);
        }
        test5();
        */

宏任务和微任务

  • 宏任务:script setTimeout setInterval ajax DOM 事件
  • 微任务:Promise.then()(对于前端来说)async/await
    • 注意:Promise的函数体是定义时就立即执行的,then()是微任务
  • 宏任务比微任务执行的更早,执行一个宏任务就要清空所有微任务后再执行一个宏任务。注意js主体是一个宏任务,所以js后紧接着的是微任务
1
2
3
4
5
6
7
8
9
console.log(100)
setTimeout(() => { // 宏任务
console.log(200)
})
Promise.resolve().then(() => { // 微任务
console.log(300)
})
console.log(400)
// 100 400 300 200

event-loop和DOM渲染的关系

  • JS是单线程的,且和DOM渲染共用一个线程,所以JS执行时需要留一些时机供DOM渲染
  • 每一次 call stack 结束(即同步任务执行完),都会给一次 DOM 渲染的机会(不一定非得渲染,就是给一次 DOM 渲染的机会!!!),DOM结构如有改变则重新渲染然后再触发下一次 event loop
  • 例子:可以看出js执行和DOM渲染共用一个线程,js执行完才到DOM渲染
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
    // (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
    // 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预

    // 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
    setTimeout(function () {
    alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
    })
    event-loop和DOM渲染的关系例子结果

宏任务和微任务的执行顺序的原因

  • 为什么 异步任务 中微任务比宏任务执行更早
    • 宏任务:DOM 渲染再触发,如script setTimeout setInterval ajax DOM 事件
    • 微任务:DOM 渲染会触发,如Promise.then()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)

// // 微任务:DOM渲染之前执行(DOM 结构已更新,但未渲染)
// Promise.resolve().then(() => {
// const length = $('#container').children().length
// console.log('length1',length) // length1 3
// alert(`Promise then`) // DOM渲染了吗?--没有
// })

// 宏任务:DOM渲染之后执行(DOM 结构已更新)
setTimeout(() => {
const length = $('#container').children().length
console.log('length2',length) // length2 3
alert(`macro task ${length}`) // macro task 3
})

宏任务和微任务的执行顺序

event-loop图解宏任务和微任务与DOM渲染的执行顺序

  • 微任务微任务是ES6语法规定的。 ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何处理,即可一次性处理完,更快更及时。
  • 宏任务宏任务是浏览器规定的。 ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。处理起来就比较慢,所以在DOM渲染后才触发宏任务。

event-loop图解宏任务的执行顺序
event-loop图解微任务的执行顺序


题目

描述event loop机制

  • 第一遍可以只到宏任务微任务
  • 第二遍深入可加上和宏任务微任务与DOM渲染的关系
  • 不要混着讲,怕乱

宏任务与微任务的区别

  • 宏任务:script setTimeout setInterval ajax DOM 事件
    • 除script,其他都是浏览器引擎来执行,因为宏任务都是Web API,不属于ES语法
  • 微任务:Promise.then()(对于前端来说)async/await
    • js引擎来执行
    • 注意:Promise的函数体是定义时就立即执行的,then()是微任务
  • 宏任务比微任务执行的更早,js引擎处理速度比浏览器引擎快,所以 异步任务中微任务执行时机先于宏任务
  • 微任务在DOM渲染触发,宏任务在DOM渲染触发

Promise的三种状态,如何变化

  • pending,resolved,rejected
  • pending–>resolved 或 pending–>rejected
  • 变化不可逆

Promise catch 连接 then

catch执行与否的问题

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
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 3

// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 2 3

// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => { // 注意这里是 catch
console.log(3)
})
// 1 2

async/await 语法问题

1
2
3
4
5
6
7
async function fn() {
return 100
}
(async function () {
const a = fn() // promise 值是100
const b = await fn() // 100
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(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')
})() // 执行完毕,打印出那些内容?

// start
// a 100
// b 200

Promise 和 setTimeout 顺序

  • 同步任务先于异步任务
  • 宏任务(setTimeout)先于微任务(Promise.then)
  • 但是同步任务也属于宏任务,所以 执行宏任务(同步任务)=》清空微任务(Promise.then)=》执行宏任务(setTimeout)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    console.log(100)
    setTimeout(() => {
    console.log(200)
    })
    Promise.resolve().then(() => {
    console.log(300)
    })
    console.log(400)
    // 100 400 300 200

【重点】async/await 执行顺序问题

  • 注意:
    • async()调用时函数体立即执行,await当行代码为同步,await后是微任务。
    • Promise 的函数体(即new Promise(){…})会立刻执行。
    • then()是微任务
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
// 网上很经典的面试题
async function async1 () {
console.log('async1 start') // 2
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行

//上面有 await ,下面就变成了“异步”,类似 callback 的功能(微任务)
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() // async的函数体是立即执行的

new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // 4,Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2') // 7
})

console.log('script end') // 5

宏任务(同步,async)=》清空微任务(await后,then)=》宏任务(setTimeout)

script start
async1 start
async2
promise1
script end
(同步代码执行完毕)
async1 end
promise2
setTimeout