JavaScript异步
JavaScript异步类型
- 延迟类型:setTimeout、setInterval、setImmediate
- 监听事件:监听 new Image 加载状态、监听 script 加载状态、监听 iframe 加载状态、Message
- 带有异步功能类型: Promise、ajax、Worker、async/await
需要说明的是,在 ES6 之前,JavaScript 语言本身没有异步,延迟类型、监听类型的异步都是由宿主提供的,并非语言的核心部分。
JavaScript常用异步编程
Promise
Promise 对象用于表示一个异步操作的最终状态,及结果值。
Promise有几个特点:
- 对象的状态不受外界影响,有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。只有异步操作的结果可以决定当前是哪种状态,其他操作无法改变。
- 状态一旦改变,就不会再变,任何时候都可以得到这个结果。状态改变只可能是:pending -> fulfilled 或 pending -> rejected
- 实例化后,会立即执行一次。所以一般将其用函数包裹起来,使用的时候调用一次。
- 如果执行后的回调也要做一些异步操作,可以无限的.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();
注意:
- async/await 并没有脱离 Promise,它的出现能够更好地协同 Promise 工作。
- 怎么体现更好地协同?它替代了then catch的写法。使得等待 promise 值的操作更优雅,更容易阅读和书写。
- 函数仅仅加上 async 并没有意义,它仍然是同步函数,只有与 await 结合使用,它才会变成异步函数。
- 这需要精准理解 await。它在等待的时候并没有阻塞程序,此函数也不占用 CPU 资源,使得整个函数做到了异步执行。当 async 函数在执行的时候,第一个 await 之前的代码都是同步执行的。
- doIt() 函数内部是串行执行的,但它本身是异步函数。
- 在这个异步函数内,可能会做很多操作 ABC,他们有执行的先后顺序。这时你可能会想,A、B、C之间没有关联,他们之间可以是并行执行的,并不需要串行,那怎么办?
- 【错误想法】这样想没错,但是没必要。因为他们已经存在于异步函数内了,所有的操作已经是异步的。在同样的环境情景下,底层执行的效率是相同的,并不见得因为A和B之间互相异步而提高效率。
- 【正确想法】这样想是有必要的。参照两个 doIt() ,调用的函数返回 promise 对象,前者是依次生成 promise 对象(依次执行任务),依次等待返回结果。等待总时长取决于所有任务执行时间之和。后者则是同时生成 promise 对象(同时执行任务),依次等待。等待总时长取决于耗时最长的任务。后者的 CPU 运用率更高。
- async 函数内任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。为了不中断后面的操作,我们可以将 await 语句放在 try ... catch 结构内,或者在 await 后面的 Promise 对象跟一个 catch 方法。
- 错误处理。最标准的方法是使用 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 ,但其实不然,前者的优点在于更优雅地处理异步操作,后者能够支持函数内外进行数据交流。
异步生成器函数会返回一个异步迭代器,这个异步迭代器有两种使用方式:
- 通过 for await of 遍历得到值,非常方便
- 通过循环 .next() 得到
两种方式又有不同:
- 前者不能得到异步生成器内 return 的值,后者可以
- 前者不能给 yield 传值,后者可以通过 .next() 方法传值
- 除此之外,可以将后者看成前者的手动实现
如何进一步理解异步生成器呢?其实可以看成是为异步函数提供了一种异步返回、多次返回的机制。在非异步生成器函数中,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异步的更多相关文章
- JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上
众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作.这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异 ...
- JavaScript异步编程原理
众所周知,JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着. ...
- javascript异步编程的前世今生,从onclick到await/async
javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...
- JavaScript异步编程(2)- 先驱者:jsDeferred
JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...
- 【转】JavaScript 异步进化史
前言 JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback.对于简单的异步操作 ...
- 对Javascript异步执行的理解
简单的查看了下Javascript异步编程的代码.按照网上的说法,Javascript异步编程的核心就在于setTimeout.这个系统函数让我们将函数的执行放在了一个指定的新“线程”中.于是本来的顺 ...
- Promises与Javascript异步编程
Promises与Javascript异步编程 转载:http://www.zawaliang.com/2013/08/399.html 在如今都追求用户体验的时代,Ajax应用真的是无所不在.加上这 ...
- Javascript 异步加载详解
Javascript 异步加载详解 本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy ...
- javascript异步延时载入及推断是否已载入js/css文件
<html> <head> <script type="text/javascript"> /**======================= ...
- Javascript异步请求你能捕获到异常吗?
Javascript异步请求你能捕获到异常吗? 异常处理是程序发布之前必须要解决的问题,不经过异常处理的应用会让用户对产品失去信心.在异常处理中,我们一贯的做法是按照函数调用的次序,将异常从数据访问层 ...
随机推荐
- [自学]数据库ER图基础概念整理(转)
ER图分为实体.属性.关系三个核心部分.实体是长方形体现,而属性则是椭圆形,关系为菱形. ER图的实体(entity)即数据模型中的数据对象,例如人.学生.音乐都可以作为一个数据对象,用长方体来表示, ...
- 【转载:java】详解java中的注解(Annotation)
目录结构: contents structure [+] 什么是注解 为什么要使用注解 基本语法 4种基本元注解 重复注解 使用注解 运行时处理的注解 编译时处理的注解 1.什么是注解 用一个词就可以 ...
- 2019-2-18-VisualStudio-给项目添加特殊的-Nuget-的链接
title author date CreateTime categories VisualStudio 给项目添加特殊的 Nuget 的链接 lindexi 2019-02-18 15:56:48 ...
- NCDC 天气数据的预处理
"Hadoop: The Definitive Guild" 这本书的例子都是使用NCDC 天气数据的,但由于书的出版和现在已经有一段时间了,NCDC现在提供的原始数据结构已经有了 ...
- T2988 删除数字【状压Dp+前缀和优化】
Online Judge:从Topcoder搬过来,具体哪一题不清楚 Label:状压Dp+前缀和优化 题目描述 给定两个数A和N,形成一个长度为N+1的序列,(A,A+1,A+2,...,A+N-1 ...
- Fiilter
过滤器 过滤请求和响应 作用: 自动登录. 统一编码. 过滤关键字 .... Filter是一个接口 编写filter步骤: 1.编写一个类 a ...
- pg_hba.conf配置文件
实例级别的权限由pg_hba.conf来控制,例如 : # TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix doma ...
- TZ_03_mybatis的xml开发
1.通过Student.xml编写sql来操作数据库 1>insert语句插入后返回主键 加入标签useGeneratedKeys=“true” keyProperty=“oid” 中 keyP ...
- psu online course
https://onlinecourses.science.psu.edu/statprogram/programs Graduate Online Course Overviews Printer- ...
- Redis源码解析:28集群(四)手动故障转移、从节点迁移
一:手动故障转移 Redis集群支持手动故障转移.也就是向从节点发送"CLUSTER FAILOVER"命令,使其在主节点未下线的情况下,发起故障转移流程,升级为新的主节点,而原 ...