Skip to content

异步

同步程序指的是浏览器是一行一行的读取我们的程序,在异步中,只有达到一定条件或操作完成才可以继续。

Promise

Promise是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。

在基于 Promise 的 API 中,异步函数会启动操作并返回 Promise 对象。然后,你可以将处理函数附加到 Promise 对象上,当操作完成时(成功或失败),这些处理函数将被执行。

Promise的三种状态:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch() 返回 Promise 时的状态,此时请求还在进行中。
  • 已兑现(fulfilled):意味着操作成功完成。当 Promise 完成时,它的 then() 处理函数被调用。
  • 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的 catch() 处理函数被调用。

Promise.all()

使用 Promise.all() 允许有效并行执行多个异步操作,并在它们全部可用后处理合并的结果。

Promise.all()返回的 Promise:

  • 当数组中所有的 Promise 都被兑现时,才会通知 then() 处理函数,并提供一个包含所有响应的数组,数组中响应的顺序与被传入 all() 的 Promise 的顺序相同。
  • 被拒绝——如果数组中有任何一个 Promise 被拒绝。此时,catch() 处理函数被调用,并携带第一个拒绝的 Promise 的原因。这种行为有时被称为“快速失败”。如果需要处理全部Promise,且被拒绝后,仍然继续执行,可以使用Promise.allSettled()

示例:

js
const fetchPromise1 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
);
const fetchPromise2 = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found"
);
const fetchPromise3 = fetch(
  "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json"
);
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}:${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`获取失败:${error}`);
  });

注意

fetch 函数即使在HTTP请求错误的情况下(例如404或500错误),也不会返回一个被拒绝的promise。fetch 只有在网络错误发生时,无法完成请求时,才会返回一个被拒绝的promise(例如网络断开)。

实现一个简单的promise.all(),代码如下:

js
const promiseAll = (functions) => {
  let len = functions.length;
  let arr = new Array(len);
  return new Promise((resolve, reject) => {
    let count = 0;
    functions.forEach((fn, index) => {
      fn()
        .then((val) => {
          arr[index] = val;
          count++;
          if (count === len) {
            resolve(arr);
          }
        })
        .catch((error) => reject(error));
    });
  });
};

// 传入promise数组,返回其输出值,若有错误,则直接抛出第一个错误的原因
let arr = [
  () => new Promise((resolve) => setTimeout(() => resolve(5), 100)),
  () => new Promise((resolve) => setTimeout(() => resolve(1), 50)),
  () => new Promise((resolve) => setTimeout(() => resolve(10), 200)),
  /*
  () =>
    new Promise((resolve, reject) =>
      setTimeout(() => reject(new Error("Error1")), 50)
    ),
  */
];

const result = promiseAll(arr);
result.then((val) => console.log(val)); // [ 5, 1, 10 ]
js
// 用同步代码的形式编写异步代码,可读性更高
const promiseAll = (functions) => {
  let len = functions.length;
  let arr = new Array(len);
  return new Promise((resolve, reject) => {
    let count = 0;
    functions.forEach(async (fn, index) => {
      try {
        const val = await fn();
        arr[index] = val;
        count++;
        if (count === len) {
          resolve(arr);
        }
      } catch (err) {
        reject(err);
      }
    });
  });
};

async和await

使用asyncawait可以像编写同步代码那样编写异步代码,避免了创建.then的显式Promise链。

  • async:用于定义异步函数,确保函数始终返回一个Promise。当在函数声明或函数表达式前使用async关键字时,它变成一个异步函数。请注意,从异步函数返回的非Promise对象会自动包装成Promise对象。
  • await:用于暂停异步函数的执行,直到Promise解析。它只能在异步函数内部使用。当在Promise之前使用await时,它等待Promise解析或拒绝。如果已解析,则继续执行;如果被拒绝,则抛出异常。

事件循环

js使用调用堆栈来管理函数的执行。当调用函数时,它会被添加到堆栈中。当函数完成时,它会从堆栈中移除。由于js是单线程的,一次只能执行一个函数。

然而,如果一个函数需要较长时间才能执行(例如网络请求),这可能会有问题。这就是事件循环的用武之地。

事件循环是一个持续的循环,检查调用堆栈是否为空。如果为空,它会从任务队列(也称为事件队列或回调队列)中获取第一个任务并将其推送到调用堆栈中,立即执行它。

异步调用

当运行到一个setTimeout时,就会开始执行定时器,在计时的同时,不会阻塞其他函数的执行,这是js的非阻塞特性。

当计时时间到,就会将setTimeout中的回调函数添加到任务队列中,事件循环检查调用堆栈是否为空和此回调函数是否位于任务队列第一位,是则放入调用堆栈进行执行。

异步任务介绍

1.宏任务

也称为任务队列,宏任务现在分为用户交互任务队列,延时任务队列,网络请求任务队列等,不仅仅局限于一个队列中;script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax,I/0,UI交互,postMessage,setImmediate(Node.js 环境)。

2.微任务

也称为Job队列,Promise队列;Promise.then catch finally、queueMicrotask、MutaionObserver、Object.observe、Promise.resolve、Promise.reject、process.nextTick(Node.js 环境)。

提示

同步任务的主线程队列会比异步任务的宏任务和微任务先运行。

异步任务的执行顺序为:

分析以下代码,判断其执行顺序:

html
<!-- 点击事件宏任务 -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>点击</button>

</body>
<script>
// 宏任务 - 用户交互事件
document.querySelector('button').addEventListener('click', function () {
    console.log('宏任务 - 用户交互事件');
});

// 宏任务 - setTimeout
setTimeout(()=>{
    console.log('宏任务-setTimeout')
},2000)


new Promise(function (resolve, reject) {
    console.log('同步任务1');
    resolve();
    console.log('同步任务2')
}).then(function () {
  // 微任务
  console.log('微任务 - Promise通过then执行 ');
});

console.log('同步任务3');
</script>
</html>
txt
同步任务1
同步任务2
同步任务3
微任务 - Promise通过then执行
宏任务-setTimeout
宏任务 - 用户交互事件  (点击触发)
js
const first = () => (new Promise((resolve, reject) => {
  console.log(3);
  let p = new Promise((resolve, reject) => {
    console.log(7);
    setTimeout(() => {
      console.log(5);
      resolve(6);
      console.log(p);
    }, 0);
    resolve(1);
  });
  resolve(2);
  p.then(
    (arg) => {
      console.log(arg);
    }
  );
}));
first().then((arg) => {
  console.log(arg);
});
console.log(4);
js
const first = () => (new Promise((resolve, reject) => {
  console.log(3);
  let p = new Promise((resolve, reject) => {
    console.log(7);
    setTimeout(() => {
      console.log(5);
      //  一个promise中只能有一个resolve,所以以下这个不会使用到
      resolve(6);
      // 到这里p的状态已经是fulfilled,所以会执行then方法,并返回外层那个resolve的值
      console.log(p); // Promise {<fulfilled>: 1}
    }, 0);
    resolve(1);
  });
  // 这里给first一个返回值
  resolve(2);
  // first箭头函数里边的promise会通过这个then来执行,并返回值
  p.then(
    (arg) => {
      console.log(arg);
    }
  );
}));
// first箭头函数里边的promise会通过这个then来执行,并返回值
first().then((arg) => {
  console.log(arg);
});
console.log(4);
// 3 7 4 1 2 5 Promise {<fulfilled>: 1}

/*
同步任务:
3 7
console.log(4)

微任务:
p里边的promise
1=> {
  console.log(1)
}

first里边的promise
2=> {
  console.log(2)
}

宏任务:
setTimeout(() => {
  console.log(5);
  resolve(6);
  console.log(p);
}, 0);
*/
js
const first = async () => {
  console.log(3);
  let p = new Promise((resolve, reject) => {
    console.log(7);
    setTimeout(() => {
      console.log(5);
      resolve(6);
      console.log(p);
    }, 0);
    resolve(1);
  });
  let result = 2;
  let pResult = await p;
  console.log(pResult);
  return result;
};
const run = async () => {
  let firstResult = await first();
  console.log(firstResult);
}

run();
console.log(4);
https://cdn.jsdelivr.net/gh/hr1201/img@main/imgs/202407272201959.png

vue3 nextTick 示例代码

js
import {nextTick} from 'vue'
setTimeout(() => { 
    console.log(2)
    new Promise(() => {
        console.log(9)
    })
    setTimeout(() => {
        console.log(3)
    })
})
// 
nextTick(() => { 
    console.log(4)
    setTimeout(() => {
        console.log(5)
    })
})

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(6)
    })
    resolve()
    console.log(7)
}).then(() => { 
    console.log(8)
})

console.log(1)
js
import {nextTick} from 'vue'
// 宏任务
setTimeout(() => { 
    // 5. 宏任务1
    console.log(2)
    new Promise(() => {
        // 6. 宏任务1-同步任务
        console.log(9)
    })
    // 9. 宏任务1-宏任务
    setTimeout(() => {
        console.log(3)
    })
})

// nextTick实际上也是执行一个微任务
nextTick(() => { 
    // 3. 微任务1
    console.log(4)
    // 8. 微任务1-宏任务
    setTimeout(() => {
        console.log(5)
    })
})

// 特殊说明: new Promise()属于同步任务,第一个放入任务队列,在主线程直接执行
let promise = new Promise((resolve, reject) => {
    // 7. 微任务2-宏任务
    setTimeout(() => {
        console.log(6)
    })
    // 2. 调用resolve(),将.then()回调添加至微任务队列,会在当前执行栈的最后才执行
    resolve()
    // 1. 同步任务1
    console.log(7)
}).then(() => { 
    // 4. 微任务2
    console.log(8)
})

// 同步任务2
console.log(1)
// 7 1 4 8 2 9 6 5 3

解释:同步任务会在主线程中执行,所以可算作任务队列第一位,之后会将微任务添加进任务队列,最后在宏任务,例如setTimeout时间到达之后,也将其放入任务队列。当调用堆栈为空时,就会将任务队列按 先进先出 放入调用堆栈中进行运行。如果有嵌套,也会按照以上顺序进行。

顺序:同步任务 --> 微任务 --> 宏任务

实战(调用接口)

  1. 地狱回调方式:

逐层嵌套,代码可读性差,难以维护。

js
function getData(callback){
  http.$axios.get('https://api.github.com/id')
    .then(res => {
      http.$axios.get(`https://api.github.com/users`)
        .then(res => {
          callback(res.data)
        })
    })
}
  1. promise方式:

代码可读性好,但是需要不断的链式调用。

js
function getData(){
  return new Promise((resolve, reject) => {
    resolve('data').then(res => {
      return http.$axios.get(`https://api.github.com/id`)
    }).then(res => {
      return http.$axios.get(`https://api.github.com/users`)
    }).then(res => {
      return res.data
    }).catch(err => {
      console.log(err)
    })
  })
}
  1. generator方式:

代码可读性好,但是需要不断的调用next()

js
function* getData(){
  yield http.$axios.get('https://api.github.com/id')
  yield http.$axios.get(`https://api.github.com/users`)
}

let g = getData()

// 获取到id的数据
g.next().value.then(res => {
  console.log(res.data.id)
})

// 获取到users的数据
g.next().value.then(res => {
  console.log(res.data.user)
})
  1. async/await方式:

代码可读性好,且不需要不断的链式调用。

js
async function getData(){
  try {
    let id = await http.$axios.get('https://api.github.com/id')
    let users = await http.$axios.get('https://api.github.com/users')
    return id + users.data
  } catch (err) {
    console.log(err)
  }
}

Released under the MIT License.