前言
同步和异步的最大区别在于:
- 同步会阻塞,上面的代码执行完成后下面的代码才会执行
- 异步不会阻塞,上面的代码执行未完成下面的代码就已经开始执行了 我们知道 JS 是单线程语言,所以如果存在运行时间比较长的任务(如:定时器、DOM事件、异步请求等),如果把这些任务视为同步任务,那将会造成很严重的“阻塞现象”,所以浏览器把这些任务归为异步任务,JS 引擎线程遇到这些异步任务时,会通知其他线程(如:定时器线程、事件处理线程、异步请求线程等)去做,最后只需要把最终结果反馈 JS 引擎线程就行
好,那这个通知 和 反馈 怎么做呢?
于是就有了JavaScript的异步编程方式:Promise、Generator、async / await
我们来看看这些方式是怎么完成这项工作的:
Promise
Promise 的基本思想
回调地狱:
利用 Promise 解决回调地狱:
Promise 是什么?
在控制台打印出 Promise ,结果可以看到是 [native code] ,说明是官方封装的函数,代码是本地的
在控制台输出 Promise.prototype ,结果可以看到有 then 、catch 、finally 等方法
综上,我们把 Promise 当做是构造函数,使用 new 关键字创建 Promise 的一个最简单的实例对象 p:
var p = new Promise((resolve,reject)=>{
if(1) resolve("异步任务完成")
else reject("异步任务失败")
})
在控制台输出实例对象 p
我们在实例对象 p 的原型
__proto__属性里面找到了 Promise.prototype,所以我们才能让实例对象 p 使用 then / catch / finally 等方法
Promise 的通知与反馈过程
通知
var p = new Promise((resolve,reject)=>{
if(1) resolve("异步任务完成")
else reject("异步任务失败")
})
以上就是 Promise 的通知过程,Promise 里面是一个箭头函数,传入参数是 resolve 和 reject,{ }里面运行的代码是 resolve() 和 reject() 两个调用其他函数的操作。官方规定:写在前面的参数表示“任务完成”,写在第二个参数表示“任务失败”。所以由此可见:这两个参数的名称是可以自定的,只不过我们为了更加语义化才采用 resolve 和 reject。
状态
- 我们在控制台输出实例对象 p,显示的是一个 Promise 对象,对象的属性里发现除了原型还有
[[PromiseState]]和[[PromiseResult]]两个属性,顾名思义分别表示状态和结果,其中的状态的值是fulfilled,而结果的值就是我们刚刚 Promise 里面箭头函数里写的 resolve() 括号的实参"异步任务完成"
2. 我们修改一下 Promise 函数,控制其任务失败:
var p = new Promise((resolve,reject)=>{
if(0) resolve("异步任务完成")
else reject("异步任务失败")
})
3. 其中
[[PromiseResult]]就是我们刚刚 Promise 里面箭头函数里写的 reject() 括号的实参"异步任务失败",而[[PromiseState]]变成了rejected。
这说明了:我们的通知过程中 Promise 所做的背后工作(原理)就是:执行 Promise 里面的箭头函数时会把该实例对象 p (一个Promise对象)里的
[[PromiseState]]属性的值进行改变:
- 若是执行第一个参数(resolve)对应的函数(resolve())的话,就把这个属性的值改为
fulfilled;(源码就是这个道理)- 若是执行第二个参数(reject)对应的函数(reject())的话,就把这个属性的值改为
rejected
- 那这个属性原来的值是什么,我们用 setTimeout() 作一下延时,才能看得到:
- 可以看到这个属性原来的值是
pending。
反馈
- 我们对实例对象 p 使用其继承过来的 then 方法:(针对的是任务完成的例子)
可以看到调用 p.then() 后还是返回一个 Promise 对象,并且这个对象的
状态变为fulfilled,结果变为undefined(因为我没有调用其他函数并传参)
- 我们针对任务失败的例子使用 then() 方法:
- 可以发现状态还是 rejected,也没有打印 res,说明 p.then() 并没有执行,我们在对这个返回的 Promise 对象使用 catch() 方法:
9. 可以看到调用 p.then().catch() 后还是返回一个 Promise 对象,并且这个对象的
状态变为fulfilled,结果变为undefined,还把 err 给打印出来了
- 这就是实例对象 p 可以采用链式调用的本质原因,也证明了为什么任务失败只会执行catch()方法,而任务完成执行了then()方法后还会继续执行链上下一个then()方法,一直走到catch()不执行退出的原因
- then() 方法的入口钥匙是
[[PromiseState]]属性的值是fulfilled;- catch() 方法的入口钥匙是
[[PromiseState]]属性的值是rejected。(源码就是这个道理)
(任务完成的 p)
(任务失败的 p)
总结
- 使用 Promise 解决异步问题的步骤:
- 利用构造函数 Promise 创建实例对象;
- 在 构造函数里面写个箭头函数,传入两个参数,一个代表任务完成,一个代表任务失败;
- 在箭头函数里执行异步任务,执行完成后调用以这两个为名称的函数,可把执行结果做实参传过去;
- Promise()里的箭头函数的异步任务执行完毕后会返回一个 Promise 对象,这个对象已经赋值给了实例对象 p;p 可以使用从 Promise.prototype 继承过来的 then() / catch() 等方法
- p 执行这些方法前会看
[[PromiseState]]这个属性的值看是否进入当前方法,进去之后出来[[PromiseState]]和[[PromiseResult]]两个属性的值都可能发生改变; - 每次执行完后都还是返回一个 Promise 对象
明白了这些本质之后,对于 Promise 的源码的基本框架就大概知道是什么了(虽然现在还可能很难看懂hhh),以及后面的各种用法、面对各种场景也都比较有把握了。
上面说了 Promise 的本质性的东西,接下来我们研究一下 Promise 的源码,是如何实现“通知” 和 “反馈” 的,想直接跳过的同学 点击这里
Promise((resolve,reject)=>{}) 的实现原理
- 核心:
- 返回 Promise 对象
- 怎么实现:任务成功,状态置为"fulfilled",任务失败,状态置为"rejected",并把 value 值返回
- 怎么实现:异步任务完成后不会里面反馈结果,而是放置一个微任务队列,等调用 then() 方法才会执行
来自 神三元同学的做法:
class MyPromise {
//传一个异步函数进来
constructor(excutorCallBack){
this.status = 'pending';
this.value = undefined;
this.fulfillAry = [];
this.rejectedAry = [];
//=>执行Excutor
let resolveFn = result => {
if(this.status !== 'pending') return;
let timer = setTimeout(() => {
this.status = 'fulfilled';
this.value = result;
this.fulfillAry.forEach(item => item(this.value));
}, 0);
};
let rejectFn = reason => {
if(this.status !== 'pending')return;
let timer = setTimeout(() => {
this.status = 'rejected';
this.value = reason;
this.rejectedAry.forEach(item => item(this.value))
}, 0)
};
try{
//执行这个异步函数
excutorCallBack(resolveFn, rejectFn);
} catch(err) {
//=>有异常信息按照rejected状态处理
rejectFn(err);
}
}
then(fulfilledCallBack, rejectedCallBack) {
//resolve和reject函数其实一个作为微任务,因此他们不是立即执行,而是等then调用完成后执行
this.fulfillAry.push(fulfilledCallBack);
this.rejectedAry.push(rejectedCallBack);
//一顿push过后他们被执行
}
}
Promise.then().catch() 的实现原理
- 核心:
- 怎么实现:任务成功的结果会被then方法捕捉到,错误只被catch捕捉到
- 怎么实现:链式调用 把上面的 MyPromise 类里的 then() 方法进行修改:
//then传进两个函数
then(fulfilledCallBack, rejectedCallBack) {
//保证两者为函数
typeof fulfilledCallBack !== 'function' ? fulfilledCallBack = result => result:null;
typeof rejectedCallBack !== 'function' ? rejectedCallBack = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null
//返回新的Promise对象,后面称它为“新Promise”
return new Promise((resolve, reject) => {
//注意这个this指向目前的Promise对象,而不是新的Promise
//再强调一遍,很重要:
//目前的Promise(不是这里return的新Promise)的resolve和reject函数其实一个作为微任务
//因此他们不是立即执行,而是等then调用完成后执行
this.fulfillAry.push(() => {
try {
//把then里面的方法拿过来执行
//执行的目的已经达到
let x = fulfilledCallBack(this.value);
//下面执行之后的下一步,也就是记录执行的状态,决定新Promise如何表现
//如果返回值x是一个Promise对象,就执行then操作
//如果不是Promise,直接调用新Promise的resolve函数,
//新Promise的fulfilAry现在为空,在新Promise的then操作后.新Promise的resolve执行
x instanceof Promise ? x.then(resolve, reject):resolve(x);
}catch(err){
reject(err)
}
});
//以下同理
this.rejectedAry.push(() => {
try {
let x = rejectedCallBack(this.value);
x instanceof Promise ? x.then(resolve, reject):resolve(x);
}catch(err){
reject(err)
}
})
}) ;
}
有了then方法,catch自然而然调用即可:
catch(rejectedCallBack) {
return this.then(null, rejectedCallBack);
}
测试如下:
var p = new MyPromise((resolve, reject) => {
setTimeout(() => {
let num = Math.random()
console.log(num)
num < 0.5 ? resolve(100) : reject(-100);
}, 1000)
})
console.log(p)
p.then(res=>{
console.log(p)
console.log(res)
}).catch(err=>{
console.log(p)
console.log(err)
})
(异步任务成功)
(异步任务失败)
Promise().then().catch() 处理多个串行异步任务
比如有个需求 —— 想要获取一个地级市拥有多少个辖区:
- 我们现在得先调用【获取当地省份】的接口
- 拿到省份id后才能够调用【获取地级市】的接口
- 拿到对应地级市的id后才能调用【获取地级市拥有多少个辖区】的接口
其伪代码如下:
ajax('url_1',data1);
ajax('url_2',data2); // 执行之前需要拿到 ajax('url_1') 的结果
ajax('url_3',data3); // 执行之前需要拿到 ajax('url_2') 的结果
按照之前回调函数的写法就是:
ajax('url_1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
先不说它的可读性差,如果中间某一环节出现错误,修改起来很麻烦。所以我们用 Promise 来解决这个问题,好处就是 Promise 实现了将多个异步任务串行执行写成同步任务执行顺序
let promise = fn('url_1',data1)
promise.then(data2 => { // 注意这里 promise 不要加括号,它是执行 new Promise(...) 后返回的一个对象
fn('url_2',data2)
}).then(data3 => {
fn('url_3',data3)
}).then(res =>{
console.log(res) // 串行完毕可以得到你想要的结果了
}).catch(err => {
console.log(err)
})
function fn(url,data){
return new Promise((resovle,reject)=>{
ajax(url,data).success(function(res){
resolve(res)
})
})
}
这样代码可读性就好很多了,清晰明了。(其实后面还有更加简洁的方法)
说完串行了,那么并行怎么办? 当有多个异步事件,之间并无联系而且没有先后顺序,只需要全部完成就可以开始工作了。
串行会把每一个异步事件的等待时间进行一个相加,明显会对完成进行一个阻塞。那么并行的话该怎么确定全部完成呢?
Promise.all 与 Promise.race 解决并行问题
Promise.all([promise1,promise2,promise3,...])
- 接收一个数组,数组的每一项都是一个 promise 对象
- 当数组中所有的 promise 的状态都达到 resolved 的时候,Promise.all的状态就会变成 resolved
- 如果其中有一个 promise 的状态变成了 rejected,那么 Promise.all 的状态就会变成 rejected
- 调用then方法时的结果成功的时候是回调函数的参数也是一个数组,按顺序保存着每一个promise对象resolve执行时的值
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},4000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},3000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},5000)
});
let promiseAll = Promise.all([promise1,promise2,promise3])
promiseAll.then(res=>{
console.log(res); // [1,2,3]
}).catch(err => {
console.log(err);
})
控制台 5s 后输出结果:(执行时间最长的 promise 完成才算完成)
顺序是[1,2,3],证明与哪个 promise 的状态先变成 resolved 无关
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(1);
},4000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},3000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},5000)
});
let promiseAll = Promise.all([promise1,promise2,promise3]);
promiseAll.then(res => {
console.log(res);
}).catch(err => {
console.log("任务" + err + "失败了") // 1 说明是 promise1 里的异步任务执行失败了
})
控制台 4s 后输出结果:(其中一个 promise 一失败就退出)
Promise.all() 的实现原理:
Promise.all = function (promises) {
let index = 0
let result = [] // 存放异步任务执行成功的结果
if (promises.length === 0) resolve(result)
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
processValue(i, data) // 拿取任务完成的结果
}, (err) => {
reject(err) // 一旦发现错误,立马抛出
return
})
}
function processValue(i, data) {
result[i] = data // 把任务完成的结果存入数组中
if (++index === promise.length) {
// 每成功一次计数器就会加1,直到所有任务成功时会与values长度一致,则认定为都成功了
resolve(result)
}
}
})
}
总结:
- 对数组中的每一个 promise 对象调用 then()方法和 catch()方法进行结果检验,一旦发现错误就抛出
- 如果当前遍历的 promise 对象的结果是成功的,则放入一个 results 数组
- 当 results 数组的长度和原来的 promises 数组长度一致时返回 results 数组
Promise.race([promise1,promise2,promise3,...]) 竞速模式
- 接受一个每一项都是promise的数组。
- 但是与all不同的是,第一个promise对象状态变成resolved时自身的状态变成了resolved,第一个promise变成rejected自身状态就会变成rejected。第一个变成resolved的promsie的值就会被使用
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},4000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},3000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},5000)
});
let promiseRace = Promise.race([promise1,promise2,promise3])
promiseRace.then(res => {
console.log(res); // 打印出2 因为 promise2 最先完成,其余的就忽略了
}).catch(err => {
console.log("任务" + err + "失败了");
})
但是在控制台输出 promise1 和 promise3 都显示“任务完成”的状态,也就是说“竞赛”输了的选手也还是会完成这场比赛
Promise.race() 的实现原理
Promise.race = function (promises) {
let index = 0
let result = [] // 存放异步任务执行成功的结果
if (promises.length === 0) resolve(result)
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data) // 其中一个任务一有任务完成的结果,立马抛出
}, (err) => {
reject(err) // 一旦发现错误,立马抛出
return
})
}
})
}
总结:
- 对数组中的每一个 promise 对象调用 then()方法和 catch()方法进行结果检验
- 一旦发现其中一个 promise 对象是任务完成的状态,立马抛出
- 一旦发现其中一个 promise 对象是任务失败的状态,立马抛出
Promise.resolve() 与 Promise.reject()
Promise.resolve() 用法
Promise.resolve()的作用是将现有对象转换成状态为 fulfilled 的 Promise 对象
下面分四种不同参数的情况讨论其使用效果:
1. Promise.resolve(Promsie实例)
参数是一个 Promise 实例,这种情况Promise.resolve()什么都不做
2. Promise.resolve(thenable对象)
参数是具有then方法的对象(thenable对象),Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。例如:
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42
3. Promise.resolve(普通对象或原始值)
参数不是具有then方法的对象,或根本就不是对象,Promise.resolve返回一个状态为resolved的对象
const p = Promise.resolve('Hello');
// 因为p的状态为resolved所以.then()会立即执行
p.then(function (s){
console.log(s)
});
// Hello
4. Promise.resolve() 不带任何参数
这种情况直接返回状态为resolved的Promise对象。如果希望得到一个 Promise 对象,比较方便的方法就是直接调用。 立即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.resolve() 实现原理
核心在于:将现有对象转换成状态为 fulfilled 的 Promise 对象,如果已经是 Promise 对象则直接返回
Promise.resolve = function (value) {
if (value instanceof Promise) return value
return new Promise(resolve => resolve(value))
}
Promise.reject() 用法
Promise.reject() 的作用是将现有对象转换成状态为 rejected 的 Promise 对象,参数情况也是四种,和上面的 Promise.resolve() 相似
Promise.reject() 实现原理
核心在于:将现有对象转换成状态为 rejected 的 Promise 对象
Promise.reject = function (value) {
return new Promise((resolve, reject) => reject(value))
}
Generator
Generator 是什么
Generator(生成器)是ES6标准引入的新的数据类型。一个 generator 看上去像一个函数,但可以返回多次
function* fn(max) {
yield ...;
return;
}
generator 生成器长得像函数,与函数不同的是用function*定义的
yield 关键字
关键字 yield,有点像 return,yield 和 return 的区别在于:
- return 只在函数调用之后返回值,return 语句之后不允许你执行任何其他操作
- yield 相当于暂停函数执行而返回一次值,下次调用 next() 时,它将执行到下一个 yield 语句那里
return 返回的值
在控制台输出 return 的值(如果没有return就执行到函数结束)会得到一个 generator 对象,其属性
[[GeneratorState]]的值是suspended表示“暂停状态”
Generator.prototype.next() 方法
在控制台输出 generator 对象的原型会看到有 next、return、throw等方法,我们重点看一下 next 方法:
使用 g.next() 方法后会得到一个对象
{value: 1,done: false},此时输出 g,可观察到其状态还是suspended,当执行第三次 g.next() 后将会得到 {value: 1,done: false}对象,此时输出 g 的状态为 "closed"。告知外界该 generator 对象已经执行完毕
返回的 value 就是 yield 的返回值,done 表示这个 generator 是否已经执行结束了。如果done为true,则value 就是 return的返回值。当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。
Generator.prototype.next() 方法 传参
next() 方法也可以传参
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
第一次 g.next() 输出 { value: 3, done: false } 的原因是 gen(1) 传入参数 1,然后 yield 返回 1 + 2,所以得到 value 值为 3,而第二次 g.next() 传入参数 2,相当于把整个 yield x + 2 换成 2,所以 return 返回 y 的值为 2,执行结束,done 值为 true
Generator.prototype.return() 方法
return()方法,可以返回给定的值,并且终止遍历 Generator 函数
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
Generator.prototype.throw() 方法
Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
相当于把整个 yield x + 2语句换成 throw("出错了"),然后 try...catch...语句捕获到了,于是输出 e
Generator 的使用
- 输出斐波那契数列的每一项 以一个著名的斐波那契数列为例子引入:
0 1 1 2 3 5 8 13 21 34 ...
要编写一个产生斐波那契数列的函数,可以这么写:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
[a, b] = [b, a + b];
arr.push(b);
}
return arr;
}
// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
普通函数只能返回一次,所以必须返回一个Array。但有时我们需要每次拿其中一个返回值,比如做一个与用户交互的进度条啊之类的,但普通函数只能返回一次,所以不能完成这项需求,但是,如果换成 generator,就可以一次返回一个数,不断返回多次:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
直接调用试试:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接调用一个 generator 和调用函数不一样,fib(5)仅仅是创建了一个 generator 对象,这个对象可以想象成隐藏了结果的数组,需要我们一次次的去触发它,它才会按顺序地一次次的冒出数组元素
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
f.next(); // {value: undefined, done: true}
next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”【所以相比每次执行普通函数去获取斐波那契数列的第n项的性能要好很多】
或者直接用for ... of循环迭代generator对象,这种方式不需要我们每次去 next() 和判断 done:
for (var x of fib(5)) {
console.log(x); // 依次输出0, 1, 1, 2, 3
}
- 解决回调地狱:(还是上面那个 求一个地级市有多少个辖区 的例子)
ajax('url_1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
有了generator,用AJAX时可以这么写:
try {
r1 = yield ajax('url_1', data1);
r2 = yield ajax('url_2', data2); // 可以使用ajax('first')的结果,也就是 r1
r3 = yield ajax('url_3', data3); // 可以使用ajax('second')的结果,也就是 r2
success(r3);
}
catch (err) {
handle(err);
}
这里使用 try...catch... 语句,所以在 try{ } 区域内不用对结果 r1、r2、r3 进行错误处理,哪个地方出错了自然会被 catch 语句捕获到,并且输出错误参数 err
- 更高级的应用是在 Lazy-loading (未完成)
Generator
Generator 的实现原理
Generator 的实现原理:我关于它的实现原理的简单理解就是,有点像把函数的结果存到一个对象(数组)里面,然后返回,然后我们在对这个对象(数组)一个一个地遍历,得到每一次我们想想要的结果。只不过区别在于:使用 Generator 代替函数不会造成空间上的浪费,因为是在执行 Generator.next() 的时候才会去运行 Generator 函数
Yield
Generator 函数内部对代码的运行处理方式是:
- 第一次是从 函数起点 到 第一个yield 语句;
- 最后一次是从 最后一个 yield 语句 到 函数终点 或 return;
- 其余的每一次都是从 yield 语句 执行到 下一个yield 语句 后停下。
所以这就好比把一个函数分割成很多很多子块函数,每一块子函数的起点都是 yield 语句,终点是下一个 yield 语句(第一块和最后一块除外),自然就能起到“暂停”的效果【从而实现“同步阻塞”的效果】,而且每一小块函数的返回值都会存在一个叫“迭代器”的容器里面
最终,Generator 函数执行结束后会返回一个迭代器
Generator.next() —— 迭代器模式
定义:迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示 简单理解就是:在不暴露对象的内部表示的情况下,能够遍历出所有元素 下面我们就来实现一个简单的迭代器:
// 在数据获取的时候没有选择深拷贝内容,
// 对于引用类型进行处理会有问题
// 这里只是演示简化了一点
function Iterdtor(arr){
let data = [];
if(!Array.isArray(arr)){
data = [arr];
}else{
data = arr;
}
let length = data.length;
let index = 0;
// 迭代器的核心next
// 当调用next的时候会开始输出内部对象的下一项
this.next = function(){
let result = {};
result.value = data[index];
result.done = (index === length - 1 ? true : false;)
if(index !== length){
index++;
return result;
}
// 当内容已经没有了的时候返回一个字符串提示
return 'data is all done'
};
}
let arr = [1,2,3,4];
// 生成一个迭代器对象。
let iterdtor = new Iterdtor(arr);
控制台输出:
async 和 await
async 函数是 Generator 函数的语法糖。使用
async关键字代替 Generator 函数的星号*,await关键字代替yield
相较于Generator函数,async函数改进了以下四点:
- 内置执行器 Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。
- 更好的语义 async 和 await,比起 * 和 yield,语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- 更广的适用性 co 模块约定,yield 命令后面只能是
Thunk 函数或Promise 对象,而async 函数的await 命令后面,可以是Promise对象和原始类型的值。 - 返回值 async 函数的返回值是Promise对象,这比Generator 函数的返回值是 Iterator对象方便多了。你可以用 then 方法指定下一步的操作。
async 和 await 的使用
- 普通函数用 function 关键字开头
- async 函数就用 async function 关键字开头
- await 就是
等待后面那一项的完成,如果后面那一项没完成绝不会往下走
还是上面那个 求一个地级市有多少个辖区 的例子:
ajax('url_1',data1);
ajax('url_2',data2); // 执行之前需要拿到 ajax('url_1',data1) 的结果
ajax('url_3',data3); // 执行之前需要拿到 ajax('url_2',data2) 的结果
使用 async / await:
function fn(url,data) {
return new Promise((resolve, reject) => {
ajax(url, data, function (err, res) {
if (err) reject(err)
resolve(res)
})
})
}
async function asyncFn() {
let p1 = await fn('url_1',data1)
let p2 = await fn('url_2',data2)
let p3 = await fn('url_3',data3)
return p3
}
asyncFn().then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
注意事项
- 凡是在前面添加了
async 的函数在执行后都会自动返回一个 Promise 对象 await必须在 async 函数里使用,不能单独使用await 后面如果跟的是Promise 对象,则会等待该对象里面的函数执行完成;如果跟的不是 Promise 对象则会执行其后面的代码 / 等于后面的值
// await 后面是 Promise 对象
let promiseDemo = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('success')
console.log(3)
},0)
})
async function test() {
let result = await promiseDemo // 3
return result
}
// await 后面不是 Promise 对象
let promiseDemo = fn(() => {
console.log(3)
})
async function test() {
let result = await promiseDemo // 3
return result
}
async 和 await 的执行顺序
这道题跟 async / await 的应用有关,还和 JavaScript系列 -- event loop 事件轮询 有关
console.log(1) // 1
let promiseDemo = new Promise((resolve, reject) => {
console.log(2) // 1 2
setTimeout(() => {
let random = Math.random()
if (random >= 0) {
resolve('success')
console.log(3) // 1 2 4 6 3
} else {
reject('failed')
console.log(3)
}
}, 1000)
})
async function test() {
console.log(4) // 1 2 4
let result = await promiseDemo // 注意这里不用加括号,它是执行 new Promise(...) 后返回的一个对象
return result
}
test().then(result => {
console.log(5) // 1 2 4 6 3 5
}).catch((result) => {
console.log(5)
})
console.log(6) // 1 2 4 6
值得注意的是:
- 我们在最开始 new Promise 的时候就已经执行 Promise 里面的箭头函数的代码了
- 而 async 函数 test() 只有在 test().then().catch() 的时候才被调用,test() 函数里面的代码才开始被执行
解析:
- 1 2 4 6 是同步任务,由主线程来办,所以在前半部分输出
- 进入 async 函数 test() 里面后面执行到
await promiseDemo语句,就会等待定时器完成计时,输出 3 - 然后会返回一个 Promise 对象并赋值给变量 result,async 函数再把这个 Promise 对象返回到函数体外
- 函数体外对这个 Promise 对象使用 then() 方法和 catch() 方法,从而输出 5