异步编程中篇

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来拦截请求)。

<body>

<script src="./jquery-1.12.4.min.js"></script>

<script src="./mock-min.js"></script>

<script>

//拦截Ajax请求

Mock.mock(/.json/, {

'stuents|5-10': [{

'id|+1': 1,

'name': '@cname',

'gende r': /[男女]/, //在正则表达式匹配的范围内随机

'age|15-30': 1, //年龄在15-30之间生成,值1只是用来确定数据类型 'phone': /1d{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);

});

</script>

</body>

在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

原文地址:https://www.cnblogs.com/jrzqdlgdx/p/11350735.html