关于Js的promise、generator、async、await
第一章 前言
大家都知道Javascript是单线程的,而且他的耗时操作是异步的,比如网络请求以及IO操作。在一般来说,我们比较喜欢他的异步,因为异步效率比较高,资源得到了合理的利用,但是有的时候我们为了控制流程,而且流程里面存在一些耗时操作,如果还是使其异步的话就会使得我们的流程非常难控制,所以这个时候我们就要同步执行我们的耗时操作。
第二章 关于单线程
有的时候我会想,javascript既然是单线程的,那为什么他又可以异步的呢?(因为作为小白的我的认知来说,异步就是开个新的线程去执行这个耗时任务 o(╥﹏╥)o)
这里就有一个主线程的概念了,所谓单线程就是Javascript 引擎在解释、处理javascript代码的线程只有一个,这个线程就是主线程。实际上浏览器还存在其他线程,比如处理网络请求、处理DOM等的线程,这些线程称为工作线程,这里说的单线程的意思是javascript无论什么时候都只有一个线程在运行javascript程序
这样的好处就是javascript的单线程简化了处理事件的机制,不必理会资源竞争和线程同步这些复杂的问题。
第三章 关于同步、异步、阻塞、非阻塞
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
-
同步
同步就是程序一行一行的执行代码,只有等上一行的代码执行完了,才会继续执行下一行代码
function sync{ console.log("1") console.log("2") console.log("3") console.log("4") console.log("5") } sync() // 输出 1,2,3,4,5 -
阻塞
阻塞调用是指在返回结果之前,程序会等待这个调用直到这个调用返回结果,才会继续往下执行
function block(){ console.log(1); console.log(2); // 这里假设这个文件比较大,需要花费1分钟才能打开它 let res = fs.readFileSync("xxx.json") console.log(3); console.log(4); } block() // 输出 1,2,(这里过了1分钟之后) 继续输出 3,4 -
异步
异步操作在js中的原理是当遇到异步操作时(比如网络请求、IO耗时操作等),这个异步任务会挂起,放到
任务队列,任务队列的任务会等到任务队列之外的所有代码执行完毕之后在执行,因此程序的执行顺序可能和代码中的顺序不一致。function async() { console.log("开始准备请求数据"); console.log("马上要开始请求了..."); $.ajax('http://xxxx.com', function(resp) { console.log('请求完成~'); }); console.log("请求发出完成"); } async() // 开始准备请求数据 // 马上要开始请求了... // 请求发出完成 // 请求完成~ -
非阻塞
非阻塞调用是指程序执行到非阻塞调用时,会将该任务放置
任务队列,然后程序继续往下执行,等任务队列以外的代码都执行完成之后,才开始执行任务队列中的方法function nonBlocking(){ console.log("开始读文件了"); fs.readFile("./package.json",(err,data)=>{ console.log("文件读取完成..."); }) console.log("发起文件读取完毕"); } nonBlocking() // 开始读文件了 // 发起文件读取完毕 // 文件读取完成...
第四章 异步编程的四种方式
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。 ------摘自 阮一峰
-
回调函数
/** * 吃饭 */ function eat(){ console.log("开始吃饭"); } /** * 洗手 * @param {function} afterTask */ function wishHands(afterTask) { /** * 洗手要洗一分钟 */ console.log("开始洗手..."); setTimeout(_=>{ console.log("手洗干净了..."); afterTask() },1000*60) } /** * 吃饭前需要洗手 */ wishHands(eat) // 开始洗手... // 手洗干净了... // 开始吃饭
-
事件监听
function task(){ console.log("task start"); setTimeout(_=>{ task.trigger('finish') // 触发finish事件 },1000*3) } function afterTask(){ console.log("task finish"); } // 监听finish事件 task.on('finish',afterTask) task()这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
-
发布/订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
function task(){ console.log("task start"); setTimeout(_=>{ jQuery.publish("finish") },1000*3) } function afterTask(){ console.log("task finish"); } jQuery.subscribe('finish',afterTask) task()
-
Promise 对象
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
function task(){ console.log("开始执行任务"); return new Promise((resolve,reject)=>{ setTimeout(_=>{ resolve("我完成啦") }) }) } function afterTask(){ console.log("afterTask 开始"); } task().then(res=>{ console.log(res); afterTask() })
第五章 关于Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
-
基本用法
Promise对象是一个构造函数,接受一个包含resolve回调和reject回调参数的函数为参数,执行结果符合预期可以调用resolve,不符合预期可以执行reject抛出异常。let promise = new Promise((resolve,reject)=>{ let res = task() if (res is expect) { resolve("good") } reject("res is not expect") })Promise有三种状态:
执行中、已成功、已失败resolve调用之后,promise实例的状态就从执行中->已成功reject调用之后,promise实例的状态就从执行中->已失败要对Promise的执行结果做处理可以执行它的
then方法,then方法包含两个参数,第一个是成功的回调,第二个是失败可选回调:promise.then(res=>{ console.log("exec success:"+res); // 这里res就是resolve的参数 },err=>{ console.log("exec fail:"+err); // 这里 err就是reject的参数 }) -
Promise 实例的属性
promise.then(res=>{}) // 可以理解成结果的回调 promise.catch(reason=>{}) // 执行失败或发生异常的回调 promise.finally(_=>{}) // 执行结束的回调,不管成功与否都会回调 // then()方法和catch()返回的结果仍然是promise,所以可以使用链式写法 //写法一 promise.then(res=>{ },err=>{ }).finally(_=>{ }) // 写法二 promise.then(res=>{ }).catch(err=>{ }).finally(_=>{ }) // 写法一和写法二效果是一样的 -
Promise静态方法
-
Promise.all(...promise[])将多个 Promise 实例,包装成一个新的 Promise 实例
Promise.all(p1,p2,p3)上面代码中,
Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。p的状态由p1、p2、p3决定,分成两种情况。(1)只有
p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。(2)只要
p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。-
Promise.race(...promise[])方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
Promise.race(p1,p2,p3)上面代码中,只要
p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。- Promise.resolve(task) 将一个方法包装成promise,比如他ajax请求
- Promise.reject(task) 同上
-
更多详情请参考阮一峰
promise
第六章 关于Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
-
示例
function* generatorTest(){ console.log("generator test start "); yield console.log("step 1"); yield console.log("step 2"); yield console.log("step 3"); console.log("generator test finish"); } let test = generatorTest() // 什么都没有 test.next() // generator test start // step 1 test.next() // step 2 test.next() // step 3 test.next() // generator test finish test.next() // 什么都没有 从上面实例中我们可以看出来,generator方法的声明需要加上
*号,里面还有关键字yield调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。然后需要执行next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行 更多详情请参考阮一峰
generator
第七章 async和await
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
-
使用async与generator的对比
const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; // 使用generator const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; // 使用async const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; // 一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。 -
基本用法
async声明的方法返回的是一个promise对象,可以使用then方法添加回调函数,当程序执行的过程中,一旦遇到await声明的语句,程序将会等待这个语句返回结果之后才会执行后面的方法。- 实例一:
// 这里假设getStudentIdByNumber和getStudentScoreById都是耗时的网络请求,所以是异步的 async function getStudentScoreByStudentNumber(studentNumber) { // 根据学号拿到id let studentId = await getStudentIdByNumber(studentNumber) // 通过id获取分数 let score = await getStudentScoreById(studentId) // 获取分数后返回 return score } // 使用then去获取执行结果 getStudentScoreByStudentNumber("662297").then(score=>{ console.log(score); })- 实例二:
function request(){ return new Promise((resolve,reject)=>{ setTimeout(resolve,1000*5) }) } async function networkTask(){ console.log("request start"); await request() console.log("request finish"); } networkTask() // 输出 request start // 等待5秒 // 输出 request finish
1、正常情况下,
await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。2、根据语法规则,
await命令只能出现在async函数内部,否则都会报错。3、
await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
第八章 注意
如果多个异步请求之间存在前后关系,可以像上一章一样使用
await来改造成同步-
如果多个
await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。let foo = await getFoo(); let bar = await getBar(); // 可写成 // 写法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 写法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise; await命令只能用在async函数之中,如果用在普通函数,就会报错。-
如果将
forEach方法中使用async函数会有问题// 错误一 async function dbFuc(db) { let docs = [{}, {}, {}]; // 报错 docs.forEach(function (doc) { await db.post(doc); }); } // 错误二 function dbFuc(db) { //这里不需要 async let docs = [{}, {}, {}]; // 可能得到错误结果 docs.forEach(async function (doc) { await db.post(doc); }); } // 改成for of 准确 async function dbFuc(db) { let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); } } // 改成reduce async function dbFuc(db) { let docs = [{}, {}, {}]; await docs.reduce(async (_, doc) => { await _; await db.post(doc); }, undefined); }