前言
JavaScript 的执行环境是 "单线程"(single thread),所谓"单线程",单线程就意味着一次只能完成一件任务,若有多个任务就必须排队,前面一个任务完成再执行后面一个任务,以此类推
注意:所谓的单线程是指执行代码的那个线程是单线程,但浏览器不是单线程的,JS 调用的某些内部的 API 不是单线程的,如 setTimeout 等是有专门的内部线程去负责处理,时间到了将回调函数放到消息队列等待执行
采用单线程模式工作的原因与它最早的设计初衷有关
- 最早 JS 就是运行在浏览器端的脚本语言,目的是为了实现页面上的动态交互,而实现交互的核心就是 DOM 操作,这就决定了必须使用单线程模式,否则容易出现复杂的线程同步问题
- 假定在 JS 中有多个线程同时工作,其中一个线程修改某个 DOM 元素,而另个线程同时删除该元素,此时浏览器无法明确以哪个线程为准
- 所以干脆设计成一个单线程,哪怕 HTML5 出现的 web worker 也是不允许操作 DOM 结构,可以完成一些分布式的计算
单线程模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JS 代码长时间运行(如死循环)导致整个页面卡在这个地方,其他任务无法执行
为了解决这个问题,JS 语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)
同步:指代码中的任务依次执行,执行顺序与代码的编写顺序一致。在单线程情况下,大多数任务是以同步模式去执行(同步不是指同时执行,而是指排队依次执行),缺点就是当某个任务或代码执行时间过长时就会导致后面的任务延迟,导致阻塞(如界面卡顿等)(同步是阻塞的)异步:不会等待当前任务结束才会开始下一个任务。对于耗时操作,都是开启过后立即往后执行下个任务,耗时操作的后续逻辑一般会通过回调函数的方式定义,当耗时任务完成后就会自动执行传入的回调函数,所以程序的执行顺序与任务的排列顺序是不一致的、异步的(异步是非阻塞的),若无异步模式,单线程的 JS 就无法同时处理大量的耗时任务
同步或异步不是指写代码的方式,而是指运行环境提供的API是以同步或异步模式的方式工作
同步模式的API的特点是这个任务执行完代码才会继续往下走,如 console.log()异步模式的API是下达这个任务开启的指令后就会继续往下执行,不会在这行等待任务的结束
异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是 Ajax 操作。在服务器端异步模式甚至是唯一的模式,因为执行环境是单线程的,若允许同步执行所有 http 请求,服务器性能会急剧下降,很快就会失去响应
本文总结了异步模式编程的几种方法,理解它们可以让我们写出结构更合理、性能更出色、维护更方便的 JS 程序
回调函数(Callback)
这是异步编程最基本的方法,所有异步编程方案的根基都是回调函数
简单的说回调函数就是当某些操作完成后触发处理某些功能的逻辑函数
ajax(url, () => { // 处理逻辑 })
采用这种方式可以把同步操作变成了异步操作,处理逻辑不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行
-
回调函数常用应用场合-
资源加载:动态加载 js 文件后执行回调、加载iframe后执行回调、ajax操作回调、图片加载完成执行回调等 -
DOM事件及NodeJs事件基于回调机制(NodeJs回调可能会出现多层回调嵌套的问题) -
链式调用:链式调用时在赋值器(setter)方法中(或本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为需要取值器返回需要的数据而不是this指针,若要实现链式方法,可以用回调函数来实现 -
setTimeout、setInterval函数调用得到其返回值。由于两个函数都是异步的,即:它们的调用时序和程序的主流程是相对独立的,所以没有办法在主体里等待它们的返回值,它们被打开时程序也不会停下来等待,否则也就失去了setTimeout及setInterval的意义,所以用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给相应逻辑函数进行及时处理
-
-
回调函数优缺点回调函数有个致命弱点就是容易写出
回调地狱(Callback hell),假设多个请求存在依赖性,可能就会写出如下代码ajax(url, () => { // 处理逻辑 ajax(url1, () => { // 处理逻辑 ajax(url2, () => { // 处理逻辑 }) }) })回调地狱的根本问题是:
- 嵌套函数存在耦合性,一旦有所改动就会牵一发而动全身
- 嵌套函数一多,就很难处理错误
- 回调嵌套很多时,代码就会非常繁琐且不利于维护和阅读,会给编程带来很多的麻烦
回调函数的
优点:简单、容易理解和实现缺点:不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),且每个任务只能指定一个回调函数。此外它不能使用try catch捕获错误和不能直接return
事件监听
采用事件驱动模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生
下面 fn1 和 fn2,要求 fn2 必须等到 fn1 执行完成才能执行。首先,为 fn1 绑定一个事件(这里采用的 jQuery 的写法)
fn1.on('done', fn2);
上面这行代码的意思是当 fn1 发生 done 事件,就执行 fn2,然后对 fn1 进行改写
function fn1(){
setTimeout(function() {
// fn1 的任务代码
fn1.trigger('done');
}, 1000);
}
fn1.trigger('done') 表示执行完成后立即触发 done 事件,从而开始执行 fn2
这种方法的优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数且可以"去耦合"(Decoupling),有利于实现模块化
缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰
发布/订阅
假定存在一个"信号中心",某个任务执行完成就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行,即发布/订阅模式(publish-subscribe pattern),又称观察者模式(observer pattern)
首先,fn2 向信号中心 jQuery 订阅 done 信号
jQuery.subscribe("done", fn2);
然后,fn1 进行如下改写
function f1() {
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
jQuery.publish("done") 的意思是 fn1 执行完成后向"信号中心" jQuery 发布 done 信号,从而引发 fn2 的执行
此外,fn2 完成执行后也可以取消订阅(unsubscribe)
jQuery.unsubscribe("done", f2);
这种方法的性质与事件监听类似,但是明显优于后者。因为可以通过查看"消息中心" 了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行
Promise
-
什么是 promise?Promise是异步编程的一种解决方案,比传统的回调函数和事件解决方案更合理和更强大,它由CommonJS社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象所谓
Promise其实就是一个对象,用来表示一个异步任务最终结束过后究竟是成功还是失败,就是内部对外部的一个“承诺”,承诺过一段时间会给出一个结果。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理Promise对象有以下两个特点:-
对象的状态不受外界影响
Promise对象代表一个异步操作,有三种状态:pending(等待)、fulfilled(成功)和rejected(失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态 -
状态一旦改变,就不会再变,任何时候都是得到这个结果
Promise对象的状态改变,只有两种可能:从pending->fulfilled和从pending->rejected。只要这两种情况发生,状态就不会再变了,会一直保持这个结果
只要状态改变已经发生,再对
Promise对象添加回调函数也会立即得到这个结果(这与事件(Event)完全不同,事件的特点是:若错过了再去监听是得不到结果的)。当成功或者失败确定后会有对应的任务自动执行,分别对应:onFulfilled、onRejectedlet p = new Promise((resolve, reject) => { reject('reject'); resolve('success'); }); p.then(value => { console.log(value); }, reason => { console.log(reason); }) // reject1、当构造
Promise时构造函数内部的代码是立即执行的
2、平时用的很多库或插件都运用了Promise,如axios、fetch等有了
Promise对象就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外Promise对象提供统一的接口,使得控制异步操作更加容易Promise也有一些缺点:- 无法取消
Promise,一旦新建它就会立即执行,无法中途取消 - 若不设置
回调函数,Promise内部会抛出的错误,不会反应到外部 - 当处于
pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
-
-
Promise 解决什么?- 回调地狱,代码难以维护,常常第一个函数的输出是第二个函数的输入等现象
Promise可以支持多个并发的请求,获取并发请求中的数据Promise可以解决异步的问题,本身不能说Promise是异步的- 回调调用次数过多/少、早/晚、不调等问题
-
Promise 的用法ES6规定Promise对象是一个构造函数,用来生成Promise实例。Promise身上有all、reject、resolve等方法,原型上有then、catch等方法Promise的构造函数接收一个函数作为参数且这个函数需要传入两个参数(它们是两个函数,由 JS 引擎提供,不用自己部署)resolve:将Promise对象的状态从pending变为fulfilled,在异步操作成功时调用并将异步操作的结果作为参数传递出去reject:将Promise对象的状态从pending变为rejected,在异步操作失败时调用并将异步操作报出的错误作为参数传递出去
基本用法
const promise = new Promise(function(resolve, reject) { // resolve(100); reject(new Error('promise rejected')); }); promise.then(function(value) { console.log('resolved', value); }, function(error) { console.log('rejected', error); })
从表面上看
Promise只是能够简化层层回调的写法,而实质上Promise的精髓是状态,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多
-
then 方法Promise实例生成以后可以用then方法分别指定fulfilled状态和rejected状态的回调函数,then方法是定义在原型对象Promise.prototype上的-
then方法里有两个参数:onFulfilled、onRejected(成功有成功的值,失败有失败的原因)(它们都是可选的)。若它们是函数则必须分别在当状态state变为fulfilled、状态state为rejected后被调用,value或reason依次作为它们的第一个参数 -
then方法返回的是一个新的 Promise实例(不是原来那个Promise实例),因此可以采用链式写法,即then方法后面再调用另一个then方法(相比传统回调函数的方式,Promise最大的优势就是可以链式调用,这样可以最大程度的避免回调函数嵌套) -
前面
then方法中回调函数的返回值会作为后面then方法回调的参数,若then回调函数中返回一个新Promise对象,后个then就是为这个返回的新Promise对象去添加状态明确后的回调,该回调会等待返回的新Promise对象状态变化确定后才会调用(若变为fulfilled就调用第一个回调函数,若状态变为rejected,就调用第二个回调函数 -
若返回一个普通的值,该值就作为
then方法返回的Promise的值,下个then方法接收的回调函数参数就是该值;若没有返回任何值,就默认返回一个undefined -
在
then中若使用了return,则return的值会被Promise.resolve()包装 -
then中可以不传递参数,若不传递会透到下一个then中Promise.resolve(1).then(res => { console.log(res); // 1 return 2; }).catch(err => 3).then(res => { console.log(res); // 2 }) Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error('my error'); }) .catch(() => 1).then(x => x + 1) .then(x => console.log(x);) // 2 .catch(console.error)
一般来说,调用
resolve或reject后Promise的使命就完成了,后继操作应该放到then等方法里,而不应该直接写在resolve或reject的后面。所以最好在它们前面加上return语句,这样就不会有意外new Promise((resolve, reject) => { return resolve(1); // 后面的语句不会执行 console.log(2); }) -
-
异常处理Promise.prototype.catch()方法是then(null, rejection)或then(undefined, rejection)的别名,用于指定发生错误时的回调函数。catch方法注册失败回调在链式调用中更常见getJSON('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error); });上面代码中
getJSON()方法返回一个Promise对象,若该对象状态变为fulfilled,则会调用then()方法指定的回调函数;若异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误注意:
then()方法指定的回调函数,若运行中抛出错误也会被catch()方法捕获Promise抛出一个错误,就被catch()方法指定的回调函数捕获const promise = new Promise(function(resolve, reject) { throw new Error('test'); }); promise.catch(function(error) { console.log(error); }); // Error: test // 等价于下面两种写法 // 写法一 const promise = new Promise(function(resolve, reject) { try { throw new Error('test'); } catch(e) { reject(e); } }); promise.catch(function(error) { console.log(error); }); // 写法二 const promise = new Promise(function(resolve, reject) { reject(new Error('test')); }); promise.catch(function(error) { console.log(error); });若
Promise状态已经变成resolved,再抛出错误是无效的const promise = new Promise(function(resolve, reject) { resolve('ok'); throw new Error('test'); }); promise .then(function(value) { console.log(value) }) .catch(function(error) { console.log(error) }); // okPromise对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。即错误总是会被下一个catch语句捕获// 一共有三个 Promise 对象:一个由 getJSON() 产生,两个由 then() 产生 // 它们之中任何一个抛出的错误都会被最后一个 catch() 捕获 getJSON('/post/1.json').then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { ... }).catch(function(error) { // 处理前面三个 Promise 产生的错误 });一般来说,不要在
then()方法里面定义reject状态的回调函数(即then的第二个参数),建议使用catch方法下面第二种写法要好于第一种写法,理由是第二种写法还可以捕获前面
then方法执行中的错误,也更接近同步的写法(try/catch)。因此建议总是使用catch()方法,而不使用then()方法的第二个参数,而且若then中返回一个Promise且该Promise中又存在异常,该then方法第二个参数的失败回调是捕获不到该异常的// bad promise.then(function(data) { // success }, function(err) { // error }); // good promise .then(function(data) { //cb // success }) .catch(function(err) { // error });跟传统的
try/catch代码块不同的是,若没有使用catch()方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应const asyncExample = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为 x 没有声明 resolve(x + 2); }); }; asyncExample().then(function() { console.log('!!!'); }); setTimeout(() => { console.log(123); }, 2000); // Uncaught (in promise) ReferenceError: x is not defined // 123上面代码中,
asyncExample()函数产生的Promise对象,内部有语法错误。浏览器运行到这一行时会打印出错误提示,但是不会退出进程、终止脚本执行,2 秒后还是会输出123,即Promise内部的错误不会影响到Promise外部的代码再看下面的例子
const promise = new Promise(function (resolve, reject) { resolve('ok'); setTimeout(function () { throw new Error('test') }, 0) }); promise.then(function (value) { console.log(value) }); // ok // Uncaught Error: test上面代码中,
Promise指定在下一轮事件循环再抛出错误,到了那时Promise的运行已经结束了,所以这个错误是在Promise函数体外抛出的,会冒泡到最外层,成了未捕获的错误一般总是建议,
Promise对象后面要跟catch()方法,这样可以处理Promise内部发生的错误。catch()方法返回的还是一个Promise对象,因此后面还可以接着调用then()方法const asyncExample = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为 x 没有声明 resolve(x + 2); }); }; asyncExample() .catch(function(error) { console.log('oh no', error); }) .then(function() { console.log('carry on'); }); // oh no ReferenceError: x is not defined // carry on若没有报错则会跳过
catch()方法Promise.resolve() .catch(function(error) { console.log('oh no', error); }) .then(function() { console.log('carry on'); }); // carry on上面的代码因为没有报错,跳过了
catch()方法,直接执行后面的then()方法。此时要是then()方法里面报错,就与前面的catch()无关了catch()方法之中,还能再抛出错误在执行
resolve的回调(即then中的第一个参数)时,若抛出异常(代码出错等)则并不会报错卡死 JS,而是会进到这个catch方法中,即进到catch方法里去且把错误原因传到了reason参数中,即便是有错误的代码也不会报错了,这与try/catch语句有相同的功能// 在resolve的回调中,console.log(somedata) 时 somedata 这个变量是没有被定义的 // 如果不用 Promise,代码运行到这里就直接在控制台报错了,不往下运行了 // 但是在这里,会得到下面的结果 p.then((data) => { console.log('resolved', data); console.log(somedata); //此处的 somedata 未定义 }).catch((err) => { console.log('rejected', err); }); // resolved 4 // rejected ReferenceError: somedata is not defined -
finally 方法finally是原型上的方法Promise.prototype.finally()finally方法用于指定不管Promise对象最后状态如何,都会执行的操作,该方法是ES2018引入标准的promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});上面代码中,不管
Promise最后的状态,执行完then或catch指定的回调函数后都会执行finally方法指定的回调函数finally方法的回调函数不接受任何参数,这意味着没有办法知道Promise状态到底是fulfilled还是rejected,即finally方法里的操作应该是与状态无关的,不依赖于Promise的执行结果finally本质上是then方法的特例promise.finally(() => {...}); // 等同于 promise.then(result => { ... return result; }, error => { ... throw error; });上面代码中,若不使用
finally方法,同样的语句需要为成功和失败两种情况各写一次,有了finally方法,则只需要写一次finally方法总是会返回原来的值// Promise {<fulfilled>: undefined} Promise.resolve(2).then(() => {}, () => {}); // Promise {<fulfilled>: 2} Promise.resolve(2).finally(() => {}) // Promise {<fulfilled>: undefined} Promise.reject(3).then(() => {}, () => {}) // Promise {<rejected>: 3} Promise.reject(3).finally(() => {}) -
静态方法-
Promise.resolve():快速把一个值转换成Promise对象-
若接收的是另一个
Promise对象,则该对象会原封不动的返回 -
若传入的是一个有跟
Promise一样的then方法(在该方法中可以接收onFulfilled和onRejected的两个回调)的对象,当调用onFulfilled传入值,则这样的对象也可以作为Promise对象被执行,在后面的then方法也可以拿到对应传入的值。带有then方法的对象称为实现了thenable的接口(可以被then的对象)(thenable对象指的是具有then方法的对象)
Promise.resolve('foo').then(function(value) { console.log(value); }) // 上面的与下同等 new Promise(function(resolve, reject) { resolve('foo'); }) const promise1 = ajax('xxx'); const promise2 = Promise.resolve(promise1); console.log(promise1 === promise2); // true // Promise.resolve() 方法会将 thenable 对象转为 Promise 对象 let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function (value) { console.log(value); // 42 });- 参数不是具有
then()方法的对象或根本就不是对象:若参数是一个原始值或是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的Promise对象,状态为resolved
const a = Promise.resolve('hhh'); a.then(function (value) { console.log(value); // hhh });上面代码生成一个新的
Promise对象的实例a。由于字符串hhh不属于异步操作(判断方法是字符串对象不具有then方法),返回Promise实例的状态从一生成就是resolved,所以回调函数会立即执行Promise.resolve()方法的参数,会同时传给回调函数Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的Promise对象,所以若希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve()方法const a = Promise.resolve(); a.then(function () {...});需要注意的是,立即
resolve()的Promise对象,是在本轮事件循环(event loop)的结束时执行,而不是在下一轮事件循环的开始时setTimeout(function () { console.log('three'); }, 0); Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one // two // three上面代码中,
setTimeout(fn, 0)在下一轮事件循环开始时执行,Promise.resolve()在本轮事件循环结束时执行,console.log('one')则是立即执行,因此最先输出 -
-
Promise.reject():快速创建一个表示失败的Promise对象,该实例的状态为rejectedconst a = Promise.reject('wrong'); // 等同于 const a = new Promise((resolve, reject) => reject('wrong')) a.then(null, function (v) { console.log(v); }); // wrongPromise.reject()方法的参数会原封不动地作为reject的reason,变成后续方法的参数Promise.reject('wrong') .catch(e => { console.log(e); }) // wrong
-
-
并行执行-
Promise.all():接受一个数组作为参数(参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例),数组每个元素都是一个Promise对象(若不是则会先调用Promise.resolve方法,将参数转为Promise实例后再进一步处理),返回一个全新的Promise对象(这是一个很好的同步执行多个异步任务的方式,它们完成顺序不重要,但必须都要完成)-
当参数内部所有的
Promise都完成后,返回的全新的Promise才会完成 -
内部所有的
Promise状态都变成fulfilled,返回的新的Promise才会变成fulfilled,此时返回值组成一个数组,数组包含每个异步任务执行的结果 -
若其中有某个任务失败,则返回的新的
Promise的状态为rejected,此时返回值是第一个被reject的实例 -
就本质而言列表中的每个值都会通过
Promise.resolve过滤,以确保要等待的是一个真正的Promise,若数组为空则主Promise会立即执行 -
有了
all就可以并行执行多个异步操作且在一个回调中处理所有的返回数据。有一个场景很适合用:一些游戏类的素材比较多的应用,打开网页时预先加载需要用到的各种资源如图片、flash 及各种静态文件,所有的都加载完后再进行页面的初始化
// 例 1 let Promise1 = new Promise(function(resolve, reject) {resolve('111')}); let Promise2 = new Promise(function(resolve, reject){resolve('222')}); let Promise3 = new Promise(function(resolve, reject){resolve('333')}); let p = Promise.all([Promise1, Promise2, Promise3]); p.then(value => { console.log('success:', value); }, err => { console.log('error:', err); }) // success: ['111', '222', '333'] // 例 2 let Promise1 = new Promise(function(resolve, reject){resolve('111')}); let Promise2 = new Promise(function(resolve, reject){resolve('222')}); let Promise3 = new Promise(function(resolve, reject){reject('333')}); let p = Promise.all([Promise1, Promise2, Promise3]); p.then(value => { console.log('success:', value) }, err => { console.log('error:', err) }) // error: 333注意,若作为参数的
Promise实例自己定义了catch方法,则它一旦被rejected,并不会触发Promise.all()的catch方法var test1 = new Promise((resolve, reject) => { resolve('hello'); }).then(result => result).catch(e => e); var test2 = new Promise((resolve, reject) => { throw new Error('报错了'); }).then(result => result).catch(e => e); Promise.all([test1, test2]) .then(result => console.log(result)) .catch(e => console.log(e)); // ['hello', Error: 报错了] -
上面代码中,
test1会resolved,test2首先会rejected,但是test2有自己的catch方法,该方法返回的是一个新的Promise实例,test2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数若
test2没有自己的catch方法,就会调用Promise.all()的catch方法var test1 = new Promise((resolve, reject) => { resolve('hello'); }).then(result => result).catch(e => e); var test22 = new Promise((resolve, reject) => { throw new Error('报错了'); }).then(result => result); Promise.all([test1, test22]) .then(result => console.log(result)) .catch(e => console.log(e)); // Error: 报错了 -
-
Promise.race():Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例。只要其中某个任务完成了(状态改变),所返回的Promise对象就完成结束了(谁跑的快,以谁为准执行回调)-
Promise.race()方法的参数与Promise.all()方法一样,若不是Promise实例则会先调用Promise.resolve()方法,将参数转为Promise实例,再进一步处理 -
一旦有任何一个
Promise决议完成,Promise.race([...])就会完成;一旦有任何一个Promise决议结果是rejected,则结果的状态就是rejected -
传入一个空数组则
Promise.race([...])永远不会决议,而不是立即决议(注意:不要传空数组) -
因为只有一个
Promise能取胜,所以完成值是单个消息,而不是像Promise.all([...])那样可能是个数组 -
Promise.race()的使用场景:如可以用Promise.race()给某个异步请求设置超时时间且在超时后执行相应的操作
// requestImg 函数会异步请求一张图片,把地址写为"图片的路径",所以肯定是无法成功请求到的 // timeout 函数是一个延时 5 秒的异步操作 // 把这两个返回 Promise 对象的函数放进 race,于是他俩就会赛跑,若 5 秒之内图片请求成功了则会进入 then 方法,执行正常的流程 // 若 5 秒钟图片还未成功返回则 timeout 就跑赢了,则进入 catch,报出“图片请求超时”的信息 // 请求某个图片资源 function requestImg(){ var p = new Promise((resolve, reject) => { var img = new Image(); img.onload = function(){ resolve(img); }; img.src = '图片的路径'; }); return p; } // 延时函数,用于给请求计时 function timeout(){ var p = new Promise((resolve, reject) => { setTimeout(() => { reject('图片请求超时'); }, 5000); }); return p; } Promise.race([requestImg(), timeout()]).then((data) => { console.log(data); }).catch((err) => { console.log(err); }); -
-
Promise.allSettled()ES2020引入了Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败),包含了fulfilled和rejected两种情况Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个Promise对象并返回一个新的Promise对象。只有等到参数数组的所有Promise对象都发生状态变更(不管是fulfilled还是rejected),返回的Promise对象才会发生状态变更该方法返回的新的
Promise实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个Promise对象var resolved = Promise.resolve(1); var rejected = Promise.reject(2); var allSettledPromise = Promise.allSettled([resolved, rejected]); allSettledPromise.then(function (results) { console.log(results); }); // [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 2}]上面代码中,
Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的回调函数接收到的参数是数组results,该数组的每个成员都是一个对象,对应传入Promise.allSettled()的数组里面的两个Promise对象的异步操作的结果成员对象的
status属性的值只可能是字符串fulfilled或字符串rejected,用来区分异步操作是成功还是失败。若是fulfilled,对象会有value属性,若是rejected,会有reason属性,对应两种状态时前面异步操作的返回值 -
Promise.any()ES2021引入了Promise.any()方法,该方法接受一组Promise实例作为参数,包装成一个新的Promise实例返回Promise.any([ fetch('https://v8.dev/').then(() => 'home'), fetch('https://v8.dev/blog').then(() => 'blog'), fetch('https://v8.dev/docs').then(() => 'docs') ]).then((first) => { // 只要有一个 fetch() 请求成功 console.log(first); }).catch((error) => { // 所有三个 fetch() 全部请求失败 console.log(error); });只要参数实例有一个变成
fulfilled状态,包装实例就会变成fulfilled状态;若所有参数实例都变成rejected状态,包装实例就会变成rejected状态Promise.any()跟Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个Promise变成rejected状态而结束,必须等到所有参数Promise变成rejected状态才会结束Promise.any()抛出的错误,不是一个一般的Error错误对象,而是一个AggregateError实例,它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误,下面是AggregateError的实现示例// new AggregateError() extends Array const err = new AggregateError(); err.push(new Error("first error")); err.push(new Error("second error")); // ... throw err;var resolved = Promise.resolve(1); var rejected = Promise.reject(2); var alsoRejected = Promise.reject(Infinity); Promise.any([resolved, rejected, alsoRejected]).then(function (result) { console.log(result); // 1 }); Promise.any([rejected, alsoRejected]).catch(function (results) { console.log(results); // AggregateError: All promises were rejected }); -
执行时序Promise的回调会作为微任务执行,会在本轮任务结束末尾执行宏任务在执行过程中可以临时加上一些额外需求,可以选择作为一个新的宏任务进到队列中排队绝大多数异步调用 API 都可作为
宏任务进入到回调队列,Promise、MutationObserver、process.nextTick(node)等会作为微任务 -
Promise 相关实现-
Promise 的声明:
Promise是一个类,这里用class来声明由于
new Promise((resolve, reject)=>{}),在执行这个类时需要传递一个执行器(回调函数),执行器立即执行,秘籍里称为executor,executor是立即执行executor里有两个参数,分别是resolve(成功)和reject(失败)由于
resolve和reject可执行,所以都是函数,这里用let声明class MyPromise { // 构造器 constructor(executor) { // 立即执行 executor(this.resolve, this.reject); } // 成功 resolve = () => {}; // 失败 reject = () => {}; }Promise中有三种状态:- 等待(pending)
- 成功(fulfilled)
- 失败(rejected)
一旦状态确定就不可更改(等待 -> 成功、等待 -> 失败)
resolve和reject函数是用来更改状态的- resolve -> fulfilled
- reject -> rejected
// 创建三个常量用于表示状态,对于经常使用的一些值都应该通过常量来管理,便于开发及后期维护 const PENDING = 'pending'; // 等待 const FULFILLED = 'fulfilled'; // 成功 const REJECTED = 'rejected'; // 失败 class MyPromise { constructor(executor) { // 捕获执行器错误 // 若 executor 执行报错,则直接执行reject try { executor(this.resolve, this.reject); } catch (e) { this.reject(e); } } // 初始化 state 为等待状态 state = PENDING; // 成功的值 value = undefined; // 失败的原因 reason = undefined; resolve = value => { // 若状态不是等待,则阻止程序向下执行 if (this.status !== PENDING) return; // 将状态改为成功 this.status = FULFILLED; // 保存成功后的值 this.value = value; }; reject = reason => { // 若状态不是等待,则阻止程序向下执行 if (this.state === PENDING) return; // 将状态改为成功 this.status = REJECTED; // 保存成功后的值 this.reason = reason; }; } -
then 方法
then方法内部做的事情就是判断状态:若成功(state 为 fulfilled)则调用成功的回调函数,传入对应的 value 参数;相反则是调用失败(state 为 rejected)的回调函数,传入对应的 reason 参数then方法是被定义在原型对象中的class MyPromise { constructor(executor) { ... } // ... then(onFulfilled, onRejected) { // 判断状态 if (this.status === FULFILLED) { onFulfilled(this.value); } else if (this.status === REJECTED) { onRejected(this.reason); } } }加入异步逻辑
-
当在
执行器中加入异步代码时,由于异步代码没有立即执行,且异步代码中调用resolve或reject时,而then是立即执行的,此时的状态是等待状态,若是等待状态此时并不知道是成功还是失败,因此此处得增加判断以及相应处理 -
在
then调用时将成功和失败存到各自的数组,当异步代码执行完成调用resolve或reject时,此时才去执行成功或失败对应的回调函数 -
此处增加实现
then方法多次调用添加多个处理函数
const PENDING = 'pending'; // 等待 const FULFILLED = 'fulfilled'; // 成功 const REJECTED = 'rejected'; // 失败 class MyPromise { constructor(executor) { // 捕获执行器错误 // 若 executor 执行报错,则直接执行reject try { executor(this.resolve, this.reject); } catch (e) { this.reject(e); } } // 初始化 state 为等待状态 state = PENDING; // 成功的值 value = undefined; // 失败的原因 reason = undefined; // 成功回调存放 successCallback = []; // 失败回调存放 failCallback = []; resolve = value => { // 若状态不是等待,则阻止程序向下执行 if (this.status !== PENDING) return; // 将状态改为成功 this.status = FULFILLED; // 保存成功后的值 this.value = value; // 成功回调是否存在,若存在则调用 // this.successCallback && this.successCallback(this.value); // 从前到后执行回调 // Array.shift() 取出数组第一个元素,然后()调用,shift 操作后,数组将删除该元素,直到数组为空 while (this.successCallback.length) this.successCallback.shift()(this.value); }; reject = reason => { // 若状态不是等待,则阻止程序向下执行 if (this.state === PENDING) return; // 将状态改为成功 this.status = REJECTED; // 保存成功后的值 this.reason = reason; // 成功回调是否存在,若存在则调用 // this.failCallback && this.failCallback(this.value) // 从前到后执行回调 // Array.shift() 取出数组第一个元素,然后()调用,shift 操作后,数组将删除该元素,直到数组为空 while (this.failCallback.length) this.failCallback.shift()(this.reason); }; then(onFulfilled, onRejected) { if (this.status === FULFILLED) { onFulfilled(this.value); } else if (this.status === REJECTED) { onRejected(this.reason); } else { // 等待状态 // 将成功和失败回调先存储起来 this.successCallback.push(onFulfilled); this.failCallback.push(onRejected); } } }解决链式调用
-
后面的
then的参数值取决于前面的then的回调的返回值 -
then要实现链式调用的前提是每次then方法都返回一个Promise对象 -
秘籍 规定
onFulfilled()或onRejected()的值,即第一个then返回的值,叫做x,判断x的函数叫做resolvePromise(1)首先,要看
x是不是Promise,若是promise则取它的结果,作为新的promise2成功的结果;若是普通值,则直接作为promise2成功的结果(4)所以要比较
x和promise2(5)
resolvePromise的参数有promise2(默认返回的Promise)、x(return的对象)、resolve、reject(6)
resolve和reject是promise2的
class MyPromise { constructor(executor) {...} // ... then(onFulfilled, onRejected) { let promise2 = new MyPromise((resolve, reject) => { if (this.status === FULFILLED) { // 获取前个 then 的回调函数的返回值,传递给下个 then // 若是返回一个普通值,直接调用 resolve // 若返回的是一个 promise 对象,查看 promise 对象返回的结果,根据返回结果决定用 resolve 或 reject let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } else if (this.status === REJECTED) { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } else { // 等待状态 // 将成功和失败回调先存储起来 this.successCallback.push(onFulfilled); this.failCallback.push(onRejected); } }); } }完成
resolvePromise函数function resolvePromise(promise2, x, resolve, reject) { // 判断 x 是不是 promise2 // 循环引用报错 if(x === promise2) { // reject报错 return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); } // 如果是 Promise 实例直接调用 then 方法,免去了后面判断 thenable、取 then 方法的操作 if (x instanceof MyPromise) { x.then(resolve, reject); } if (x !== null && (typeof x === 'object' || typeof x === 'function')) { // 防止多次调用 let called; try { // A+ 规定,声明 then = x 的 then方法 let then = x.then; // 若 then 是函数,就默认是 promise 了 if (typeof then === 'function') { then.call(x, y => { if (called) return; called = true; // resolve 的结果依旧是 promise 则继续解析 resolvePromise(promise2, y, resolve, reject); }, err => { // 成功和失败只能调用一个 if (called) return; called = true; reject(err); }) } else { resolve(x); } } catch(e) { if (called) return; called = true; // 取 then 出错了那就不要在继续执行了 reject(e); } } else { resolve(x); } }完善及最终完成版,包括
catch、resolve和reject静态方法、finally、all、race// 创建三个常量用于表示状态,对于经常使用的一些值都应该通过常量来管理,便于开发及后期维护 const PENDING = 'pending'; // 等待 const FULFILLED = 'fulfilled'; // 成功 const REJECTED = 'rejected'; // 失败 class MyPromise { constructor(executor) { // 捕获执行器错误 // 若 executor 执行报错,则直接执行 reject try { executor(this.resolve, this.reject); } catch (e) { this.reject(e); } } // 初始化 state 为等待状态 state = PENDING; // 成功的值 value = undefined; // 失败的原因 reason = undefined; // 成功回调存放 successCallback = []; // 失败回调存放 failCallback = []; resolve = value => { // 若状态不是等待,则阻止程序向下执行 if (this.status !== PENDING) return; // 将状态改为成功 this.status = FULFILLED; // 保存成功后的值 this.value = value; // 成功回调是否存在,若存在则调用 // this.successCallback && this.successCallback(this.value); // 从前到后执行回调 // Array.shift() 取出数组第一个元素,然后()调用,shift 操作后,数组将删除该元素,直到数组为空 while (this.successCallback.length) this.successCallback.shift()(this.value); } reject = reason => { // 若状态不是等待,则阻止程序向下执行 if (this.status !== PENDING) return; // 将状态改为成功 this.status = REJECTED; // 保存成功后的值 this.reason = reason; // 成功回调是否存在,若存在则调用 // this.failCallback && this.failCallback(this.value) // 从前到后执行回调 while (this.failCallback.length) this.failCallback.shift()(this.reason); } then(onFulfilled, onRejected) { // 参数可选 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; // 参数可选 onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }; let promise2 = new MyPromise((resolve, reject) => { // 判断状态 // 下面 resolvePromise 传递的 promise2 其实是不能获取到 promise2,因为 promise2 是 new MyPromise 执行完成之后才有的 // 因此可以通过将下面处理代码变成异步代码,先让其他同步代码先执行完再去执行异步代码,就可以确保拿到 promise2 const fulfilledHandle = () => { setTimeout(() => { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch(e) { // 此处的错误会在下个 then 的错误回调中捕获到 reject(e); } }, 0) }; const rejectedHandle = () => { setTimeout(() => { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { // 此处的错误会在下个 then 的错误回调中捕获到 reject(e); } }, 0) }; if (this.status === FULFILLED) { fulfilledHandle(); } else if (this.status === REJECTED) { rejectedHandle(); } else { this.successCallback.push(fulfilledHandle); this.failCallback.push(rejectedHandle); } }) } } // @param {*} promise2 // @param {*} x // @param {*} resolve // @param {*} reject function resolvePromise(promise2, x, resolve, reject) { // 判断 x 是不是 promise2 // 循环引用报错 if (x === promise2) { // reject报错 return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); } // 若 x 是 Promise 实例则直接调用 then 方法,免去了后面判断 thenable、取 then 方法的操作 if (x instanceof Promise) { return x.then(resolve, reject); } if (x !== null && (typeof x === 'object' || typeof x === 'function')) { // 防止多次调用 let then, called; try { then = x.then; } catch (e) { // 一定要写 return,若访问 then 报错就不走后面判断 then 的逻辑了 return reject(e); } if (typeof then === 'function') { try { then.call(x, y => { if (!called) { called = true; resolvePromise(promise, y, resolve, reject); } }, r => { if (!called) { called = true; reject(r); } }) } catch (e) { if (!called) { reject(e); } } } else { resolve(x); } } else { resolve(x); } } // resolve 静态方法 // 将给定的值包装成 promise 对象并返回 class MyPromise { ... static resolve(value) { if (value instanceof MyPromise) return value; return new MyPromise((resolve, reject) => resolve(value)); } } // reject 静态方法 class MyPromise { ... static reject(reason) { return new MyPromise((resolve, reject) => { reject(reason); }); } } // catch // 可以用来捕获当前 promise 对象的错误 // 该方法不是一个静态方法,被定义在原型对象上 class MyPromise { ... catch(onRejected) { return this.then(null, onRejected); } } // finally // 无论当前 Promise 对象最终是成功还是失败,finally 方法中的回调始终都会被执行一次 // 在 finally 方法后可以链式调用 then 来拿到当前 Promise 对象最终返回的结果 // 该方法不是一个静态方法,被定义在原型对象上 class MyPromise { ... finally(callback) { // this.then 获取当前 promise 对象的状态 return this.then(value => { return MyPromise.resolve(callback()).then(() => value); }, reason => { return MyPromise.resolve(callback()).then(() => { throw reason }); }) } } // all 方法 // Promise.all 方法用来解决异步并发问题,允许按照异步代码调用的顺序得到异步代码执行的结果 // 该方法的返回也是个 promise 对象,若所有任务都是成功的则返回的状态也是成功的,只要有一个失败则返回的状态是失败的 class MyPromise { ... static all(array) { let result = []; let index = 0; return new MyPromise((resolve, reject) => { function addData(key, value) { result[key] = value; index ++; // 判断当所有任务都完成了才调用 resolve 去结束 all 方法 if(index === array.length) { resolve(result); } } for(let i = 0; i < array.length; i ++) { let current = array[i]; if(current instanceof MyPromise) { // promise 对象 current.then(value => addData(i, value), reason => reject(reason)) } else { // 普通值 addData(i, array[i]); } } }); } } // race 方法 // race 方法传入 Promise 对象数组 // race 只返回执行最快的一个 Promise 结果,不论成功与失败 // 返回结果与最快的 Promise 的成功与失败保持一致 class MyPromise { ... static race(array) { return new MyPromise(function (resolve, reject) { for (let i = 0; i < array.length; i++) { let current = array[i]; if (current instanceof MyPromise) { current.then(resolve, reject); } else { resolve(elem); } } } } } // deferred // 这个是 Promise 提供的一个快捷使用,自己实现 promise 时一定要加,否则 promises-aplus-tests promise.js 跑不过 class MyPromise { ... deferred() { let result = {}; dfd.promise = new MyPromise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }) } } -
-
-
实例Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的
const promise = new Promise((resolve, reject) => { console.log(1); resolve(); console.log(2); }); promise.then(() => { console.log(3); }) console.log(4); // 1 // 2 // 4 // 3promise 有 3 种状态:pending、fulfilled 或 rejected。状态改变只能是 pending -> fulfilled 或 pending -> rejected,状态一旦改变则不能再变。下面的 promise2 并不是 promise1,而是返回的一个新的 Promise 实例
const promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('success'); }, 1000); }); const promise2 = promise1.then(() => { throw new Error('error!!!'); }); console.log('promise1', promise1); console.log('promise2', promise2); setTimeout(() => { console.log('promise1', promise1); console.log('promise2', promise2); }, 2000) // promise1 Promise {<pending>} // promise2 Promise {<pending>} // Uncaught (in promise) Error: error!!! at <anonymous>:7:9 // promise1 Promise {<fulfilled>: "success"} // promise2 Promise {<rejected>: Error: error!!! at <anonymous>:7:9构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用,呼应结论:promise 状态一旦改变则不能再变
const promise = new Promise((resolve, reject) => { resolve('success1'); reject('error'); resolve('success2'); }); promise.then((res) => { console.log('then: ', res); }).catch((err) => { console.log('catch: ', err); }) // then: success1promise 可以链式调用
- 提起链式调用通常会想到通过 return this 实现,不过 Promise 并不是这样实现的
- promise 每次调用 then 或 catch 都会返回一个新的 promise,从而实现了链式调用
Promise.resolve(1) .then((res) => { console.log(res); return 2; }).catch((err) => { return 3; }).then((res) => { console.log(res); }) // 1 // 2promise 的 then 或 catch 可被调用多次,但这里 Promise 构造函数只执行一次,或者说 promise 内部状态一经改变且有了一个值,那么后续每次调用 then 或 catch 都会直接拿到该值
const promise = new Promise((resolve, reject) => { setTimeout(() => { console.log('once'); resolve('success'); }, 1000) }); const start = Date.now(); promise.then((res) => { console.log(res, Date.now() - start); }); promise.then((res) => { console.log(res, Date.now() - start); }); // once // success 1004 // success 1004then 或 catch 中 return 一个 error 对象并不会抛出错误,因为返回任意一个非 promise 的值都会被包裹成 promise 对象,即 return new Error('error!!!') 等价于 return Promise.resolve(new Error('error!!!')),所以不会被后续的 catch 捕获
Promise.resolve().then(() => { return new Error('error!!!'); }).then((res) => { console.log('then: ', res); }).catch((err) => { console.log('catch: ', err); }) // then: Error: error!!! at <anonymous>:2:10 // 需要改成下面这样 catch 即可捕获到 Promise.resolve().then(() => { return Promise.reject(new Error('error!!!')); }).then((res) => { console.log('then: ', res) }).catch((err) => { console.log('catch: ', err) }) // catch: Error: error!!! at <anonymous>:2:25then 或 catch 返回的值不能是 promise 本身,否则会造成死循环
const promise = Promise.resolve().then(() => { return promise; }); promise.catch(console.error); // TypeError: Chaining cycle detected for promise #<Promise>then 或 catch 的参数期望是函数,传入非函数则会发生值穿透
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log) // 1then 可以接收两个参数:处理成功的函数及处理错误的函数。catch 是 then 第二个参数的简便写法,但它们用法上有一点需要注意:
then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的 catch 可以捕获之前的错误Promise.resolve().then(function success(res) { throw new Error('error'); }, function fail1(e) { console.error('fail1: ', e); }).catch(function fail2(e) { console.error('fail2: ', e); }) // fail2: Error: error at success (<anonymous>:3:11) // 当然以下代码也可以 Promise.resolve().then(function success1(res) { throw new Error('error'); }, function fail1(e) { console.error('fail1: ', e); }).then(function success2(res) {}, function fail2(e) { console.error('fail2: ', e); }) // fail2: Error: error at success1 (<anonymous>:2:11)process.nextTick 和 promise.then 都属于 microtask(微任务),而 setImmediate 属于 macrotask(宏任务),在事件循环的 check 阶段执行。事件循环的每个阶段(macrotask)之间都会执行 microtask,事件循环的开始会先执行一次 microtask
process.nextTick(() => { console.log('nextTick'); }) Promise.resolve().then(() => { console.log('then'); }) setImmediate(() => { console.log('setImmediate'); }) console.log('end'); // end // nextTick // then // setImmediate
Generator
-
简介Generator是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator是 ES6 中的一个新的语法,其最大的特点就是可以控制函数的执行语法上,首先可以把
Generator函数理解为一个状态机,封装了多个内部状态执行
Generator函数会返回一个遍历器对象,即Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态形式上,
Generator函数是一个普通函数,但是有两个特征:function关键字与函数名之间有一个*号- 函数体内部使用
yield表达式,定义不同的内部状态(yield在英语里的意思就是产出)
function* helloGenerator() { yield 'hello'; yield 'world'; return 'generator'; } var h = helloGenerator();上面代码定义了一个 Generator 函数
helloGenerator,它内部有yield表达式(hello和world),即该函数有三个状态:hello 、world 和 return 语句(结束执行)Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,即遍历器对象(Iterator Object)下一步,必须调用遍历器对象的
next方法,使得指针移向下一个状态。即每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式或return语句为止。换言之,Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行每次调用遍历器对象的
next方法,会返回一个有着value和done两个属性的对象value属性:表示当前的内部状态的值,是yield表达式后面那个表达式的值done属性:是一个布尔值,表示是否遍历结束
h.next(); // {value: 'hello', done: false} h.next(); // {value: 'world', done: false} h.next(); // {value: 'generator', done: true} h.next(); // {value: undefined, done: true}第一次调用
next,Generator函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束第二次调用
next,Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束第三次调用
next,Generator函数从上次yield表达式停下的地方,一直执行到return语句(若没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(若没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束第四次调用
next,此时Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true,以后再调用next方法,返回的都是这个值ES6 没有规定
function关键字与函数名之间的星号必须写在哪个位置,下面的写法都可以function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } // 一般采用这种写法 function*foo(x, y) { ··· } -
yield 表达式由于
Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数,yield表达式就是暂停标志遍历器对象的
next方法的运行逻辑如下:-
遇到
yield表达式,就暂停执行后面的操作并将紧跟在yield后的那个表达式的值,作为返回的对象的value属性值 -
下一次调用
next方法时再继续往下执行,直到遇到下一个yield表达式 -
若没有再遇到新的
yield表达式,就一直运行到函数结束,直到return语句为止并将return语句后的表达式的值,作为返回的对象的value属性值 -
若该函数没有
return语句,则返回的对象的value属性值为undefined
注意:
yield表达式后的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此可以理解为 JS 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能function* gen() { yield 123 + 456; }上面代码中,
yield后的表达式123 + 456不会立即求值,只会在next方法将指针移到这一句时才会求值yield表达式与return语句既有相似之处,也有区别:-
相似之处:都能返回紧跟在语句后的那个表达式的值 -
不同之处:每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能一个函数里面,只能执行一次或者说一个
return语句,但可以执行多次或者说多个yield表达式正常函数只能返回一个值,因为只能执行一次
return;Generator函数可以返回一系列的值,因为可以有任意多个yield从另一个角度看,
Generator生成了一系列的值,这也就是它的名称的来历(英语中generator是“生成器”的意思)
Generator函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数function* gen() { console.log("执行了!"); } let g1 = gen(); console.log(g1); // gen {<suspended>}上面代码中,若函数
gen是一个普通函数的话则调用时就会立即执行,打印"执行了!",但作为一个Generator,调用时是不会立即执行的,只有在调用了next方法后才会执行function* gen() { console.log("执行了!"); } let g1 = gen(); console.log(g1.next()); // 执行了! // {value: undefined, done: true}需要注意,
yield表达式只能用在Generator函数里,用在其他地方都会报错function f() { yield 123; } // Uncaught SyntaxError: Unexpected number另外,
yield表达式若用在另一个表达式之中,必须放在圆括号里面function* demo() { console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK }yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK } -
-
与 Iterator 接口的关系任意一个对象的
Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象由于
Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3]上面代码中
Generator函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了Iterator接口,可以被...运算符遍历了Generator函数执行后返回一个遍历器对象,该对象本身也具有Symbol.iterator属性,执行后返回自身function* gen(){ ... } var g = gen(); g[Symbol.iterator]() === g; // true上面代码中
gen是一个Generator函数,调用它会生成一个遍历器对象g,它的Symbol.iterator属性也是一个遍历器对象生成函数,执行后返回它自己 -
next 方法的参数yield表达式本身没有返回值,或者说总是返回undefined,next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值function* fn() { for(var i = 0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } var g = fn(); g.next(); // { value: 0, done: false } g.next(); // { value: 1, done: false } g.next(true); // { value: 0, done: false }上面代码先定义了一个可以无限运行的
Generator函数fn,next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined,当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增这个功能有很重要的语法意义,
Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的,通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值,即可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为再看一个例子
function* foo(x) { let y = 2 * (yield (x + 1)); let z = yield (y / 3); return (x + y + z); } let it = foo(5); it.next(); // {value: 6, done: false} it.next(); // {value: NaN, done: false} it.next(); // {value: NaN, done: true} let it = foo(5); it.next(); // {value: 6, done: false} it.next(12); // {value: 8, done: false} it.next(13); // {value: 42, done: true}上面代码中,不带参数时,第二次运行
next方法时导致y的值等于2 * undefined(即NaN),除以3以后还是NaN,因此返回对象的value属性也等于NaN,第三次运行next方法时z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN若向
next方法提供参数,返回结果则完全不一样。上面代码第一次调用next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42注意,由于
next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数再看一个通过
next方法的参数,向Generator函数内部输入值的例子function* dataConsumer() { console.log('Started'); console.log(`1. ${yield}`); console.log(`2. ${yield}`); return 'result'; } let genObj = dataConsumer(); genObj.next(); // Started genObj.next('a'); // 1. a genObj.next('b'); // 2. b若想要第一次调用
next方法时就能够输入值,可以在Generator函数外面再包一层function wrapper(generatorFn) { return function (...args) { let genObj = generatorFn(...args); genObj.next(); return genObj; }; } const wrapped = wrapper(function* () { console.log(`Input: ${yield}`); return 'DONE'; }); wrapped().next('hi!') // Input: hi! // {value: 'DONE', done: true} -
for...of 循环for...of循环可以自动遍历Generator函数运行时生成的Iterator对象,且此时不再需要调用next方法function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5上面代码使用
for...of循环,依次显示 5 个yield表达式的值,这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止且不包含该返回对象,所以上面代码的return语句返回的6不包括在for...of循环之中下面是一个利用
Generator函数和for...of循环,实现斐波那契数列的例子function* fibonacci() { let [prev, curr] = [0, 1]; for (;;) { yield curr; [prev, curr] = [curr, prev + curr]; } } for (let n of fibonacci()) { if (n > 10) break; console.log(n); } // 1 1 2 3 5 8从上面代码可见,使用
for...of语句时不需要使用next方法利用
for...of循环可以写出遍历任意对象的方法。原生的 JS 对象没有遍历接口,无法使用for...of循环,通过Generator函数为它加上这个接口,就可以用了function* objEntries(obj) { let propKeys = Reflect.ownKeys(obj); for (let propKey of propKeys) { yield [propKey, obj[propKey]]; } } let name = { first: 'tn', last: 'tnna' }; for (let [key, value] of objEntries(name)) { console.log(`${key}: ${value}`); } // first: tn // last: tnna上面代码中,对象
name原生不具备Iterator接口,无法用for...of遍历,这时我们可以通过Generator函数objEntries为它加上遍历器接口,就可以使用for...of遍历了加上遍历器接口的另一种写法是,将
Generator函数加到对象的Symbol.iterator属性上面function* objEntries() { let propKeys = Object.keys(this); for (let propKey of propKeys) { yield [propKey, this[propKey]]; } } let name = { first: 'tn', last: 'tnna' }; name[Symbol.iterator] = objEntries; for (let [key, value] of name) { console.log(`${key}: ${value}`); } // first: tn // last: tnna除了
for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的也都是遍历器接口,这意味着它们均可将Generator函数返回的Iterator对象作为参数function* numbers () { yield 1; yield 2; return 3; yield 4; } // 扩展运算符 [...numbers()]; // [1, 2] // Array.from 方法 Array.from(numbers()); // [1, 2] // 解构赋值 let [x, y] = numbers(); x // 1 y // 2 // for...of 循环 for (let n of numbers()) { console.log(n); } // 1 // 2 -
yield* 表达式若在
Generator函数内部调用另一个Generator函数,需要在前者的函数体内部,自己手动完成遍历function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; // 手动遍历 foo() for (let i of foo()) { console.log(i); } yield 'y'; } for (let v of bar()){ console.log(v); } // x // a // b // y上面代码中
foo和bar均是Generator函数,在bar里面调用foo就需要手动遍历foo,若有多个Generator函数嵌套,写起来就非常麻烦ES6 提供
yield*表达式作为解决办法,用来在一个Generator函数里面执行另一个Generator函数function* bar() { yield 'x'; yield* foo(); yield 'y'; } // 等同于 function* bar() { yield 'x'; yield 'a'; yield 'b'; yield 'y'; } // 等同于 function* bar() { yield 'x'; for (let v of foo()) { yield v; } yield 'y'; } for (let v of bar()){ console.log(v); } // x // a // b // y再来看一个对比的例子
function* inner() { yield 'hello!'; } function* outer1() { yield 'open'; yield inner(); yield 'close'; } var gen = outer1(); gen.next(); // {value: 'open', done: false} gen.next(); // {value: inner, done: false} gen.next(); // {value: 'close', done: false} function* outer2() { yield 'open' yield* inner() yield 'close' } var gen = outer2(); gen.next(); // {value: 'open', done: false} gen.next(); // {value: 'hello!', done: false} gen.next(); // {value: 'close', done: false}上面例子中,
outer2使用了yield*,outer1没使用,结果就是outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值从语法角度看,若
yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象,这被称为yield*表达式yield*后面的Generator函数没有return语句时等同于在Generator函数内部部署一个for...of循环function* test(iter1, iter2) { yield* iter1; yield* iter2; } // 等同于 function* test(iter1, iter2) { for (var value of iter1) { yield value; } for (var value of iter2) { yield value; } }上面代码中
yield*后面的Generator函数没有return语句时,是for...of的一种简写形式,完全可以用后者替代前者若有
return语句时则需要用var value = yield* iterator的形式获取return语句的值若
yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员function* gen() { yield* ["a", "b", "c"]; } gen().next(); // {value: 'a', done: false}实际上任何数据结构只要有
Iterator接口就可以被yield*遍历const read = (function* () { yield 'hello'; yield* 'hello'; })(); read.next(); // {value: 'hello', done: false} read.next(); // {value: 'h', done: false}上面代码中
yield表达式返回整个字符串,yield*语句返回单个字符,因为字符串具有Iterator接口,所以被yield*遍历若被代理的
Generator函数有return语句,则就可向代理它的Generator函数返回数据function* foo() { yield 2; yield 3; return "foo"; } function* bar() { yield 1; var v = yield* foo(); console.log("v: " + v); yield 4; } var it = bar(); it.next(); // {value: 1, done: false} it.next(); // {value: 2, done: false} it.next(); // {value: 3, done: false} it.next(); // v: foo // {value: 4, done: false} it.next(); // {value: undefined, done: true}上面代码在第四次调用
next方法时会有输出,这是因为函数foo的return语句向函数bar提供了返回值再看一个例子
function* genFuncWithReturn() { yield 'a'; yield 'b'; return 'c'; } function* logReturned(genObj) { let result = yield* genObj; console.log(result); } [...logReturned(genFuncWithReturn())] // c // ['a', 'b']上面代码中存在两次遍历:第一次是
...扩展运算符遍历函数logReturned返回的遍历器对象,第二次是yield*语句遍历函数genFuncWithReturn返回的遍历器对象这两次遍历的效果是叠加的,最终表现为
...扩展运算符遍历函数genFuncWithReturn返回的遍历器对象,所以最后的数据表达式得到的值等于[ 'a', 'b' ]。但函数genFuncWithReturn的return语句返回值c会返回给函数logReturned内部的result变量,因此会有终端输出yield*命令可以很方便地取出嵌套数组的所有成员function* iterTree(tree) { if (Array.isArray(tree)) { for(let i = 0; i < tree.length; i++) { yield* iterTree(tree[i]); } } else { yield tree; } } const tree = [ 'a', ['b', 'c'], ['d', 'e'] ]; for(let x of iterTree(tree)) { console.log(x); } // a b c d e // 由于扩展运算符 `...` 默认调用 `Iterator` 接口,所以上面这个函数也可以用于嵌套数组的平铺 [...iterTree(tree)]; // ["a", "b", "c", "d", "e"] -
作为对象属性的 Generator 函数若一个对象的属性是
Generator函数,可以简写成下面的形式const obj = { * genMethod() { // ··· } }; // 等价于 const obj = { genMethod: function* () { // ··· } };上面代码中
genMethod属性前面有一个星号,表示这个属性是一个Generator函数 -
Generator 函数的 thisGenerator函数总是返回一个遍历器,ES6 规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法function* gen() {} gen.prototype.hello = function () { return 'say hi!'; }; const obj = gen(); obj instanceof gen; // true obj.hello(); // 'say hi!'上面代码中,
Generator函数gen返回的遍历器obj,是gen的实例,而且继承了gen.prototype。但若把gen当作普通的构造函数并不会生效,因为gen返回的总是遍历器对象,而不是this对象function* gen() { this.a = 11; } let obj = gen(); obj.next(); obj.a; // undefined上面代码中
Generator函数gen在this对象上面添加了一个属性a,但是obj对象拿不到这个属性Generator函数也不能跟new命令一起用,会报错function* F() { yield this.x = 2; yield this.y = 3; } new F(); // Uncaught TypeError: F is not a constructor有办法让
Generator函数返回一个正常的对象实例,既可以用next方法又可获得正常的this吗?下面是一个变通方法。首先生成一个空对象,使用
call方法绑定Generator函数内部的this,这样构造函数调用后该空对象就是Generator函数的实例对象了function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } var obj = {}; var f = F.call(obj); f.next(); // {value: 2, done: false} f.next(); // {value: 3, done: false} f.next(); // {value: undefined, done: true} obj.a; // 1 obj.b; // 2 obj.c; // 3上面代码中,首先是
F内部的this对象绑定obj对象然后调用它,返回一个Iterator对象,这个对象执行三次next方法(因为F内部有两个yield表达式),完成F内部所有代码的运行。此时所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例上面代码中,执行的是遍历器对象
f,但是生成的对象实例是obj,有没有办法将这两个对象统一呢?一个办法就是将obj换成F.prototypefunction* gen() { this.a = 1; yield this.b = 2; yield this.c = 3; } var f = F.call(F.prototype); // 将 F 改成构造函数,就可以对它执行 new 命令 function F() { return gen.call(gen.prototype); } var f = new F(); f.next(); // {value: 2, done: false} f.next(); // {value: 3, done: false} f.next(); // {value: undefined, done: true} f.a; // 1 f.b; // 2 f.c; // 3 -
应用Generator可以暂停函数执行,返回任意表达式的值。这种特点使得Generator有多种应用场景-
异步操作的同步化表达
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里,等到调用next方法时再往后执行,这实际上等同于不需要写回调函数,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数Ajax是典型的异步操作,通过Generator函数部署Ajax操作,可以用同步的方式表达function* main() { var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value); } function request(url) { makeAjaxCall(url, function(response){ it.next(response); }); } var it = main(); it.next();上面代码的
main函数就是通过Ajax操作获取数据。可以看到除了多了一个yield,它几乎与同步操作的写法完全一样。注意makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式本身是没有值的,总是等于undefined下面另一个例子是通过
Generator函数逐行读取文本文件function* numbers() { let file = new FileReader("numbers.txt"); try { while(!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } } -
控制流管理
若有一个多步操作非常耗时,采用回调函数,可能会写成下面这样
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { ... }); }); }); });采用
Promise改写上面的代码Promise.resolve(step1) .then(step2) .then(step3) .then(step4) .then(function (value4) { ... }, function (error) { ... }) .done();上面代码已经把回调函数改成了直线执行的形式,加入了大量
Promise的语法,Generator函数可以进一步改善代码运行流程function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } }然后使用一个函数,按次序自动执行所有步骤
scheduler(longRunningTask(initialValue)); function scheduler(task) { var taskObj = task.next(task.value); // 若 Generator 函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } }注意,上面这种做法只适合
同步操作,即所有的task都必须是同步的,不能有异步操作。因为这里的代码一得到返回值就继续往下执行,没有判断异步操作何时完成下面利用
for...of循环会自动依次执行yield命令的特性,提供一种更一般的控制流管理的方法let steps = [step1Fun, step2Fun, step3Fun]; function* iterateSteps(steps) { for (let i = 0; i < steps.length; i++) { let step = steps[i]; yield step(); } }上面代码中数组
steps封装了一个任务的多个步骤,Generator函数iterateSteps则是依次为这些步骤加上yield命令将任务分解成步骤之后还可以将项目分解成多个依次执行的任务
let jobs = [job1, job2, job3]; function* iterateJobs(jobs) { for (var i = 0; i < jobs.length; i++) { var job = jobs[i]; yield* iterateSteps(job.steps); } }上面代码中数组
jobs封装了一个项目的多个任务,Generator函数iterateJobs则是依次为这些任务加上yield*命令最后就可以用
for...of循环一次性依次执行所有任务的所有步骤for (var step of iterateJobs(jobs)){ console.log(step.id); }上面的做法只能用于所有步骤都是
同步操作的情况,不能有异步操作的步骤。for...of的本质是一个while循环,所以上面的代码实质上执行的是下面的逻辑var it = iterateJobs(jobs); var res = it.next(); while (!res.done){ var result = res.value; // ... res = it.next(); } -
部署 Iterator 接口
利用
Generator函数,可以在任意对象上部署Iterator接口function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); } // foo 3 // bar 7上述代码中
myObj是一个普通对象,通过iterEntries函数就有了Iterator接口,即可以在任意对象上部署next方法下面是一个对数组部署
Iterator接口的例子,尽管数组原生具有这个接口function* makeSimpleGenerator(arr){ var nextIndex = 0; while(nextIndex < arr.length){ yield arr[nextIndex ++]; } } var gen = makeSimpleGenerator(['yo', 'ya']); gen.next(); // {value: 'yo', done: false} gen.next(); // {value: 'ya', done: false} gen.next(); // {value: undefined, done: true} -
作为数据结构
Generator可以看作是数据结构,更确切地说可以看作是一个数组结构,因为Generator函数可以返回一系列的值,这意味着它可以对任意表达式提供类似数组的接口function* doStuff() { yield fs.readFile.bind(null, 'hello.txt'); yield fs.readFile.bind(null, 'world.txt'); yield fs.readFile.bind(null, 'and-such.txt'); }上面代码就是依次返回三个函数,但是由于使用了
Generator函数,导致可以像处理数组那样处理这三个返回的函数for (task of doStuff()) { // task 是一个函数,可以像回调函数那样使用它 }实际上,若用 ES5 表达,完全可以用数组模拟
Generator的这种用法function doStuff() { return [ fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt') ]; }上面的函数可以用一模一样的
for...of循环处理,两相一比较就不难看出Generator使得数据或操作具备了类似数组的接口
-
async / await
-
背景在 JS 中处理异步操作的回调 (callback) 通常会导致多嵌套的代码块,俗称回调地狱,这样的代码复杂,可读性、可维护性非常不友好
ES6
Promise的出现,使得能够扁平化回调函数,告别回调地狱,写出优雅的代码。但是在实践中发现Promise并不完美,若Promise的回调中出现嵌套,依旧会出现回调地狱async/await的出现提供了一种新的编写异步代码方式,使得异步代码看起来像是同步代码 -
含义ES2017 标准引入了
async函数,使得异步操作变得更加方便,async的本质就是Generator函数的语法糖下面 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); }); }); }; const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; // gen 可以写成 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函数对Generator函数的改进体现在以下四点:-
内置执行器:Generator函数的执行必须靠执行器,所以才有了如co模块,而async函数自带执行器,因此async函数的执行与普通函数一模一样,只要一行asyncReadFile(); `` 调用 `asyncReadFile` 函数,然后它就会自动执行,输出最后结果。这完全不像 `Generator` 函数需要调用 `next` 方法或用 `co` 模块才能真正执行得到最后结果 -
更好的语义:async/await比起*和yield来说语义更清晰,async表示函数里有异步操作,await表示紧跟在后面的表达式需等待结果 -
更广的适用性:async函数的await命令后面可以是Promise对象、原始类型的值(数值、字符串和布尔值,但这时会自动转成立即resolved的Promise对象);而co模块约定yield命令后面只能是Thunk函数或Promise对象 -
返回值是
Promise:async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了,可以用then方法指定下一步的操作
进一步说,
async函数完全可以看作多个异步操作包装成的一个Promise对象,而await命令就是内部then命令的语法糖 -
-
基本用法async函数有多种使用形式// 函数声明 async function foo() {} // 函数表达式 const foo = async function() {}; // 对象的方法 let obj = { async foo() {} }; obj.foo().then(...) // 箭头函数 const foo = async () => {}; // 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函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行时一旦遇到await就会先返回,等到异步操作完成再接着执行函数体内后面的语句// 指定 50 毫秒以后输出 `hello world` function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function asyncPrint(value, ms) { await timeout(ms); console.log(value); } asyncPrint('hello world', 50);async关键字表明该函数内部有异步操作,调用该函数时会立即返回一个Promise对象,由于async函数返回的是Promise对象,可以作为await命令的参数,所以上面的例子也可以写成下面的形式async function timeout(ms) { await new Promise((resolve) => { setTimeout(resolve, ms); }); } async function asyncPrint(value, ms) { await timeout(ms); console.log(value); } asyncPrint('hello world', 50); -
语法规则async函数返回一个Promise对象,async函数内部return语句返回的值会成为then方法回调函数的参数async function fn() { return 'hello world!'; } fn().then(value => console.log(value)); // hello world!async函数内部抛出错误时会导致返回的Promise对象变为rejected状态,抛出的错误对象会被catch方法回调函数接收到async function fn() { throw new Error('wrong!!!'); } fn().then(value => console.log('resolve', value), reason => console.log('reject', reason)) // reject Error: wrong!!!async函数返回的Promise对象必须等到内部所有await命令后的Promise对象执行完后才会发生状态改变,除非遇到return语句或抛出错误,即只有async函数内部的异步操作执行完才会执行then方法指定的回调函数async function getTitle(url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)</title>/i)[1]; } getTitle('https://tc39.github.io/ecma262/').then(console.log); // "ECMAScript 2017 Language Specification"函数
getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题,只有这三个操作全部完成才会执行then方法里面的console.logawait命令:-
正常情况下
await命令后面是一个Promise对象,返回该对象的结果若不是Promise对象就直会调用Promise.resolve()进行转化再返回async function fn() { // 等同于 // return 123; return await 123; } fn().then(value => console.log(value)); // 123 -
另一种情况,
await命令后面是一个thenable对象(即定义了then方法的对象),则await会将其等同于Promise对象class Sleep { constructor(timeout) { this.timeout = timeout; } then(resolve, reject) { const startTime = Date.now(); setTimeout(() => resolve(Date.now() - startTime), this.timeout); } } (async () => { const sleepTime = await new Sleep(1000); console.log(sleepTime); })(); // 1000上面
await命令后面是一个Sleep对象的实例,这个实例不是Promise对象,但是因为定义了then方法,await会将其视为Promise处理
JS 一直没有休眠的语法,但借助
await命令就可以让程序停顿指定的时间function sleep(interval) { return new Promise(resolve => { setTimeout(resolve, interval); }) } // 用法 async function oneTimeWait() { for(let i = 1; i <= 5; i++) { console.log(i); await sleep(1000); } } oneTimeWait();await命令后面的Promise对象若变为rejected状态,则reject的参数会被catch方法的回调函数接收到async function fn() { await Promise.reject('wrong!!!'); } fn().then(value => console.log(value)).catch(reason => console.log(reason)); // wrong!!!注意,上面代码中
await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数,这里若在await前面加上return,效果是一样的任何一个
await语句后面的Promise对象变为reject状态,则整个async函数都会中断执行async function fn() { await Promise.reject('wrong!!!'); await Promise.resolve('hello world'); // 不会执行 }有时我们希望即使前一个异步操作失败也不要中断后面的异步操作,这时可以将第一个
await放在try...catch结构里,这样不管这个异步操作是否成功,第二个await都会执行async function fn() { try { await Promise.reject('wrong!!!'); } catch(e) {} return await Promise.resolve('hello world!'); } fn().then(value => console.log(value)); // hello world!另一种方法是
await后面的Promise对象跟一个catch方法,处理前面可能出现的错误async function fn() { await Promise.reject('wrong!!!').catch(e => console.log(e)); return await Promise.resolve('hello world!'); } fn().then(value => console.log(value)); // wrong!!! // hello world!使用注意点:
-
上面已经说过
await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中 -
多个
await命令后面的异步操作,若不存在继发关系,最好让它们同时触发。以下两种写法getFoo和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函数中,若用在普通函数就会报错async function fn(db) { let arr = [{}, {}, {}]; arr.forEach(function (item) { await db.post(item); }); } // Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules此处注意:若将
forEach方法的参数改成async函数,也可能有问题。如下面代码,可能不会正常工作,原因是这时三个db.post()操作将是并发执行,而不是继发执行,正确的写法是采用for循环function fn(db) { let arr = [{}, {}, {}]; arr.forEach(async function (item) { await db.post(item); }); }另一种方法是使用数组的
reduce()方法async function fn(db) { let arr = [{}, {}, {}]; await arr.reduce(async (_, item) => { await _; await db.post(item); }, undefined); }上面例子中,
reduce()方法的第一个参数是async函数,导致该函数的第一个参数是前一步操作返回的Promise对象,所以必须使用await等待它操作结束。另外,reduce()方法返回的是arr数组最后一个成员的async函数的执行结果,也是一个Promise对象,在它前面也必须加上await上面的
reduce()的参数函数里没有return语句,原因是这个函数的主要目的是db.post()操作,不是返回值。而且async函数不管有没有return语句,总是返回一个Promise对象,所以这里的return是不必要的若确实希望多个请求并发执行,可以使用
Promise.all方法,当三个请求都会resolved时,下面两种写法效果相同async function fn(db) { let arr = [{}, {}, {}]; let promises = arr.map((item) => db.post(item)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function fn(db) { let arr = [{}, {}, {}]; let promises = arr.map((item) => db.post(item)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
-
-
async 函数的实现原理async函数的实现原理就是将Generator函数和自动执行器包装在一个函数里async function fn(args) {...} // 等同于 function fn(args) { return spawn(function* () {...}); }所有的
async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器下面给出
spawn函数的实现function spawn(gen) { return new Promise(function(resolve, reject) { const g = gen(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return g.next(v); }); }, function(e) { step(function() { return g.throw(e); }); }); } step(function() { return g.next(undefined); }); }); } -
与其他异步处理方法的比较通过
async/await可以彻底摆脱回调地狱,以同步方式编写异步代码,代码简洁,十分友好通过一个例子来看
async函数与Promise、Generator函数的比较假定某个 DOM 元素上面部署了一系列的动画,前一个动画结束才能开始后一个,若当中有一个动画出错就不再往下执行,返回上一个成功执行的动画的返回值
Promise的写法function chainAnimationsPromise(elem, animations) { // 变量 ret 用来保存上一个动画的返回值 let ret = null; // 新建一个空的 Promise let p = Promise.resolve(); // 使用 then 方法添加所有动画 for(let anim of animations) { p = p.then(function(val) { ret = val; return anim(elem); }); } // 返回一个部署了错误捕捉机制的 Promise return p.catch(function(e) { /* 忽略错误,继续执行 */ }).then(function() { return ret; }); }虽然
Promise的写法比回调函数的写法大大改进,但是一眼看上去代码完全都是Promise的API(then、catch等),操作本身的语义反而不容易看出来Generator函数的写法function chainAnimationsGenerator(elem, animations) { return spawn(function*() { let ret = null; try { for(let anim of animations) { ret = yield anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }); }上面代码使用
Generator函数遍历了每个动画,语义比Promise写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于必须有一个任务运行器来自动执行Generator函数,上面的spawn函数就是自动执行器,它返回一个Promise对象,而且必须保证yield语句后面的表达式返回一个Promiseasync函数的写法async function chainAnimationsAsync(elem, animations) { let ret = null; try { for(let anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }可以看到
async函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将Generator写法中的自动执行器改在语言层面提供,不暴露给用户,因此代码量最少错误处理:try...catch可以处理同步、异步错误。如下代码块 一 中,try...catch并不能捕捉then方法中JSON.parse异常出错,因为它在Promise中,我们需要使用catch,这样错误处理代码非常冗余且在实际生产代码会更加复杂;但是在代码块 二 中,使用了async/await使得try...catch能捕捉JSON.parse可能的异常出错// 代码块一 function test () { try { getJSON().then((res) => { var data = JSON.parse(res); // 此处可能出错 }) } catch (e) { ... } } // 代码块二 async function test () { try { var data = JSON.parse(await getJSON()); } catch (e) { ... } }中间值:有时会遇到这样的情形,promise1 返回值 value1,promise2 依赖 value1,返回value2,promise3 依赖 value1 和 value2。最简单的做法是通过嵌套解决 promise 间的依赖,显然这种简单粗暴的处理方式使得又掉进回调地狱了,还可以通过中间变量来抹平这个回调嵌套,而通过async/await可以最优、最简洁的解决这个问题。注意,在下面代码的返回语句 promise3 并没有 await,因为异步函数的会将其返回值隐式封装在Promise.resolve中function test() { return promise1().then(value1 => { return promise2(valu1).then(value2 => { return promise3(value1, value2); }) }) } function test() { var value1; var value2; return promise1().then(vle => { value1 = vle; return promise2(value1); }).then(vle => { value2 = vle; return promise3(value1, value2); }) } async function test() { var value1 = await promise1(); var value2 = await promise2(value1); return promise3(value1, value2); }条件语句:使用async/await来书写条件语句会更加直观function loadData() { return getJSON().then(function(response) { if (response.needsAnotherRequest) { return makeAnotherRequest(response) .then(function(anotherResponse) { console.log(anotherResponse) return anotherResponse }) } else { console.log(response) return response } }) } // async async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) { var anotherResponse = await makeAnotherRequest(response); console.log(anotherResponse) return anotherResponse } else { console.log(response); return response; } }async函数可以保留运行堆栈const a = () => { b().then(() => c()); };上面代码中,函数
a内部运行了一个异步任务b,当b运行时函数a不会中断而是继续执行,等到b运行结束时可能a早就运行结束了,b所在的上下文环境已经消失了,若b或c报错,错误堆栈将不包括a现将这个例子改成
async函数const a = async () => { await b(); c(); };上面代码中
b运行时a是暂停执行的,上下文环境都保存着,一旦b或c报错,错误堆栈将包括a再看下面例子
let a = 0; let b = async () => { a = a + await 10; console.log('2', a); }; b(); a ++; console.log('1', a); // '1' 1 // '2' 10上面代码中,首先函数
b先执行,在执行到await 10之前变量a还是0,因为await内部实现了generator,generator会保留堆栈中的东西,所以这时 a = 0 被保存下来因为
await是异步操作,后面的表达式不返回Promise的话就会包装成Promise.reslove(返回值),然后去执行函数外的同步代码函数外的同步代码执行完毕后开始回来执行异步代码,将保存下来的值拿出来使用,这时候
a = 0 + 10 = 10
总结
对于常用的不同异步编程处理方案,各有优势劣势,也没必要顶一个踩一个,虽然技术在不断发展优化,但有些技术不至于淘汰如此之快,存在即合理。个人观点最好的做法是针对不同的业务场景可根据情况选择合适高效的方案