同步与异步
同步是指发起一个请求时,如果未得到请求结果,代码逻辑将会等待,直到结果出来才会继续执行之后的代码。
异步是指当发起一个请求时,不会等待请求结果,直接继续执行后面的代码。请求结果的处理逻辑,会添加一个监听,等到反馈结果出来后,在回调函数中处理对应的逻辑。
使用 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 对象。