异步编程那些事

回调函数

最原始的callback,优点是简单、容易理解,当然也有很严重的缺点是不利于代码的阅读和维护,多了之后的回调地狱。我们使用了这么多年,如今,想来大多数前端开发已经它抛弃了。

promise

从多年以前CommonJS工作组提出这个概念,到ES6最终官方实现了Promise对象,Promise终于由小妾转为正房,更是打败回调,成为“大夫人”。

Promise 是一个对象,从它可以获取异步操作的消息,如今许多异步的方式都是基于Promise(比如fetch和co)

Promise相对使用也比较简单,记住最重要的几点:

三种状态

Promise有三种状态:Pending(进行中)、Resolved(已完成)和Rejected(已失败)。最开始的状态为Pending,异步操作的结果返回后,会改变状态,从Pending到Resolved,或者从Pending到Rejected。
下面来自阮大师的ES6入门的一段代码:

var promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

可以看到,我们new一个新的Promise对象,两个参数resolve函数和reject函数,在里面我们进行异步操作,根据异步操作的结果,我们分别调用resolve和reject函数
resolve函数将Promise对象的状态从Pending变为Resolved,在异步操作成功时调用,并将异步操作返回的结果(value),作为参数;reject函数状态从Pending变为Rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

then和catch

现在我们有了一个异步操作,如果按照以前的操作我们需要一个回调函数来在异步操作之后执行,在Promise中,回调就对应着then方法。
用then方法可以分别指定Resolved状态和Reject状态的回调函数,根据promise的状态会进入不同的回调。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是代表成功的回调函数(也就是Resolved时调用,对应的promise的resolve),第二个回调函数是失败的回调函数(也就是为Reject时调用,对应的promise的reject)。当然由于第二个函数是可选的,我们一般都只写第一个,而用catch来代替错误的回调

promise
  .then(function(data) {
    // success
  })
  .catch(function(err) {
    // error
  });

链式调用

无论是then方法还是catch方法(catch可以看做then(null,rejection))返回的都是一个新的Promise实例,这也就意味着then和catch后面仍然可以调用then和catch,也就是可以采用链式写法。

promise.then(function(data) {
      return promiseA;
    }).then(function(data) {
        console.log("Resolved: ",data);
    }).catch(function(error){
        console.log("Rejected: ", err);
    });

采用链式的then,最主要的是可以指定一组按照次序调用的回调函数,也就是后面的异步请求依赖于前面异步请求的结果的状况下会采用链式调用。
这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

all和race

Promise有两个很有用的方法,all和race,这两个方法都是将多个Promise实例,包装成一个新的Promise实例,不同的是,race是取多个实例中最先返回的实例状态作为新实例的状态,而,all有两种状况,只有所有实例都返回Resolved,新实例的状态才变成Resolved,但是只有有一个实例返回了Rejected,新实例的状态就变成了Rejecte。

    var p = Promise.all([p1, p2, p3]);//p1,p2,p3都变成Resolved,p变成Resolved,p1,p2,p3中的任意一个变成Rejected,p变成Rejected。
    var p = Promise.race([p1, p2, p3]);//p1,p2,p3中的任意一个Resolved,p变成Resolved,其他的实例仍然保持原状态,也就是会继续执行

fetch

原生的XHR对象对Promise似乎不是很友好,于是fetch出现了,尽管到现在其兼容性还不是很好,我们只能用Polyfil来使用。

fetch是用来替代XHR发起异步请求的API,好的一点是fetch请求返回的是一个Promise对象。

fetch("/data.json",init).then(function(res) {
  // Resolved状态下返回一个Response对象
}, function(e) {
  console.log("Fetch failed!", e);
});

fetch()第一个参数是请求的地址,第二个参数则是请求的一些基本配置。

init = { 
    method: 'GET',//请求使用的方法,如 GET、POST
    headers: myHeaders,//请求的头信息,形式为 Headers 对象或 ByteString
    body:data,//请求的 body 信息,
    mode: 'cors',//请求的模式,如 cors、 no-cors 或者 same-origin
    credentials:'include', //请求的 credentials
    cache: 'default'//请求的 cache 模式: default, no-store, no-cache等
    }

这里注意,get方法是没有body参数的,query参数只能自己拼在url里了。

fetch中两个重要的API,Header和Response。

Header是要来生成请求的头信息,然后在fetch的第二个参数配置项中使用。

    reqHeaders = new Headers({
      "Content-Type": "text/plain",
      "Content-Length": content.length.toString(),
      "X-Custom-Header": "ProcessThisImmediately",
    });

Response对象则是fetch请求成功后的返回对象,里面包含了许多信息,其中我们比较常用的有:

status — 整数(默认值为200) 为response的状态码,对应着http的状态码
statusText — 字符串(默认值为"OK"),该值与HTTP状态码消息对应.
ok -该属性是来检查response的状态是否在200-299(包括200,299)这个范围内.该属性返回一个Boolean值.(我们可以直接通过这个来判断请求是否成功)
body-返回的数据一般都在这里

ES6 Generator/yield

程序员的最大特点就是嫌麻烦,Promise解决了回调地狱,但是我们还是不满足,Promise使得代码冗余,一大堆then,原来的语义变得很不清楚,我们希望以同步的形式执行异步操作,于是ES6增加了Generator/yield 。

什么是generator/yield呢?Generator是一类函数,这类函数长得向下面这样

    function* aGenerator() {
      yield 1+2;
      yield 2+3;
      return 'ending';
    }

    var test = aGenerator();  

可以看到这个函数相对于普通函数,有几个特点:
1.function和函数名之间带有*号,这是Generator函数的标志性特点,带 * 号的就是Generator函数(注意,这个星号只要放在function和函数名之间。。具体怎么放没有要求,一般我们靠近function)
2.yield语句,yield是一个暂停标志,一旦遇到这个标志,线程就暂时不执行,把执行权交给外部,直到next被调用,再把执行权拿过来,执行下面的代码。yield语句有点return的功能,会把紧随其后的表达式作为返回值,紧随其后的表达式自然会被执行。

分析一下上面的代码,初始化test的时候,aGenerator其实是不执行的(Generator函数都是暂缓执行的函数)。我们需要手动调用next来使之执行。

test.next();//开始执行aGenerator,直到遇到yield,return {value:3,done:false}
test.next();//从上次暂停的地方继续执行,直到遇到yield或return,return {value:5,done:false}
 test.next();//继续执行,遇到return返回,return {value:‘ending’,done:true}  

next方法可以带一个参数,这个参数可以传入Generator函数,来替换掉上一个next的返回值的value。

function* test(){
  var x = yield 2;
  yield x+3;
  return 'ending';
}

var t = test();
t.next()
//Object {value: 2, done: false}
t.next(10)
//Object {value: 13, done: false},原本应该用上面计算的2来计算,但是next的参数10,替换掉了这个结果。输出变成了10+3

然而实际开发中,单纯的Generator/yield没有啥实用价值,由于Generator函数无法自执行,需要手动调用next,我们不可能每次异步请求完之后再去调用next,这样太复杂了,于是出现了co以及ES7中的async/await ,这两者都是对Generator/yield的优化吧,可以是使Generator函数根据异步执行的结果来自动next。

ES7 async/await

前面说到单纯的Generator/yield没有啥实用价值,到了ES7,提出了async/await ,这套才算是进入规范,一个能自执行的Generator/yield。

在写法上,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,好处就是,async函数会自动执行,直到最后的return,相当于官方实现的co模块。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
    return 'ending'
}

asyncPrint('hello world', 1000).then(function(result){console.log(result)})  

以上代码来自ECMAScript 6 入门,如果是Generator/yield,asyncPrint只会在next方法调用的时候执行,现在,调用asyncPrint,会在1000毫秒之后输出‘hello world’,同时,asyncPrint函数返回一个Promise对象,执行then之后的函数,输出‘ending’;

修改一下上面的代码

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
  a=b;
    return 'ending'
}

asyncPrint('hello world', 1000).then(function(result){console.log(result)}).catch(function(error){console.log(error)})
//输出
//hello world
//ReferenceError: b is not defined

asyncPrint函数内部的错误使Promise对象rejected,执行catch之后的函数,之后的return ‘ending’则不再执行。如果想要即使前一个异步操作失败,也不要中断后面的操作,我们需要在asyncPrint函数内部就catch错误,这时可以将代码放在try...catch结构里面,或者把异步请求转为 Promise 对象后面再跟一个catch方法,处理前面可能出现的错误。

写在最后

在现在,最适合我们的就是用fetch代替xhr,用co来执行ES6,或者直接用async/await 来写异步操作,当然从兼容性来看,这些都需要我们做一些polyfill。

除了使异步操作变得更加方便,async/await解决了我之前的一个问题,后面的操作依赖于前面多个异步操作的返回结果,有点类似Promise的all方法,但是all方法中的任意一个异步请求失败都会影响整个Promise的状态,我们想要,无论里面的任意一个异步失败,最终的Promise都是resolve的。

yield语句后面可以跟数组或者对象,数组或者对象里的异步操作同时进行,等到它们全部完成,才进行下一步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容