同步与异步 同步是指发起一个请求时,如果未得到请求结果,代码逻辑将会等待,直到结果出来才会继续执行之后的代码。
异步是指当发起一个请求时,不会等待请求结果,直接继续执行后面的代码。请求结果的处理逻辑,会添加一个监听,等到反馈结果出来后,在回调函数中处理对应的逻辑。
使用 Promise 模拟一个发起请求的函数,该函数在 1s 之后返回数值 30。
1 2 3 4 5 6 7 8 9 function fn ( ) { return new Promise (function (resolve, reject ) { setTimeout (function ( ) { resolve (30 ); }, 1000 ); }); } fn ().then ((res ) => console .log (res));
在该函数的基础上,我们可以使用 async/await 来模拟同步的效果。
1 2 3 4 5 6 7 8 9 10 var foo = async function ( ) { var res = await fn (); console .log (res); console .log ('next code' ); }; foo ();
而异步效果则会有不同的输出
1 2 3 4 5 6 7 8 9 10 var foo = function ( ) { fn ().then ((res ) => { console .log (res); }); console .log ('next code' ); };
ajax ajax 是网页与服务端进行数据交互的一种技术。 我们可以通过服务端提供的接口,利用 ajax 向服务端请求需要的数据
整个过程的简单实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var url = 'http: www.demo.com/user/info' ;var result;var XHR = new XMLHttpRequest ();XHR .open ('GET' , url, true );XHR .send ();XHR .onreadystatechange = function ( ) { if (XHR .readyState == 4 && XHR .state == 200 ) { result = XHP .reponse ; console .log (result); } };
这看起来没什么麻烦的,但是这时候,如果我们还需要做另一个 ajax 请求,这个请求的参数是从上一个 ajax 请求中获取的,那么我们就不得不如下这么做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var url = "http: www.demo.com/user/info" var result;var XHR = new XMLHttpRequest ()XHR .open ('GET' , url, true )XHR .send ()XHR .onreadystatechange = function ( ) { if (XHR .readyState == 4 && XHR .state == 200 ) { result = XHP .reponse console .log (result) var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams ; var XHR2 = new XMLHttpRequest (); XHR2 .open ('GET' , url, true ); XHR2 .send (); XHR2 .onreadystatechange = function ( ) { ... } } }
当出现第三个(甚至更多)仍然依赖上一个请求的时候,代码就变成了一场灾难。
我们需要不停的嵌套回调函数。这样的灾难,我们称之为 回调地狱。
**Promise **可以帮助我们解决这个问题。
Promise 我们知道,如果要确保代码在谁之后执行,可以利用函数调用栈,将想要执行的代码放入回调函数中。 如
1 2 3 4 5 6 7 8 9 10 11 function want ( ) { console .log ('这是你想要执行的代码' ); } function fn (want ) { console .log ('这里表示执行了一大堆其他代码' ); want && want (); } fn (want);
或者可以利用任务队列
1 2 3 4 5 6 7 8 9 10 11 function want ( ) { console .log ('这是你想要执行的代码' ); } function fn (want ) { want && setTimeout (want, 0 ); console .log ('这里表示执行了一大堆其他代码' ); } fn (want);
与 setTimeout 类似, Promise 也可以认为是一种任务分发器,它将任务分发到 PromiseJobs 执行队列中。通常的用法是,我们发起一个请求,然后等待并处理请求结果。
简单用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var tag = true ;var p = new Promise (function (resolve, reject ) { if (tag) { resolve (); } else { reject (); } }); p.then (function (result ) { console .log (result); }).catch (function (err ) { console .log (err); });
定义 Promise 是一个对象,保存着未来某一时刻才会执行的事件。
我们常使用 Promise 来解决反馈结果需要等待的场景。
例如
前端向服务端发送一个接口请求。请求结果不会马上返回,而是需要等待一段时间。
加载图片,需要等待一段时间
弹窗中,等待用户点击确认或者取消
基础知识 创建 Promise 实例 1 const p = new Promise ();
Promise 函数中的第一个参数为一个回调函数,我们可以称之为 executor 。通常情况下,在这个函数中,我们将会执行发起请求操作,并修改结果的状态值。
1 2 3 4 5 6 7 8 const p = new Promise ((resolve, reject ) => { if (true ) { resolve (); } if (false ) { reject (); } });
状态 状态有三种
pending:等待结果状态
fulfilled:已出结果,结果符合预期完成状态
rejected:已出结果,结果未符合预期完成状态
promise 表达的就是从发起请求开始,从没有结果 padding 到有结果 fulfilled/rejected 的一个过程。
在 executor 函数中,我们可以分别使用 resolve 与 reject 将状态修改为对应的 fulfilled 与 rejected.
resolve/reject 是 executor 函数的两个参数。他们能够将请求结果的具体数据传递出去。
Promise 实例拥有 then
方法,用来处理请求结果变为 fulfilled 状态时的逻辑。then
的第一个参数也是一个回调函数,该函数的参数则是 resolve 传递出来的数据。第二个参数用来处理 rejected 状态时的逻辑。
Promise 实例拥有 catch
方法,用来处理请求结果变为 rejected 时的逻辑。catch
的第一个参数也是一个回调函数,该函数的参数则是 reject 传递出来的数据。
基本使用 写个例子感受一下 Promise 的用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function fn (num ) { return new Promise (function (resolve, reject ) { setTimeout (function ( ) { if (typeof num == 'number' ) { resolve (num); } else { var err = num + ' is not a number' ; reject (err); } }, 2000 ); }); } fn ('abc' ) .then (function (resp ) { console .log (resp); }) .catch (function (err ) { console .log (err); }); console .log ('next code' );
then
方法可以接收两个参数,第一个参数用来接收 fulfilled 状态的逻辑,第二个参数用来处理 rejected 状态的逻辑。
1 2 3 4 5 6 7 8 fn ('abc' ).then ( function (resp ) { console .log (resp); }, function (err ) { console .log (err); }, );
因此 catch
方法其实与下面的写法等价。
1 2 3 fn ('abc' ).then (null , function (err ) { console .log (er); });
then
方法返回的仍然是一个 Promise 实例对象,因此 then 方法可以链式调用,通过在内部 return 的方式,能够将数据持续往后传递。
封装 ajax 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var url = 'http://www.demo.com/user/info' ;function getJSON (url ) { return new Promise (function (resolve, reject ) { var XHR = new XMLHttpRequest (); XHR .open ('GET' , url, true ); XHR .send (); XHR .onreadystatechange = function ( ) { if (XHR .readyState == 4 ) { if (XHR .status == 200 ) { try { var response = JSON .parse (XHR .responseText ); resolve (response); } catch (e) { reject (e); } } else { reject (new Error (XHR .statusText )); } } }; }); } getJSON (url).then (function (resp ) { console .log (resp); });
Promise.all 当有一个 ajax 请求,它的参数需要另外 2 个甚至更多请求都有了结果之后才能确定,那么这个时候,就需要 Promise.all 来帮助我们应该这个场景。
1 var p = Promise .all ([p1, p2, p3]);
Promise.all 接收一个由 Promise 对象组成的数组作为参数,当 Promise 对象状态都变成 fulfilled 的时候,才会去调用 then 方法。
如果其中一个 Promise 对象状态变成 rejected,那么 p 的状态就会变成 rejected,第一个被 reject 的实例的返回值会传递给回调函数。
如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all() 的 catch() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const p5 = new Promise (function (resolve ) { resolve ('hello' ); }); const p6 = new Promise (function (resolve, reject ) { throw new Error ('报错了' ); }).catch ((e ) => e); Promise .all ([p5, p6]) .then ((res ) => console .log (res)) .catch ((e ) => console .log (e));
Promise.race 与 Promise.all 相似的是,Promise.race 也是接收一个 Promise 对象组成的数组作为参数,不同的是,只要当数组中的一个 Promise 状态变为 fulfilled 或者 rejected 时,就可以调用 .then 方法了。
1 var p = Promise .race ([p1, p2, p3]);
Promise.race 可以理解为 Promise 实例赛跑,哪个实例有了状态就返回哪个,通常用于处理规定时间内请求超时的情况。
如果 5 秒之内请求无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Promise .race ([ new Promise ((resolve, reject ) => { setTimeout (resolve, 10000 , 'my name is a' ); }), new Promise (function (resolve, reject ) { setTimeout (reject, 5000 , new Error ('超时了' )); }), ]) .then ((res ) => { console .log (res); }) .catch ((err ) => { console .log ('err: ' , err); alert (err); });
封装 如何封装与使用息息相关。
加载图片 封装一个加载图片的函数,Promise 的使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function imageLoad (url ) { const img = new Image (); img.src = url; return new Promise (function (resolve, reject ) { img.onload = function ( ) { resolve ('图片加载成功' ); }; img.onerror = function ( ) { reject ('图片加载失败' ); }; }); }
然后我们就可以使用 imageLoad 来执行图片加载完成之后的逻辑。
1 2 3 4 5 6 7 imageLoad ('xxx.png' ) .then ((res ) => { alert (res); }) .catch ((err ) => { alert (err); });
封装的核心关键是: Promise 的最终目的是,为了执行 then 中的回调函数,我们称它为 then_cb
。
所以在封装的时候,我们就应该思考如何在 Promise 内部,让 then_cb
执行。
简易版 MyPromise 显而易见,Promise 包含原型方法 then,构造函数需要传递回调函数 executor
,该回调函数包含两个参数,resolve 与 reject 。
根据这些特点,我们得出:
1 2 3 4 5 6 7 8 9 10 11 12 class MyPromise { construcotr (executor) { executor (this ._resolve .bind (this ), this ._reject .bind (this ) } _resolve (value) {} _reject () {} then (then_cb) {} }
目的是为了调用 then_cb ,通过封装加载图片可以发现,调用 resolve 时,then_cb 才会执行,所以可以得出结论, then_cb 的执行需要被 resolve 触发 。
我们可以通过保存 then_cb 引用的方式来解决。所以代码就变成了这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class MyPromise { constructor (executor ) { this .thenCallback = null ; executor (this ._resolve .bind (this ), this ._reject .bind (this )); } _resolve (value ) { this .thenCallback (value); } _reject (value ) {} then (then_cb ) { this .thenCallback = then_cb; } }
同理,再解决 catch 回调函数的执行问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class MyPromise { constructor (executor ) { this .thenCallback = null ; this .rejectCallback = null ; executor (this ._resolve .bind (this ), this ._reject .bind (this )); } _resolve (value ) { this .thenCallback (value); } _reject (value ) { this .rejectCallback (value); } then (then_cb, onRejected ) { this .thenCallback = then_cb; this .rejectCallback = onRejected; } catch (onRejected) { this .then (null , onRejected); } }
如果不追求别的特性,我们的 Promise 对象就已经封装好了,并且可以使用了。
1 2 3 4 5 6 7 8 9 10 11 12 const p = new MyPromise ((resolve, reject ) => { setTimeout (() => { reject ('some err' ); }, 1000 ); }); p.catch ((err ) => [console .log ('err' , err)]);
再模拟将 then_cb 放入队列中执行,简单调整如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class MyPromise { constructor (executor ) { this .thenCallback = null ; this .rejectCallback = null ; executor (this ._resolve .bind (this ), this ._reject .bind (this )); } _resolve (value ) { setTimeout (() => { this .thenCallback (value); }, 0 ); } _reject (value ) { setTimeout (() => { this .rejectCallback (value); }, 0 ); } then (then_cb, onRejected ) { this .thenCallback = then_cb; this .rejectCallback = onRejected; } catch (onRejected) { this .then (null , onRejected); } }
加入队列机制后,就可以在 executor 中直接执行 resolve ,否则会报错 this.thenCallback is not a function
1 2 3 4 5 6 7 const p = new MyPromise ((resolve, reject ) => { resolve ('123' ); }); p.then ((res ) => { console .log (res); });
Promise.all Promise.all 返回的是一个数组,我们要将参数 array 内的每个 Promise 的执行结果放在一个数组 result 里,并且 result 数组成员的顺序要与传入时的 array 成员顺序保持一一对应。
实现 Promise.all 的重点在于对 all 参数内 Promise 实例全部执行完毕时机的判断。
因为 Promise 是异步的,我们不能保证 Promise 实例完成的时机与数组顺序一样。也就是说我们不能使用数组的 length 属性来表达 Promise 全都执行完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const delay = (data ) => { return new Promise ((resolve, reject ) => { setTimeout (() => { resolve (data); }, Math .random () * 1000 ); }); }; Promise ._all = (array ) => { return new Promise ((resolve, reject ) => { let count = 0 ; const result = []; for (let i = 0 , len = array.length ; i < len; i++) { array[i].then ((data ) => { result[i] = data; count++; if (result.length === array.length ) { resolve (result); } }, reject); } }); }; const p1 = delay (1 );const p2 = delay (2 );const p3 = delay (3 );Promise ._all ([delay (2 ), delay (1 ), delay (3 )]).then ( (res ) => { console .log ('res: ' , res); }, (err ) => { console .log ('err: ' , err); }, );
上面的代码,如果最后一个 Promise 先执行完毕,赋值时 result[2] = data
,那么 result.length
等于 3, 满足 result.length === array.length
的判断条件,就会提前执行 resolve 。
所以,我们可以参考垃圾回收机制的引用计数法,在内部添加一个计数变量 count 来判断是否所有 Promise 都已执行完毕。当 count 等于传入的 Promise 数组长度时,表示所有 Promise 都有了结果,然后我们再执行 resolve 将结果传递出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Promise ._all = (array ) => { return new Promise ((resolve, reject ) => { let count = 0 ; const result = []; for (let i = 0 , len = array.length ; i < len; i++) { array[i].then ((data ) => { result[i] = data; count++; if (count === array.length ) { resolve (result); } }, reject); } }); };
async/await 异步问题除了使用 Promise 来解决之外,还可以使用 ES7 中新增的语法 async/await 来搞定。
在函数声明前面,加上关键字 async
,这就是 async 的具体使用了。
1 2 3 4 5 6 7 8 async function fn ( ) { return 30 ; } const fn = async ( ) => { return 30 ; };
然后我们查看一下 fn 的运行结果
1 2 3 4 5 6 7 8 console .log (fn ());Promise = { __proto__ : Promise , [[PromiseStatus ]]: 'resolved' , [[PromiseValue ]]: 30 , };
发现 fn 函数运行返回的是一个标准的 Promise 对象。也就是说 async 其实就是 Promise 的一个语法糖,目的是为了让写法更加简单。于是,我们可以使用 Promise 的相关语法来处理后续的逻辑
1 2 3 fn ().then ((res ) => { console .log (res); });
await 含义为等待,意思就是需要等待 await 后面函数运行完了,并且有了返回结果,才能继续执行下面的代码。这正是同步的效果。
需要注意的是,await 关键字只能在 async 函数中使用。并且 await 后面的函数运行后必须返回一个 Promise 对象才能实现同步的效果。
当我们使用一个变量去接收 await 的返回值时,该返回值为 Promise 中 resolve 传递出来的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function fn ( ) { return new Promise ((resolve, reject ) => { setTimeout (resolve, 1000 , 30 ); }); } const foo = async ( ) => { const t = await fn (); console .log ('t' , t); console .log ('next code' ); }; foo ();
从例子中我们可以看出,在 async 函数中,遇到 await 时,就会等待 await 后面的函数运行完毕,而不会直接执行 next code。
如果我们直接使用 then 方法,就不得不把后续的逻辑写在 then 方法中。
1 2 3 4 5 6 7 const foo = ( ) => { return fn ().then ((t ) => { console .log ('t: ' , t); console .log ('next doce' ); }); }; foo ();
很明显,如果使用 async/await 的话,代码会更简洁,逻辑也更清晰。
错误处理
在 Promise 中,我们使用 .catch 方法来捕获错误
而使用 async 时,我们使用 try/catch 来捕获错误
如果有多个 await ,只会捕获到第一个错误 :::info tyr/catch 除了可以捕获使用 async 时 Promise 抛出的错误,还可以捕获 throw 关键字抛出的错误。 :::
在 Promise 中,我们使用 .catch 方法来捕获错误 ,而不是使用 .then 的第二个参数。因为:
更接近同步的语法(try/catch)
then 中的错误也会被 catch 捕获
使用 then 的第二个参数,并不能捕获到第一个参数内部抛出的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const p3 = new Promise (function (resolve, reject ) { setTimeout (() => { resolve (123 ); }, 1000 ); }); p3.then ( (result ) => { console .log (result); throw new Error ('is err' ); }, (error ) => { console .log ('error' , error); }, );
使用 catch ,则可以捕获到上一个 then 内部抛出的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const p3 = new Promise (function (resolve, reject ) { setTimeout (() => { resolve (123 ); }, 1000 ); }); p3.then ((result ) => { console .log (result); throw new Error ('is err' ); }).catch ((error ) => { console .log ('error' , error); });
而使用 async 时,我们使用 try/catch 来捕获错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function fn ( ) { return new Promise ((resolve, reject ) => { setTimeout (() => { reject ('some error' ); }, 1000 ); }); } const foo = async ( ) => { try { await fn (); } catch (e) { console .log ('e: ' , e); } }; foo ();
如果有多个 await ,只会捕获到第一个错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function fn1 ( ) { return new Promise ((resolve, reject ) => { setTimeout (() => { reject ('some error fn1' ); }, 1000 ); }); } function fn2 ( ) { return new Promise ((resolve, reject ) => { setTimeout (() => { reject ('some error fn2' ); }, 1000 ); }); } const foo = async ( ) => { try { await fn1 (); await fn2 (); } catch (e) { console .log ('e: ' , e); } }; foo ();
Generator 函数 Generator 函数可以认为是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
1 2 3 4 5 6 7 8 function * gen (x ) { var y = yield x + 2 ; return y; } var g = gen (1 );g.next (); g.next ();
Generator 函数名前要加*号,调用 Generator 函数,会返回一个指针对象(即遍历器)。调用指针对象的 next 方法,会移动内布指针,指向第一个遇到的 yield 语句。
也就是说,next 的作用是分段执行 Generator 函数。每次调用 next 方法,都会返回一个对象,表示当前阶段的信息。
value 属性是 yield 语句后面表达式的值,done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
错误处理 使用 try catch 捕获函数体外抛出的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function * gen (x ){ try { var y = yield x + 2 ; } catch (e){ console .log (e); } return y; } var g = gen (1 );g.next (); g.throw ('出错了' );
async 从 Promise 对象,再到 Generator,异步解决方案每次都有所改进,很多人认为 async 函数是异步操作的终极解决方案。async 函数就是 Generator 函数的语法糖。 async 函数就是将 Generator 函数的*号替换成 async,将 yield 替换成 await。
1 2 3 4 5 6 7 8 9 var gen = function * () { var f1 = yield readFile ('/etc/fstab' ); var f2 = yield readFile ('/etc/shells' ); }; var asyncReadFile = async function ( ) { var f1 = await readFile ('/etc/fstab' ); var f2 = await readFile ('/etc/shells' ); };
async 函数对 Generator 函数的改进,体现在一下三点:
内置执行器。Generator 函数的执行必须依靠执行器。
更好的语义。async 和 await,比起星号和 yield,语义更清楚。
更广的适用性。await 命令后面可以跟 Promise 对象和原始类型的值(数值、字符串、布尔值),Generator 函数只能是 thunk 函数或 Promise 对象。