return timeout(2000);
}).then(function(){
console.log("fourth");
return timeout(2000);
});
由于需要多次创建Promise对象,所以用了 timeout函数将它封装起来,每次调用它都会返回一个 新的Promise对象。当then方法调用后,其内部的回调函数默认会将当前的Promise对象返回。当 然也可以手动返回一个新的Promise对象。我们这里就手动返回了一个新的计时对象,因为需要 重新开始计时。后面继续用the n方法来触发异步完成的回调函数。这样就可以做到同步的效果, 从而避免了过多的回调嵌套带来的"回调地狱"问题。
实际上Promise的应用还是比较多,比如前面讲到的fetch,它就利用了 Promise来实现AJAX的异 步操作:
let pm = fetch("/users"); // 获取Promise对象
pm.then((response) => response.text()).then(text => {
test.innerText = text; //将获取到的文本写入到页面上
})
.catch(e rror => console.log("出错了 "));
注意:response.text0返回的不是文本,而是Promise对象。所以后面又跟了一个then,然 后从新的Promise对象中获取文本内容。
Promise作为ES6提供的一种新的异步编程解决方案,但是它也有问题。比如,代码并没有因为 新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。因此它并不是异步实现的最终 形态,后续我们还会继续介绍其他的异步实现方法。
16-3迭代器与生成器
上一节中我们学习了如何使用Promise来实现异步操作。但是它也会存在一些问题,比如代码量 增多,不易理解。那么这一节咱们将一起来探索其他解决异步的方法。生成器作为ES6新增加的 语法,它也能够处理异步的操作。不过再讲生成器之前,咱们还得理解另外一个东西:迭代器。
16-3-1 迭代器(Iterator)
迭代器是一种接口,也可以说是一种规范。它提供了一种统一的遍历数据的方法。我们都知道数 组、集合、对象都有自己的循环遍历方法。比如数组的循环:
let ary = [1,2,3,4,5,6,7,8,9,10];
//for循环
for(let i = 0;i < ary.length;i++){
console.log(ary[i]);
}
//forEach 循环
ary.forEach(function(ele){
console.log(ele);
});
//for-in循环
for(let i in ary){
console.log(ary[i]);
}
//for-of 循环
for(let ele of ary){
console.log(ele);
}
集合的循环:
let list = new Set([1,2,3,4,5,6,7,8,9,10]);
for(let ele of list){
console.log(ele);
}
对象的循环:
let obj = {
name : 'tom',
age : 25,
gender :'男',
intro : function(){
console.log('my name is '+this.name);
}
}
for(let attr in obj){
console.log(attr);
}
从以上的代码可以看到,数组可以用for、forEach、for-in以及for-of来遍历。集合能用for-of。对 象能用for-in。也就是说,以上数据类型的遍历方式都各有不同,那么有没有统一的方式遍历这 些数据呢?这就是迭代器存在的意义。它可以提供统一的遍历数据的方式,只要在想要遍历的数 据结构中添加一个支持迭代器的属性即可。这个属性写法是这样的:
const obj = {
[Symbol.iterator]:function(){}
}
[Symbol.ite rato r]属性名是固定的写法,只要是拥有该属性的对象,就能够用迭代器的方式 进行遍历。
迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前。接着通过 调用n ext方法,改变指针的指向,让其指向下一条数据。每一次的n ext都会返回一个对象,该对 象有两个属性。其中value代表想要获取的数据,done是个布尔值,false表示当前指针指向的数 据有值。true表示遍历已经结束。
let ary = [1,2,3];
let it = ary[Symbol.iterator](); // 获取数组中的迭代器
数组是支持迭代器遍历的,所以可以直接获取其中的迭代器。集合也是一样。
let list = new Set([1,2,3]);
let it = list.entries(); //获取set集合中的迭代器
console.log(it.next()); // { value: console.log(it.next()); // { value: console.log(it.next()); // { value: console.log(it.next()); // { value:
set集合中每次遍历出来的值是一个数组,里面的第一和第二个元素都是一样的。
由于数组和集合都支持迭代器,所以它们都可以用同一种方式来遍历。es6中提供了一种新的循 环方法叫做f or-of。它实际上就是使用迭代器来进行遍历,换句话说只有支持了迭代器的数据结 构才能使用for-o f循环。在JS中,默认支持迭代器的结构有:
- Array
- Map
- Set
- String
- TypedArray
•函数的arguments对象
- NodeList 对象
这里面并没有包含自定义的对象,所以当我们创建一个自定义对象后,是无法通过for-of来循环 遍历它。除非将iterator接口加入到该对象中:
let obj = { name: 'xiejie', age: 18,
gende r:'男', intro: function () { console.log('my name is ' + this.name);
},
[Symbol.iterator]: function () {
let i = 0;
let keys = Object.keys(this); //获取当前对象的所有属性并形成一个数组 return {
next: function () {
return {
value: keys[i++], //外部每次执行next都能得到数组中的第i个元素 done: i > keys.length //如果数组的数据已经遍历完则返回true }
}
}
}
}
for ( let attr of obj) { console.log(attr);
// name
// age
// gender
// intro
}
let it = obj[Symbol.iterator]();
通过自定义迭代器就能让自定义对象使用for-of循环。迭代器的概念及使用方法我们清楚了, 接下来就是生成器。
16-3-2 生成器(Generator)(扩展)
生成器也是ES6新增加的一种特性。它的写法和函数非常相似,只是在声明时多了一个*号。
function* say(){}
const say = function*(){}
|注意:这个*只能写在function关键字的后面。
生成器函数和普通函数并不只是一个*号的区别。普通函数在调用后,必然开始执行该函数,直 到函数执行完或遇到return为止。中途是不可能暂停的。但是生成器函数则不一样,它可以通过 yield关键字将函数的执行挂起,或者理解成暂停。它的外部在通过调用n ext方法,让函数继续执 行,直到遇到下一个yield,或函数执行完毕。
function* say(){ yield "开始";
yield "执行中"; yield "结束";
}
let it = say(); //调用say方法,得到一个迭代器
调用say函数,这句和普通函数的调用没什么区别。但是此时say函数并没有执行,而是返回了一 个该生成器的迭代器对象。接下来就和之前一样,执行next方法,say函数执行,当遇到yield
时,函数被挂起,并返回一个对象。对象中包含value属性,它的值是yield后面跟着的数据。并 且done的值为false。再次执行next,函数又被激活,并继续往下执行,直到遇到下一个yield。
当所有的yield都执行完了,再次调用next时得到的value就是undefined, done的值为true。
如果你能理解刚才讲的迭代器,那么此时的生成器也就很好理解了。它的y ield,其实就是n ext方 法执行后挂起的地方,并得到你返回的数据。那么这个生成器有什么用呢?它的y ield关键字可以 将执行的代码挂起,外部通过next方法让它继续运行。
这和异步操作的原理非常类似,把一个操作分为两部分,先执行一部分,然后再执行另外一部 分。所以生成器可以处理和异步相关的操作。我们知道,异步操作主要是依靠回调函数实现。但 是纯回调函数的方式去处理同步效果会带来“回调地域“的问题。Promise可以解决这个问题。但 是Promise写起来代码比较复杂,不易理解。而生成器又提供了一种解决方案。看下面这个例 子:
function* delay() {
yield new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 2000) })
console.log("go on");
}
let it = delay(); //得到一个迭代器
// it.next()会执行到第一个 yield 得到的值为{ value: Promise { vpending> }, done: false }
// it.next().value 将会得到_个 Promise
// Promise会在2秒以后调用then方法
// 2秒后调用then方法执行迭代器的下一步
it.next().value.then(() => {
it.next();
});
这个例子实现了等待2秒钟后,打印字符串"go on"。下面我们来分析下这段代码。在delay这个 生成器中,yield后面跟了一个Promise对象。这样,当外部调用next时就能得到这个Promise对 象。然后调用它的then函数,等待2秒钟后Promise中会调用resolve方法,接着the n中的回调函 数被调用。也就是说,此时指定的等待时间已到。然后在the n的回调函数中继续调用生成器的 next方法,那么生成器中的代码就会继续往下执行,最后输出字符串"go on"。
例子中时间函数外面为什么要包裹一个Promise对象呢?这是因为时间函数本身就是一个异步方 法,给它包裹一个Promise对象后,外部就可以通过the n方法来处理异步操作完成后的动作。这 样,在生成器中,就可以像写同步代码一样来实现异步操作。比如,利用fetch来获取远程服务器 的数据(为了测试方便,我将用M ockJS来拦截请求)。
//拦截Ajax请求
Mock.mock(/\.json/, {
'stuents|5-10': [{
'id|+1': 1,
'name': '@cname',
'gende r': /[男女]/, //在正则表达式匹配的范围内随机
'age|15-30': 1, //年龄在15-30之间生成,值1只是用来确定数据类型 'phone': /1\d{10}/,
'add r': '@county(t rue)', //随机生成中国的一个省、市、县数据
'date': "@date('yyyy-MM-dd')"
}]
});
function* getUsers() {
let data = yield new Promise((resolve, reject) => {
$.ajax({
type: "get",
url: "/users.json",
success: function (data) {
resolve(data)
}
});
});
console.log("得到的 data 为:",data);
}
let it = getUse rs(); // 返回一个迭代器
// it.next().value会得到一个Promise, Promise里面向服务器发送请求获取数据
//数据获取成功以后调用then方法,并将获取到的数据传递给then方法
// then方法里面再次开启迭代器,执行第二句代码,并将数据传递过去 //在getUse rs函数里面data变量接收了传递过来的数据,并打印出来 it.next().value.then((data) => {
it.next(data);
});
在Promise中调用JQuery的AJAX方法,当数据返回后调用resolve,触发外部then方法的回调函 数,将数据返回给外部。外部的the n方法接收到data数据后,再次调用n ext,移动生成器的指 针,并将data数据传递给生成器。所以,在生成器中你可以看到,我声明了一个data变量来接收 异步操作返回的数据,这里的代码就像同步操作一样,但实际上它是个异步操作。当异步的数据 返回后,才会执行后面的打印操作。这里的关键代码就是y ield后面一定是一个Promise对象,因 为只有这样外部才能调用the n方法来等待异步处理的结果,然后再继续做接下来的操作。
之前我们还讲过一个替代AJAX的方法fetch,它本身就是用Promise的方法来实现异步,所以代 码写起来会更简单:
function* getUsers(){
let response = yield fetch("/users");
let data = yield response.json();
console.log("data",data);
}
let it = getUsers();
it.next().value.then((response) => {
it.next(response).value.then((data) => {
it.next(data);
});
});
|由于mock无法拦截fetch请求,所以我用nodejs+express搭建了一个mock-server服务器。
这里的生成器我用了两次yield,这是因为f etch是一个异步操作,获得了响应信息后再次调用json 方法来得到其中返回的JSO N数据。这个方法也是个异步操作。
从以上几个例子可以看出,如果单看生成器的代码,异步操作可以完全做的像同步代码一样,比 起之前的回调和Promise都要简单许多。但是,生成器的外部还是需要做很多事情,比如需要频 繁调用n ext,如果要做同步效果依然需要嵌套回调函数,代码依然很复杂。市面也有很多的插件 可以辅助我们来执行生成器,比如比较常见的co模块。它的使用很简单:
co(getUsers);
引入co模块后,将生成器传入它的方法中,这样它就能自动执行生成器了。关于co模块这里我就
不再多讲,有兴趣的话可以参考这篇文章:http://es6.ruanyifeng.eom/#docs/generator-async