【2023秋第6节课】JS异步编程

196 阅读7分钟

image.png JS异步编程

  1. 为什么需要异步编程
    浏览器执行JS的线程只有一个,如果一个任务十分耗时,一直占用该线程,那么会阻塞后续代码 的执行。比如你JS代码中有个setTimeout等待3s,不可能原地等3s,一定是先把该任务交给浏览器专门的线程执行,执行JS的主线程就跳过setTimeout代码继续往后执行,等setTimeout到时后再交给JS主线程处理

image.png

(浏览器一帧的流程)


  1. 同步

    JavaScript中的同步指的是代码按照顺序逐行执行的方式。在同步代码中,每一行代码的执行都必须等待前一行代码执行完毕才能继续执行下一行代码,这意味着代码的执行是阻塞的,直到当前行的任务完成后才会执行下一行代码。

    在同步代码中,如果遇到一个耗时的操作(如网络请求、文件读取、复杂计算等),代码会一直等待该操作完成后才会继续执行下一行代码。这可能导致页面或应用程序的阻塞,因为在等待耗时操作完成期间,JavaScript执行引擎无法执行其他任务,界面可能会冻结或不响应。

console.log("开始");
function say(){
    console.log("执行任务1");
}
say()
console.log("执行任务2");
console.log("结束");
开始
执行任务1
执行任务2
结束

在执行同步代码时,如果遇到一个耗时的操作,例如一个循环计算耗费较长的时间,那么代码会一直停留在这个循环中,直到循环计算完成才会继续执行后续代码。

  1. 异步

    JavaScript中的异步指的是代码执行的一种非阻塞方式。在异步代码中,代码不会按照顺序逐行执行,而是将耗时的操作委托给其他机制来处理,继续执行后续代码而不等待操作完成。异步代码的特点是可以同时执行多个任务,并在每个任务完成后触发相应的回调函数或处理异步结果。这样可以在等待一个任务的同时继续执行其他任务,提高代码的并发性和响应性。

console.log("开始");

setTimeout(function() {
  console.log("异步操作完成");
}, 2000);

console.log("结束");
开始
结束
异步操作完成

这个呢?

console.log("开始");

setTimeout(function() {
  console.log("异步操作完成");
}, 0);

console.log("结束");
开始
结束
异步操作完成

可以看到输出没变,这就是事件循环的作用,JS主线程会优先执行同步代码,将异步代码放到宏任务和微任务中执行

  1. 事件循环

    JS中异步任务会被放到宏任务或者微任务队列中,浏览器在每帧中,当执行完同步代码后就会去取出完成了的异步任务执行。每一帧的事件循环中,浏览器线程会优先执行微任务队列中的任务,再是宏任务队列中的任务,若有一个任务完成就会被放到执行栈中,在JS主线程空闲的时候就会检查任务栈是否有完成了的异步任务,有的话就取出执行

image.png

(当JS执行到对应的宏任务或者微任务时,这个异步代码就会被放到对应的宏任务队列or微任务队列中)

执行流程GIF:

263f472e6e8fb39cf42349634431056e.gif (783×450)

image.png

  1. 常见的异步事件
  • setTimoue,setInterval定时器函数
  • ajax,fetch请求
fetch("https://api.example.com/data").then(function (response) {
    //response.json()本身也是异步的
    return response.json();
  }).then(function (data) {
    console.log("AJAX回调函数", data);
  });
  • onClick等DOM事件
document.addEventListener("click", function () {
  console.log("点击事件回调函数");
});
  1. 异步任务处理工具Promise

    在 JavaScript 中,Promise 是一种用于处理异步操作的对象。它表示一个异步操作的最终完成或失败,并提供了一种更优雅的方式来处理异步代码。 创建 Promise 对象可以使用 Promise 构造函数,它接受一个执行器函数作为参数。new Promise的操作是同步的!!!传递的函数会立刻执行!!!,函数通常包含异步操作,并决定 Promise 是成功还是失败。执行器函数带有两个参数,它们是 resolve 和 reject 函数,用于将 Promise 标记为成功或失败。

    new Promise同步例子:
console.log('立刻执行1')
// new Promise内的函数同步执行
const p = new Promise((resolve,reject)=>{
            console.log('立刻执行2')
            setTimeout(()=>{
                console.log('异步执行1')  
            },0)
        })
console.log('立刻执行3')

输出结果:

image.png

  • Promise的基本使用

    Promise构造函数内的形参是 resolve 和 reject 函数,用于将 Promise 标记为成功或失败,传递的数据可以作为.then或者.catch捕获的返回值

    当执行到p.then的时候才会将该.then()内的函数推入微任务队列

console.log('立刻执行1')
// 成功resolve的例子
const p = new Promise((resolve,reject)=>{
            console.log('立刻执行2')
            setTimeout(()=>{
                console.log('异步执行1')
                resolve('promise resolve了')
            },0)
        })
p.then(res=>console.log(`Promise resolve后获得的返回值是:${res}`))
console.log('立刻执行3')
console.log('立刻执行1')
// 失败reject的例子
const p = new Promise((resolve,reject)=>{
            console.log('立刻执行2')
            setTimeout(()=>{
                console.log('异步执行1')
                reject('promise reject了')
            },0)
        })
p.then(res=>console.log(`Promise resolve后获得的返回值是:${res}`))
p.catch(err=>console.log(`Promise reject后获得的返回值是:${err}`))
console.log('立刻执行3')

使用场景1(获取网络数据,然后使用)

const url = 'http://ajax-api.itheima.net/api/province'
        // fetch返回的也是Promise
        fetch(url).then((response)=>{
            console.log('收到响应',response)
            // response也是Promise
            response.json().then(body=>{
                console.log('响应返回的body',body)
                // 如果还要利用获取的body数据去再去
                // 请求什么数据,那么会嵌套很多层,
                // 即形成“回调地狱”
            })
        })

回调地狱:

asyncFunc1()
  .then((result1) => {
    console.log("操作 1 成功:", result1);
    return asyncFunc2(result1);
  })
  .then((result2) => {
    console.log("操作 2 成功:", result2);
    return asyncFunc3(result2);
  })
  .then((result3) => {
    console.log("操作 3 成功:", result3);
    // 更多操作...
  })
  .catch((error) => {
    console.log("操作失败:", error);
  });

Promise.all方法,会等待数组中的所有Promise都resolve了才会resolve,如果其中一个Promise返回reject了,那么这个Promise.all这个Promise就返回reject,且err为第一个reject的Promise返回值

使用场景例子:

image.png

语法展示:

const p1 = new Promise((resolve)=>{
            setTimeout(()=>{
                console.log('Promise1 resolve')
                resolve('Promise1 resolve')
            },1000)
        })
        const p2 = new Promise((resolve)=>{
            setTimeout(()=>{
                console.log('Promise2 resolve')
                resolve('Promise2 resolve')
            },2000)
        })
        const p3 = new Promise((resolve,reject)=>{
            setTimeout(()=>{
                // 50%概率成功
                const isOK = Math.random() > 0.5 ?
                 (console.log('Promise3 resolve'),resolve('Promise3 resolve') ): 
                 (console.log('Promise3 reject'),reject('Promise3 reject'))
                
            },3000)
        })
const p = Promise.all([p1,p2,p3])
p.then(resArr=>console.log('Promise.all成功的返回值是所有Promise  reslove的返回值',resArr))
p.catch(err=>console.log('其中一个reject了,它的err为',err))
  1. Generator生成器*

    也可以控制异步任务,可以打断函数的执行

    • 基础使用
      function* gen(params){
             console.log(params)
             console.log('执行gen')
             let one = yield 111
             console.log(one)
             let two = yield 222
             console.log(two)
             let three = yield 333
             console.log(three)
             console.log(yield 444)
         }
         let iterator = gen('AAA')
         console.log(iterator.next('AAA'))
         console.log(iterator.next('BBB'))
         console.log(iterator.next('CCC'))
         console.log(iterator.next('DDD'))
         console.log(iterator.next('EEE'))
         console.log(iterator.next('FFF'))
         console.log(iterator.next('GGG'))
         console.log(iterator.next('HHH'))
      

image.png 例子2:

```js
function* gen(){
        console.log('只走到这里')
        let userData = yield getUserData()
        console.log(userData)
        let orderData = yield getOrderData()
        console.log(orderData)
        let goodsData = yield getGoodsData()
        console.log(goodsData)
    }
    function getUserData(){
        setTimeout(()=>{
            const data = '用户数据'
            iterator.next(data)
        },1000)
    }
    function getOrderData(){
        setTimeout(()=>{
            const data = '订单数据'
            iterator.next(data)
        },1000)
    }
    function getGoodsData(){
        setTimeout(()=>{
            const data = '商品数据'
            iterator.next(data)
        },1000)
    }
    let iterator = gen()
    iterator.next()
```
   

image.png

  1. async和await,异步任务的终极解决方案,异步代码写起来就像同步代码一样

    • async会将函数返回值包装为Promise,await后面的必须是返回Promise对象。可以直接拿到该Promise.resolve返回的数值
    • 例子:
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function asyncFunc() {
  console.log("开始");
  
  await delay(2000);
  console.log("等待 2 秒");
  
  await delay(1000);
  console.log("又等待 1 秒");
  
  console.log("完成");
}

asyncFunc();
  1. 事件循环面试题,看代码说输出

    (前4道题) 【前端进阶】深入浅出浏览器事件循环【内附练习题】-腾讯云开发者社区-腾讯云

  2. 补充

    • 微任务是在DOM渲染之前执行,宏任务是在DOM渲染之后执行,也是因此react的useEffect是在DOM更新后执行,useLayoutEffect是在DOM渲染前执行
    • 可以用Promise.resolve()生成一个可以反复使用的Promise,我们用它来创建微任务,通过微任务和宏任务来控制异步任务的优先级,控制执行顺序。比如vue3的组建更新的调度器

image.png