JavaScript异步类型

  • 延迟类型:setTimeout、setInterval、setImmediate
  • 监听事件:监听 new Image 加载状态、监听 script 加载状态、监听 iframe 加载状态、Message
  • 带有异步功能类型: Promise、ajax、Worker、async/await

需要说明的是,在 ES6 之前,JavaScript 语言本身没有异步,延迟类型、监听类型的异步都是由宿主提供的,并非语言的核心部分。

JavaScript常用异步编程

Promise

Promise 对象用于表示一个异步操作的最终状态,及结果值。

Promise有几个特点:

  1. 对象的状态不受外界影响,有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。只有异步操作的结果可以决定当前是哪种状态,其他操作无法改变。
  2. 状态一旦改变,就不会再变,任何时候都可以得到这个结果。状态改变只可能是:pending -> fulfilled 或 pending -> rejected
  3. 实例化后,会立即执行一次。所以一般将其用函数包裹起来,使用的时候调用一次。
  4. 如果执行后的回调也要做一些异步操作,可以无限的.then下去,当然要保证有返回值

方法:

  • 对象方法 reject、resolve、all、race、allSettled(ES2020)
  • 原型方法 then、catch、finally(ES9)
function promiseTest(n,msg) {
return new Promise((resolve,reject)=>{
setTimeout(function () {
console.log(`执行第${n}个任务`);
msg.code && resolve(msg.text); // 当认为成功的时候,调用resolve函数
!msg.code && reject(msg.text); // 当认为失败的时候,调用reject函数
},n*500)
});
}
let pro = promiseTest(1,{code:true,text:"返回的数据1"});
/* 没有catch,每个then里两个回调函数,此时第一个为成功的回调,第二个为失败的回调 */
pro.then((data)=>{
console.log(data); // 执行成功结果在这里
// return promiseTest(2,{code:true,text:"返回的数据2"});
return promiseTest(2,{code:false,text:"失败的数据"});
},(err)=>{
console.log(err); // 执行失败的结果在这里
}).then((data)=>{console.log(data)},(err)=>{console.log(err)});

观察 then 和 catch 的用法:

  • 在多次 then 后最后跟一个 catch,可以捕获所有的异常
/* 多个then和一个catch */
pro.then((data)=>{
console.log(data);
return promiseTest(2,{code:false,text:"失败的数据"});
}).then((data)=>{
console.log(data)
}).catch((err,data)=>{
console.log("失败了",err);
});

all、rece 和 allSettled 的用法:(这三个方法都是将若干个 Promise 实例,包装成一个新的 Promise 实例)

  • all 接收一个 promise 对象数组,在所有异步操作执行完且全部成功的时候才执行 then 回调,只要有一个失败,就执行 catch 回调(只对第一个失败的promise 对象执行)。
  • race 也接收一个 promise 对象数组,不同的是,哪个最先执行完,对应的那个对象就执行 then 或 catch 方法( then 或 catch 只执行一次)。
  • allSettled 同样接收一个 promise 对象数组。当所有的 promise 对象都解决时(无论是 resolve 还是 reject ),才执行 then 回调,它带来了“我只要兑现所有承诺,我不在乎结果”。
/* all的用法 */
Promise.all([
promiseTest(1,{code:true,text:"返回的数据1"}),
promiseTest(2,{code:false,text:"返回的数据2"}),
promiseTest(3,{code:false,text:"返回的数据3"})
]).then((res)=>{console.log("全部成功",res)}).catch((err)=>{console.log("失败",err);}); /* race的用法 */
Promise.race([
promiseTest(1,{code:false,text:"返回的数据1"}),
promiseTest(2,{code:false,text:"返回的数据2"}),
promiseTest(3,{code:true,text:"返回的数据3"})
]).then((res)=>{console.log("成功",res)}).catch((err)=>{console.log("失败",err);});

Generator

Generator 叫做生成器,通过 function* 关键字来定义的函数称之为生成器函数(generator function),它总是返回一个 Generator 对象。生成器函数在执行时能暂停,又能从暂停处继续执行。调用一个生成器并不会立马开始执行里面的语句,而是返回这个生成器的 迭代对象( iterator )。

Generator 对象有3个方法,都有一样的返回值 { value, done } 【与 Python 生成器的用法一样】

  • .next(value)   返回一个由yield表达式生成的值。(value 为向生成器传递的值)
  • .return(value) 该方法返回给定的值并结束生成器。(value 为需要返回的值)
  • .throw(exception)  该方法用来向生成器抛出异常,并恢复生成器的执行。(exception 用于抛出的异常)

生成器的作用:

可以和 Promise 组合使用。减少代码量,写起来更方便。在没有 Generator 时,写 Promise 会需要很多的 then,每个 then 内都有不同的处理逻辑。现在,我们将所有的逻辑写进一个生成器函数(或者在生成器函数内用 yield 进行函数调用),Promise 的每个 then 内调用同一个函数即可。

定义生成器:

function add(a,b) {
console.log("+");
return a+b;
}
function cut(a,b) {
console.log("-");
return a-b;
}
function mul(a,b) {
console.log("*");
return a*b;
}
function division(a,b) {
console.log("/");
return a/b;
}
function* compute(a, b) {
yield add(a,b);
yield cut(a,b);
let value = yield mul(a,b);
console.log("value",value); // 第三次调用.next()时无法为value赋值,需要第四次调用才能为其赋值
yield mul(a,b);
yield division(a,b);
}

使用生成器:

// 执行一下这个函数得到 Generator 实例,调用next()方法执行,遇到yield暂停
let generator = compute(4, 2); function promise() {
return new Promise((resolve, reject) => {
let res = generator.next();
if(res.value > 5)
{
resolve("OK");
}else
{
reject("小于5")
}
});
} let proObj = promise();
proObj.then((data)=>{
console.log(data);
let res = generator.next();
console.log("Promise res1",res);
}).then((data)=>{
let res = generator.next();
// let res = generator.return();
console.log("Promise res2",res);
}).then((data)=>{
let res = generator.next("qwe"); // 第四次next()时,向生成器传数据
console.log("Promise res3",res)
}).catch((err)=>{
console.log("出错",err);
});

Generator 函数的特点:

  • 最大特点就是可以交出函数的执行权(暂停执行)。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
  • 可以将 yield 关键字使得生成器函数可以与外接交流:可以将内部的值传到外界,也可以将外接的值传入

yield 和 yield* :

  • 生成器函数在执行过程中,遇到 yield 会暂停执行,并返回一个值
  • yield* 表达式用于委托给另一个 generator 函数(即可以将当前生成器函数的执行权交给另一个生成器函数)或 可迭代对象
function* g1() {
yield 2;
yield 3;
yield 4;
} function* g2() {
yield 1;
yield* g1();
yield 5;
yield* ["a", "b"];
yield* "cd";
} var iterator = g2(); console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: "a", done: false }
console.log(iterator.next()); // { value: "b", done: false }
console.log(iterator.next()); // { value: "c", done: false }
console.log(iterator.next()); // { value: "d", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

async/await

优点:简洁,节约了不少代码

  • async 函数就是 Generator 函数的语法糖。要将 Generator 函数转换成 async 函数,只需将 * 替换成 async ,yield 替换成 await 即可
  • 被 async 修饰的函数,总会返回一个 Promise 对象。如果代码中返回值不是 promise 或者没有返回值,也会被包装成 promise 对象
  • await 只能在 async 函数内使用。它是一个操作符,等待一个函数或表达式。经过该操作符处理后,输出一个值。

如果在异步函数中,每个任务都需要上个任务的返回结果,可以这么做:

function takeLongTime(n) {
return new Promise((resolve,reject) => {
setTimeout(() => {resolve(n + 200)}, n);
});
} function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
} function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
} function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
} async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();

如果这几个任务没有关联,可以这样做:

async function doIt() {  // 函数执行耗时2100ms
console.time("doIt");
await step1(300).catch((err)=>{console.log(err)}); // 异常处理
await step1(800);
await step1(1000);
console.timeEnd("doIt");
}
doIt();

当然,最好这样做:

async function doIt() { // 函数执行耗时1000ms
console.time("doIt");
const time1Pro = step1(300);
const time2Pro = step1(800);
const time3Pro = step1(1000);
await time1Pro;
await time2Pro;
await time3Pro;
console.timeEnd("doIt");
}

async function doIt() { // 函数执行耗时1000ms
console.time("doIt");
const [ time1Pro, time2Pro, time3Pro ] = await Promise.all([step1(300), step1(800), step1(1000)])
console.timeEnd("doIt");
}
doIt();

注意:

  1. async/await 并没有脱离 Promise,它的出现能够更好地协同 Promise 工作。

    • 怎么体现更好地协同?它替代了then catch的写法。使得等待 promise 值的操作更优雅,更容易阅读和书写。
  2. 函数仅仅加上 async 并没有意义,它仍然是同步函数,只有与 await 结合使用,它才会变成异步函数。
    • 这需要精准理解 await。它在等待的时候并没有阻塞程序,此函数也不占用 CPU 资源,使得整个函数做到了异步执行。当 async 函数在执行的时候,第一个 await 之前的代码都是同步执行的。
  3. doIt() 函数内部是串行执行的,但它本身是异步函数。
  4. 在这个异步函数内,可能会做很多操作 ABC,他们有执行的先后顺序。这时你可能会想,A、B、C之间没有关联,他们之间可以是并行执行的,并不需要串行,那怎么办?
    • 【错误想法】这样想没错,但是没必要。因为他们已经存在于异步函数内了,所有的操作已经是异步的。在同样的环境情景下,底层执行的效率是相同的,并不见得因为A和B之间互相异步而提高效率。
    • 【正确想法】这样想是有必要的。参照两个 doIt() ,调用的函数返回 promise 对象,前者是依次生成 promise 对象(依次执行任务),依次等待返回结果。等待总时长取决于所有任务执行时间之和。后者则是同时生成 promise 对象(同时执行任务),依次等待。等待总时长取决于耗时最长的任务。后者的 CPU 运用率更高。
  5. async 函数内任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。为了不中断后面的操作,我们可以将 await 语句放在 try ... catch 结构内,或者在 await 后面的 Promise 对象跟一个 catch 方法。
  6. 错误处理。最标准的方法是使用 try...catch 语句,但是它不仅会捕捉到 promise 的异常,还会将所有出现的异常捕获。因此,可以使用 .catch ,只会捕获 promise 相关的异常。

关于错误处理,可以这样做:

function takeLongTime(n) {
return new Promise((resolve,reject) => {
setTimeout(() => {resolve(n + 200)}, n);
}).then(data=>[data,null]).catch(err=>[null,err]);
} async doIt(){
let [data, err] = await takeLongTime(1000);
console.log(data, err);
}

另外,async函数有多种使用形式:

// 函数声明
async function foo() {} // 函数表达式
const foo = async function () {};
const foo = async () => {}; // 对象的方法
let obj = { async foo() {} };
obj.foo().then(...) // Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
} async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
} const storage = new Storage();
storage.getAvatar('jake').then(…);

异步生成器函数

即异步函数和生成器函数的结合体:async function*() {}。它就是 Generator 和 async-await 的完美结合,支持两者的用法和特性。

以前我以为,async-await 可以完全代替 Generator ,但其实不然,前者的优点在于更优雅地处理异步操作,后者能够支持函数内外进行数据交流。

异步生成器函数会返回一个异步迭代器,这个异步迭代器有两种使用方式:

  1. 通过 for await of 遍历得到值,非常方便
  2. 通过循环 .next() 得到

两种方式又有不同:

  1. 前者不能得到异步生成器内 return 的值,后者可以
  2. 前者不能给 yield 传值,后者可以通过 .next() 方法传值
  3. 除此之外,可以将后者看成前者的手动实现

如何进一步理解异步生成器呢?其实可以看成是为异步函数提供了一种异步返回、多次返回的机制。在非异步生成器函数中,return 只能有一个,且是函数结束的标志。而异步生成器函数就可以做到:间断地返回多个值,不同的返回值之间可以有同步操作也可以有异步操作。这正是集 Generator 和 async-await 的优点于一身,有利于解耦,有利于逻辑的分离。

关于异步迭代器的遍历顺序:完全按照 yield 的顺序来,没有变化。不会因为哪个耗时短而改变顺序。await 也是一样,多个 await 相互之间的顺序是固定的,无法调整,在这里只能串行执行。

关于性能:对于 ES6(+) 本身来说,以上所有的异步方式性能都 OK,但在真实的生产环境中都要由 babel 编译成 ES5 语法,结果会导致代码体积增加,执行过程中会执行另外一段代码,总体性能会低一些。

实验代码:

const asyncFunc1 = () => new Promise((resolve, reject) => {
setTimeout(() => { resolve("async-1") }, 1000);
}); const asyncFunc2 = () => new Promise((resolve, reject) => {
setTimeout(() => { resolve("async-2") }, 1500);
}); const asyncGenerator = async function* () {
const promise1 = asyncFunc1(); // 1000ms
const promise2 = asyncFunc2(); // 1500ms
const res1 = await promise1;
const res2 = await promise2;
yield res1
yield res2; // const a = yield res1;
// const b = yield res2;
return "这是异步生成器返回值";
}; const iter = asyncGenerator();
const array = []; /* 通过 for await of 遍历 */
(async () => {
console.time("记时");
for await (const i of iter) {
array.push(i);
console.timeLog("记时");
console.log("遍历", i);
}
console.timeEnd("记时");
console.log("遍历结果", array);
})() /* 通过循环 .next() 获得 */
// (async() => {
// console.log("手动循环.next()循环")
// while(true) {
// const next = iter.next("next传值");
// console.log("得到next", next);
// const { value, done } = await next;
// console.log(value, done);
// if (done) break;
// }
// })()

JavaScript异步的更多相关文章

  1. JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上

    众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作.这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异 ...

  2. JavaScript异步编程原理

    众所周知,JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着. ...

  3. javascript异步编程的前世今生,从onclick到await/async

    javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...

  4. JavaScript异步编程(2)- 先驱者:jsDeferred

    JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...

  5. 【转】JavaScript 异步进化史

    前言 JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback.对于简单的异步操作 ...

  6. 对Javascript异步执行的理解

    简单的查看了下Javascript异步编程的代码.按照网上的说法,Javascript异步编程的核心就在于setTimeout.这个系统函数让我们将函数的执行放在了一个指定的新“线程”中.于是本来的顺 ...

  7. Promises与Javascript异步编程

    Promises与Javascript异步编程 转载:http://www.zawaliang.com/2013/08/399.html 在如今都追求用户体验的时代,Ajax应用真的是无所不在.加上这 ...

  8. Javascript 异步加载详解

    Javascript 异步加载详解 本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy ...

  9. javascript异步延时载入及推断是否已载入js/css文件

    <html> <head> <script type="text/javascript"> /**======================= ...

  10. Javascript异步请求你能捕获到异常吗?

    Javascript异步请求你能捕获到异常吗? 异常处理是程序发布之前必须要解决的问题,不经过异常处理的应用会让用户对产品失去信心.在异常处理中,我们一贯的做法是按照函数调用的次序,将异常从数据访问层 ...

随机推荐

  1. [自学]数据库ER图基础概念整理(转)

    ER图分为实体.属性.关系三个核心部分.实体是长方形体现,而属性则是椭圆形,关系为菱形. ER图的实体(entity)即数据模型中的数据对象,例如人.学生.音乐都可以作为一个数据对象,用长方体来表示, ...

  2. 【转载:java】详解java中的注解(Annotation)

    目录结构: contents structure [+] 什么是注解 为什么要使用注解 基本语法 4种基本元注解 重复注解 使用注解 运行时处理的注解 编译时处理的注解 1.什么是注解 用一个词就可以 ...

  3. 2019-2-18-VisualStudio-给项目添加特殊的-Nuget-的链接

    title author date CreateTime categories VisualStudio 给项目添加特殊的 Nuget 的链接 lindexi 2019-02-18 15:56:48 ...

  4. NCDC 天气数据的预处理

    "Hadoop: The Definitive Guild" 这本书的例子都是使用NCDC 天气数据的,但由于书的出版和现在已经有一段时间了,NCDC现在提供的原始数据结构已经有了 ...

  5. T2988 删除数字【状压Dp+前缀和优化】

    Online Judge:从Topcoder搬过来,具体哪一题不清楚 Label:状压Dp+前缀和优化 题目描述 给定两个数A和N,形成一个长度为N+1的序列,(A,A+1,A+2,...,A+N-1 ...

  6. Fiilter

    过滤器 过滤请求和响应 作用:        自动登录.        统一编码.        过滤关键字        .... Filter是一个接口 编写filter步骤: 1.编写一个类 a ...

  7. pg_hba.conf配置文件

    实例级别的权限由pg_hba.conf来控制,例如 : # TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix doma ...

  8. TZ_03_mybatis的xml开发

    1.通过Student.xml编写sql来操作数据库 1>insert语句插入后返回主键 加入标签useGeneratedKeys=“true” keyProperty=“oid” 中 keyP ...

  9. psu online course

    https://onlinecourses.science.psu.edu/statprogram/programs Graduate Online Course Overviews Printer- ...

  10. Redis源码解析:28集群(四)手动故障转移、从节点迁移

    一:手动故障转移 Redis集群支持手动故障转移.也就是向从节点发送"CLUSTER  FAILOVER"命令,使其在主节点未下线的情况下,发起故障转移流程,升级为新的主节点,而原 ...