JavaScript 异步编程的前世此生(上)

前言

提到 JavaScript 异步编程,不少小伙伴都很迷茫,本人花费大约一周的业余时间来对 JS 异步作一个完整的总结,和各位同窗共勉共进步!html

目录

part1 基础部分node

  • 什么是异步

part2 jQuery的解决方案jquery

  • jQuery-1.5 以后的 ajax
  • jQuery deferred
  • jQuery promise

part3 ES6-Promisegit

  • Promise 加入 ES6 标准
  • Promise 在 ES6 中的具体应用
  • 对标一下 Promise/A+ 规范
  • Promise 真的取代 callback 了吗?
  • 用 Q.js 库

part4 Generatores6

  • ES6 中的 Generator
  • Iterator 遍历器
  • Generator 的具体应用
  • Thunk 函数
  • Generator 与异步操做
  • koa 中使用 Generator
  • Generator 的本质是什么?是否取代了 callback

part5 async-awaitgithub

  • ES7 中引入 async-await
  • 如何在 nodejs v6.x版本中使用 async-await

part6 总结ajax

  • 总结

什么是异步

提醒:若是你是初学 js 的同窗,还没有有太多项目经验和基础知识,请就此打住,不要看这篇教程chrome

我思考问题、写文章通常都不按讨论出牌,别人写过的东西我不会再照着抄一遍。所以,后面全部的内容,都是我看了许多资料以后,我的从新思考提炼总结出来的,这确定不能算是初级教程。npm

若是你是已有 js 开发经验,并了解异步的基础知识,到这里来想深刻了解一下Promise Generatorasync-await,那就太好了,很是欢迎。编程

本节内容概述

  • JS 为什么会有异步
  • 异步的实现原理是什么
  • 经常使用的异步操做有哪些

JS 为什么会有异步

首先记住一句话 —— JS 是单线程的语言,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如

var i, t = Date.now() for (i = 0; i < 100000000; i++) { } console.log(Date.now() - t)  // 250 (chrome浏览器)

上面的程序花费 250ms 的时间执行完成,执行过程当中就会有卡顿,其余的事儿就先撂一边无论了。

执行程序这样没有问题,可是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不同了。所以在浏览器端运行的 js ,可能会有大量的网络请求,而一个网络资源啥时候返回,这个时间是不可预估的。这种状况也要傻傻的等着、卡顿着、啥都不作吗?———— 那确定不行。

所以,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先无论这边了,先干其余事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。

异步的实现原理

先看一段比较常见的代码

var ajax = $.ajax({ url: '/data/data1.json', success: function () { console.log('success') } })

上面代码中$.ajax()须要传入两个参数进去,urlsuccess,其中url是请求的路由,success是一个函数。这个函数传递过去不会当即执行,而是等着请求成功以后才能执行。对于这种传递过去不执行,等出来结果以后再执行的函数,叫作callback,即回调函数

再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本同样,惟一区别就是:上面代码时网络请求,而下面代码时 IO 操做。

var fs = require('fs') fs.readFile('data1.json', (err, data) => { console.log(data.toString()) })

从上面两个 demo 看来,实现异步的最核心原理,就是将callback做为参数传递给异步执行函数,当有结果返回以后再触发 callback执行,就是如此简单!

经常使用的异步操做

开发中比较经常使用的异步操做有:

  • 网络请求,如ajax http.get
  • IO 操做,如readFile readdir
  • 定时函数,如setTimeout setInterval

最后,请思考,事件绑定是否是也是异步操做?例如$btn.on('click', function() {...})。这个问题颇有意思,我会再后面的章节通过分析以后给出答案,各位先本身想一下。

 

jQuery-1.5 以后的 ajax

$.ajax这个函数各位应该都比较熟悉了,要完整的讲解 js 的异步操做,就必须先从$.ajax这个方法提及。

想要学到全面的知识,你们就不要着急,跟随个人节奏来,而且相信我。我安排的内容,确定都是有用的,对主题无用的东西,我不会拿来占用你们的时间。

本节内容概述

  • 传统的$.ajax
  • 1.5 版本以后的$.ajax
  • 改进以后的好处
  • 和后来的Promise的关系
  • 如何实现的?

传统的$.ajax

先来一段最多见的$.ajax的代码,固然是使用万恶的callback方式

var ajax = $.ajax({ url: 'data.json', success: function () { console.log('success') }, error: function () { console.log('error') } }) console.log(ajax) // 返回一个 XHR 对象

至于这么作会产生什么样子的诟病,我想你们应该都很明白了。不明白的本身私下去查,可是你也能够继续往下看,你只须要记住这样作很很差就是了,要否则 jquery 也不会再后面进行改进

1.5 版本以后的$.ajax

可是从v1.5开始,以上代码就能够这样写了:能够链式的执行done或者fail方法

var ajax = $.ajax('data.json') ajax.done(function () { console.log('success 1') }) .fail(function () { console.log('error') }) .done(function () { console.log('success 2') }) console.log(ajax) // 返回一个 deferred 对象

你们注意看以上两段代码中都有一个console.log(ajax),可是返回值是彻底不同的。

  • v1.5以前,返回的是一个XHR对象,这个对象不可能有done或者fail的方法的
  • v1.5开始,返回一个deferred对象,这个对象就带有donefail的方法,而且是等着请求返回以后再去调用

改进以后的好处

这是一个标志性的改造,无论这个概念是谁最早提出的,它在 jquery 中首先大量使用并让全球开发者都知道原来 ajax 请求还能够这样写。这为之后的Promise标准制定提供了很大意义的参考,你能够觉得这就是后面Promise的原型。

记住一句话————虽然 JS 是异步执行的语言,可是人的思惟是同步的————所以,开发者老是在寻求如何使用逻辑上看似同步的代码来完成 JS 的异步请求。而 jquery 的这一次更新,让开发者在必定程度上获得了这样的好处。

以前不管是什么操做,我都须要一股脑写到callback中,如今不用了。如今成功了就写到done中,失败了就写到fail中,若是成功了有多个步骤的操做,那我就写不少个done,而后链式链接起来就 OK 了。

和后来的Promise的关系

以上的这段代码,咱们还能够这样写。即不用donefail函数,而是用then函数。then函数的第一个参数是成功以后执行的函数(即以前的done),第二个参数是失败以后执行的函数(即以前的fail)。并且then函数还能够链式链接。

var ajax = $.ajax('data.json') ajax.then(function () { console.log('success 1') }, function () { console.log('error 1') }) .then(function () { console.log('success 2') }, function () { console.log('error 2') })

若是你对如今 ES6 的Promise有了解,应该能看出其中的类似之处。不了解也不要紧,你只须要知道它已经和Promise比较接近了。后面立刻会去讲Promise

如何实现的?

明眼人都知道,jquery 不可能改变异步操做须要callback的本质,它只不过是本身定义了一些特殊的 API,并对异步操做的callback进行了封装而已。

那么 jquery 是如何实现这一步的呢?请听下回分解!

jQuery deferred

上一节讲到 jquery v1.5 版本开始,$.ajax可使用相似当前Promisethen函数以及链式操做。那么它究竟是如何实现的呢?在此以前所用到的callback在这其中又起到了什么做用?本节给出答案

本节内容概述

  • 写一个传统的异步操做
  • 使用$.Deferred封装
  • 应用then方法
  • 有什么问题?

写一个传统的异步操做

给出一段很是简单的异步操做代码,使用setTimeout函数。

var wait = function () { var task = function () { console.log('执行完成') } setTimeout(task, 2000) } wait()

以上这些代码执行的结果你们应该都比较明确了,即 2s 以后打印出执行完成。可是我若是再加一个需求 ———— 要在执行完成以后进行某些特别复杂的操做,代码可能会不少,并且分好几个步骤 ———— 那该怎么办? 你们思考一下!

若是你不看下面的内容,并且目前尚未Promise的这个思惟,那估计你会说:直接在task函数中写就是了!不过相信你看完下面的内容以后,会放弃你如今的想法。

使用$.Deferred封装

好,接下来咱们让刚才简单的几行代码变得更加复杂。为什么要变得更加复杂?是由于让之后更加复杂的地方变得简单。这里咱们使用了 jquery 的$.Deferred,至于这个是个什么鬼,你们先不用关心,只须要知道$.Deferred()会返回一个deferred对象,先看代码,deferred对象的做用咱们会面会说。

function waitHandle() { var dtd = $.Deferred()  // 建立一个 deferred 对象

    var wait = function (dtd) {  // 要求传入一个 deferred 对象
        var task = function () { console.log('执行完成') dtd.resolve() // 表示异步任务已经完成
 } setTimeout(task, 2000) return dtd  // 要求返回 deferred 对象
 } // 注意,这里必定要有返回值
    return wait(dtd) }

以上代码中,又使用一个waitHandle方法对wait方法进行再次的封装。waitHandle内部代码,咱们分步骤来分析。跟着个人节奏慢慢来,保证你不会乱。

  • 使用var dtd = $.Deferred()建立deferred对象。经过上一节咱们知道,一个deferred对象会有done failthen方法(不明白的去看上一节)
  • 从新定义wait函数,可是:第一,要传入一个deferred对象(dtd参数);第二,当task函数(即callback)执行完成以后,要执行dtd.resolve()告诉传入的deferred对象,革命已经成功。第三;将这个deferred对象返回。
  • 返回wait(dtd)的执行结果。由于wait函数中返回的是一个deferred对象(dtd参数),所以wait(dtd)返回的就是dtd————若是你感受这里很乱,不要紧,慢慢捋,一行一行看,相信两三分钟就能捋顺!

最后总结一下,waitHandle函数最终return wait(dtd)即最终返回dtd(一个deferred)对象。针对一个deferred对象,它有done failthen方法(上一节说过),它还有resolve()方法(其实和resolve相对的还有一个reject方法,后面会提到)

应用then方法

接着上面的代码继续写

var w = waitHandle() w.then(function () { console.log('ok 1') }, function () { console.log('err 1') }).then(function () { console.log('ok 2') }, function () { console.log('err 2') })

上面已经说过,waitHandle函数最终返回一个deferred对象,而deferred对象具备done fail then方法,如今咱们正在使用的是then方法。至于then方法的做用,咱们上一节已经讲过了,不明白的同窗抓紧回去补课。

执行这段代码,咱们打印出来如下结果。能够将结果对标如下代码时哪一行。

执行完成
ok 1
ok 2

此时,你再回头想一想我刚才说提出的需求(要在执行完成以后进行某些特别复杂的操做,代码可能会不少,并且分好几个步骤),是否是有更好的解决方案了?

有同窗确定发现了,代码中console.log('err 1')console.log('err 2')何时会执行呢 ———— 你本身把waitHandle函数中的dtd.resolve()改为dtd.reject()试一下就知道了。

  • dtd.resolve() 表示革命已经成功,会触发then中第一个参数(函数)的执行,
  • dtd.reject() 表示革命失败了,会触发then中第二个参数(函数)执行

有什么问题?

总结一下一个deferred对象具备的函数属性,并分为两组:

  • dtd.resolve dtd.reject
  • dtd.then dtd.done dtd.fail

我为什么要分红两组 ———— 这两组函数,从设计到执行以后的效果是彻底不同的。第一组是主动触发用来改变状态(成功或者失败),第二组是状态变化以后才会触发的监听函数。

既然是彻底不一样的两组函数,就应该完全的分开,不然很容易出现问题。例如,你在刚才执行代码的最后加上这么一行试试。

w.reject()

那么如何解决这一个问题?请听下回分解!

jQuery promise

上一节经过一些代码演示,知道了 jquery 的deferred对象是解决了异步中callback函数的问题,可是

本节内容概述

  • 返回promise
  • 返回promise的好处
  • promise 的概念

返回promise

咱们对上一节的的代码作一点小小的改动,只改动了一行,下面注释。

function waitHandle() { var dtd = $.Deferred() var wait = function (dtd) { var task = function () { console.log('执行完成') dtd.resolve() } setTimeout(task, 2000) return dtd.promise()  // 注意,这里返回的是 primise 而不是直接返回 deferred 对象
 } return wait(dtd) } var w = waitHandle() // 通过上面的改动,w 接收的就是一个 promise 对象
$.when(w) .then(function () { console.log('ok 1') }) .then(function () { console.log('ok 2') })

 

改动的一行在这里return dtd.promise(),以前是return dtddtd是一个deferred对象,而dtd.promise就是一个promise对象。

promise对象和deferred对象最重要的区别,记住了————promise对象相比于deferred对象,缺乏了.resolve.reject这俩函数属性。这么一来,可就彻底不同了。

上一节咱们提到一个问题,就是在程序的最后一行加一句w.reject()会致使乱套,你如今再在最后一行加w.reject()试试 ———— 保证乱套不了 ———— 而是你的程序不能执行,直接报错。由于,wpromise对象,不具有.reject属性。

返回promise的好处

上一节提到deferred对象有两组属性函数,并且提到应该把这两组完全分开。如今经过上面一行代码的改动,就分开了。

  • waitHandle函数内部,使用dtd.resolve()来该表状态,作主动的修改操做
  • waitHandle最终返回promise对象,只能去被动监听变化(then函数),而不能去主动修改操做

一个“主动”一个“被动”,彻底分开了。

promise 的概念

jquery v1.5 版本发布时间距离如今(2018年)已经老早以前了,那会儿你们网页标配都是 jquery 。不管里面的deferredpromise这个概念和想法最先是哪位提出来的,可是最先展现给全世界开发者的是 jquery ,这算是Promise这一律念最早的提出者。

其实本次课程主要是给你们分析 ES6 的Promise Generatorasync-await,可是为什么要从 jquery 开始(你们如今用 jquery 愈来愈少)?就是要给你们展现一下这段历史的一些起点和发展的知识。有了这些基础,你再去接受最新的概念会很是容易,由于全部的东西都是从最初顺其天然发展进化而来的,咱们要去用一个发展进化的眼光学习知识,而不是死记硬背。

Promise 加入 ES6 标准

从 jquery v1.5 发布通过若干时间以后,Promise 终于出如今了 ES6 的标准中,而当下 ES6 也正在被大规模使用。

本节内容概述

  • 写一段传统的异步操做
  • Promise进行封装

写一段传统的异步操做

仍是拿以前讲 jquery deferred对象时的那段setTimeout程序

var wait = function () { var task = function () { console.log('执行完成') } setTimeout(task, 2000) } wait()

以前咱们使用 jquery 封装的,接下来将使用 ES6 的Promise进行封装,你们注意看有何不一样。

Promise进行封装

const wait =  function () { // 定义一个 promise 对象
    const promise = new Promise((resolve, reject) => { // 将以前的异步操做,包括到这个 new Promise 函数以内
        const task = function () { console.log('执行完成') resolve() // callback 中去执行 resolve 或者 reject
 } setTimeout(task, 2000) }) // 返回 promise 对象
    return promise }

注意看看程序中的注释,那都是重点部分。从总体看来,感受此次比用 jquery 那次简单一些,逻辑上也更加清晰一些。

  • 将以前的异步操做那几行程序,用new Promise((resolve,reject) => {.....})包装起来,最后return便可
  • 异步操做的内部,在callback中执行resolve()(代表成功了,失败的话执行reject

接着上面的程序继续往下写。wait()返回的确定是一个promise对象,而promise对象有then属性。

const w = wait() w.then(() => { console.log('ok 1') }, () => { console.log('err 1') }).then(() => { console.log('ok 2') }, () => { console.log('err 2') })

then仍是和以前同样,接收两个参数(函数),第一个在成功时(触发resolve)执行,第二个在失败时(触发reject)时执行。并且,then还能够进行链式操做。

以上就是 ES6 的Promise的基本使用演示。看完你可能会以为,这跟以前讲述 jquery 的不差很少吗 ———— 对了,这就是我要在以前先讲 jquery 的缘由,让你感受一篇一篇看起来如丝般顺滑!

接下来,将详细说一下 ES6 Promise 的一些比较常见的用法,敬请期待吧!

Promise 在 ES6 中的具体应用

上一节对 ES6 的 Promise 有了一个最简单的介绍,这一节详细说一下 Promise 那些最多见的功能

本节课程概述

  • 准备工做
  • 参数传递
  • 异常捕获
  • 串联多个异步操做
  • Promise.allPromise.race的应用
  • Promise.resolve的应用
  • 其余

准备工做

由于如下全部的代码都会用到Promise,所以干脆在全部介绍以前,先封装一个Promise,封装一次,为下面屡次应用。

const fs = require('fs') const path = require('path')  // 后面获取文件路径时候会用到
const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) // 注意,这里执行 reject 是传递了参数,后面会有地方接收到这个参数
            } else { resolve(data.toString()) // 注意,这里执行 resolve 时传递了参数,后面会有地方接收到这个参数
 } }) }) }

以上代码一个一段 nodejs 代码,将读取文件的函数fs.readFile封装为一个Promise。通过上一节的学习,我想你们确定都能看明白代码的含义,要是看不明白,你就须要回炉重造了!

参数传递

咱们要使用上面封装的readFilePromise读取一个 json 文件../data/data2.json,这个文件内容很是简单:{"a":100, "b":200}

先将文件内容打印出来,代码以下。你们须要注意,readFilePromise函数中,执行resolve(data.toString())传递的参数内容,会被下面代码中的data参数所接收到。

const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { console.log(data) })

再加一个需求,在打印出文件内容以后,我还想看看a属性的值,代码以下。以前咱们已经知道then能够执行链式操做,若是then有多步骤的操做,那么前面步骤return的值会被当作参数传递给后面步骤的函数,以下面代码中的a就接收到了return JSON.parse(data).a的值

const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { // 第一步操做
 console.log(data) return JSON.parse(data).a  // 这里将 a 属性的值 return
}).then(a => { // 第二步操做
    console.log(a)  // 这里能够获取上一步 return 过来的值
})

总结一下,这一段内容提到的“参数传递”其实有两个方面:

  • 执行resolve传递的值,会被第一个then处理时接收到
  • 若是then有链式操做,前面步骤返回的值,会被后面的步骤获取到

异常捕获

咱们知道then会接收两个参数(函数),第一个参数会在执行resolve以后触发(还能传递参数),第二个参数会在执行reject以后触发(其实也能够传递参数,和resolve传递参数同样),可是上面的例子中,咱们没有用到then的第二个参数。这是为什么呢 ———— 由于不建议这么用。

对于Promise中的异常处理,咱们建议用catch方法,而不是then的第二个参数。请看下面的代码,以及注释。

const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { console.log(data) return JSON.parse(data).a }).then(a => { console.log(a) }).catch(err => { console.log(err.stack) // 这里的 catch 就能捕获 readFilePromise 中触发的 reject ,并且能接收 reject 传递的参数
})

在若干个then串联以后,咱们通常会在最后跟一个.catch来捕获异常,并且执行reject时传递的参数也会在catch中获取到。这样作的好处是:

  • 让程序看起来更加简洁,是一个串联的关系,没有分支(若是用then的两个参数,就会出现分支,影响阅读)
  • 看起来更像是try - catch的样子,更易理解

串联多个异步操做

若是如今有一个需求:先读取data2.json的内容,当成功以后,再去读取data1.json。这样的需求,若是用传统的callback去实现,会变得很麻烦。并且,如今只是两个文件,若是是十几个文件这样作,写出来的代码就无法看了(臭名昭著的callback-hell)。可是用刚刚学到的Promise就能够轻松胜任这项工做

const fullFileName2 = path.resolve(__dirname, '../data/data2.json') const result2 = readFilePromise(fullFileName2) const fullFileName1 = path.resolve(__dirname, '../data/data1.json') const result1 = readFilePromise(fullFileName1) result2.then(data => { console.log('data2.json', data) return result1  // 此处只需返回读取 data1.json 的 Promise 便可
}).then(data => { console.log('data1.json', data) // data 便可接收到 data1.json 的内容
})

上文“参数传递”提到过,若是then有链式操做,前面步骤返回的值,会被后面的步骤获取到。可是,若是前面步骤返回值是一个Promise的话,状况就不同了 ———— 若是前面返回的是Promise对象,后面的then将会被当作这个返回的Promise的第一个then来对待 ———— 若是你这句话看不懂,你须要将“参数传递”的示例代码和这里的示例代码联合起来对比着看,而后体会这句话的意思。

Promise.allPromise.race的应用

我还得继续提出更加奇葩的需求,以演示Promise的各个经常使用功能。以下需求:

读取两个文件data1.jsondata2.json,如今我须要一块儿读取这两个文件,等待它们所有都被读取完,再作下一步的操做。此时须要用到Promise.all

// Promise.all 接收一个包含多个 promise 对象的数组
Promise.all([result1, result2]).then(datas => { // 接收到的 datas 是一个数组,依次包含了多个 promise 返回的内容
    console.log(datas[0]) console.log(datas[1]) })

读取两个文件data1.jsondata2.json,如今我须要一块儿读取这两个文件,可是只要有一个已经读取了,就能够进行下一步的操做。此时须要用到Promise.race

// Promise.race 接收一个包含多个 promise 对象的数组
Promise.race([result1, result2]).then(data => { // data 即最早执行完成的 promise 的返回值
 console.log(data) })

Promise.resolve的应用

从 jquery 引出,到此即将介绍完 ES6 的Promise,如今咱们再回归到 jquery 。

你们都是到 jquery v1.5 以后$.ajax()返回的是一个deferred对象,而这个deferred对象和咱们如今正在学习的Promise对象已经很接近了,可是还不同。那么 ———— deferred对象可否转换成 ES6 的Promise对象来使用??

答案是能!须要使用Promise.resolve来实现这一功能,请看如下代码:

// 在浏览器环境下运行,而非 node 环境
cosnt jsPromise = Promise.resolve($.ajax('/whatever.json')) jsPromise.then(data => { // ...
})

注意:这里的Promise.resolve和文章最初readFilePromise函数内部的resolve函数可千万不要混了,彻底是两码事儿。JS 基础好的同窗一看就明白,而这里看不明白的同窗,要特别注意。

实际上,并非Promise.resolve对 jquery 的deferred对象作了特殊处理,而是Promise.resolve可以将thenable对象转换为Promise对象。什么是thenable对象?———— 看个例子

// 定义一个 thenable 对象
const thenable = { // 所谓 thenable 对象,就是具备 then 属性,并且属性值是以下格式函数的对象
    then: (resolve, reject) => { resolve(200) } } // thenable 对象能够转换为 Promise 对象
const promise = Promise.resolve(thenable) promise.then(data => { // ...
})

上面的代码就将一个thenalbe对象转换为一个Promise对象,只不过这里没有异步操做,全部的都会同步执行,可是不会报错的。

其实,在咱们的平常开发中,这种将thenable转换为Promise的需求并很少。真正须要的是,将一些异步操做函数(如fs.readFile)转换为Promise(就像文章一开始readFilePromise作的那样)。这块,咱们后面会在介绍Q.js库时,告诉你们一个简单的方法。

其余

以上都是一些平常开发中很是经常使用的功能,其余详细的介绍,请参考阮一峰老师的 ES6 教程 Promise 篇

最后,本节咱们只是介绍了Promise的一些应用,通俗易懂拿来就用的东西,可是没有提高到理论和标准的高度。有人可能会不屑 ———— 我会用就好了,要那么空谈的理论干吗?———— 你只会使用却上升不到理论高度,永远都是个搬砖的,搬一块砖挣一毛钱,不搬就不挣钱! 在我看来,全部的知识应该都须要上升到理论高度,将实际应用和标准对接,知道真正的出处,才能走的长远。

下一节咱们介绍 Promise/A+ 规范

 

对标一下 Promise/A+ 规范

Promise/A 是由 CommonJS 组织制定的异步模式编程规范,后来又通过一些升级,就是当前的 Promise/A+ 规范。上一节讲述的Promise的一些功能实现,就是根据这个规范来的。

本节内容概述

  • 介绍规范的核心内容
  • 状态变化
  • then方法
  • 接下来...

介绍规范的核心内容

网上有不少介绍 Promise/A+ 规范的文章,你们能够搜索来看,可是它的核心要点有如下几个,我也是从看了以后本身总结的

关于状态

  • promise 可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
  • promise 的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换

关于then方法

  • promise 必须实现then方法,并且then必须返回一个 promise ,同一个 promise 的then能够调用屡次(链式),而且回调的执行顺序跟它们被定义时的顺序一致
  • then方法接受两个参数,第一个参数是成功时的回调,在 promise 由“等待”态转换到“完成”态时调用,另外一个是失败时的回调,在 promise 由“等待”态转换到“拒绝”态时调用

下面挨个介绍这些规范在上一节代码中的实现,所谓理论与实践相结合。在阅读如下内容时,你要时刻准备参考上一节的代码。

状态变化

promise 可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)

拿到上一节的readFilePromise函数,而后执行const result = readFilePromise(someFileName)会获得一个Promise对象。

  • 刚刚建立时,就是 等待(pending)状态
  • 若是读取文件成功了,readFilePromise函数内部的callback中会自定调用resolve(),这样就变为 已完成(fulfilled)状态
  • 若是很不幸读取文件失败了(例如文件名写错了,找不到文件),readFilePromise函数内部的callback中会自定调用reject(),这样就变为 已拒绝(rejeced)状态

promise 的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换

这个规则仍是能够参考读取文件的这个例子。从一开始准备读取,到最后不管是读取成功或是读取失败,都是不可逆的。另外,读取成功和读取失败之间,也是不能互换的。这个逻辑没有任何问题,很好理解。

then方法

promise 必须实现then方法,并且then必须返回一个 promise ,同一个 promise 的then能够调用屡次(链式),而且回调的执行顺序跟它们被定义时的顺序一致

  • promise对象必须实现then方法这个无需解释,没有then那就不叫promise
  • “并且then必须返回一个promise,同一个 promise 的then能够调用屡次(链式)” ———— 这两句话说明了一个意思 ———— then确定要再返回一个promise,要否则then后面怎么能再链式的跟一个then呢?

then方法接受两个参数,第一个参数是成功时的回调,在 promise 由“等待”态转换到“完成”态时调用,另外一个是失败时的回调,在 promise 由“等待”态转换到“拒绝”态时调用

这句话比较好理解了,咱们从一开始就在 demo 中演示。

接下来...

Promise的应用、规范都介绍完了,看起来挺牛的,也解决了异步操做中使用callback带来的不少问题。可是Promise本质上究竟是一种什么样的存在,它是真的把callback弃而不用了吗,仍是二者有什么合做关系?它究竟是真的神通广大,仍是使用了障眼法?

这些问题,你们学完Promise以后应该去思考,不能光学会怎么用就中止了。下一节咱们一块儿来探讨~

Promise 真的取代 callback 了吗

Promise 虽然改变了 JS 工程师对于异步操做的写法,可是却改变不了 JS 单线程、异步的执行模式。

本节概述

  • JS 异步的本质
  • Promise 只是表面的写法上的改变
  • Promise 中不能缺乏 callback
  • 接下来...

JS 异步的本质

从最初的 ES三、4 到 ES5 再到如今的 ES6 和即将到来的 ES7,语法标准上更新不少,可是 JS 这种单线程、异步的本质是没有改变的。nodejs 中读取文件的代码一直均可以这样写

fs.readFile('some.json', (err, data) => { })

既然异步这个本质不能改变,伴随异步在一块儿的永远都会有callback,由于没有callback就没法实现异步。所以callback永远存在。

Promise 只是表面的写法上的改变

JS 工程师不会讨厌 JS 异步的本质,可是很讨厌 JS 异步操做中callback的书写方式,特别是遇到万恶的callback-hell(嵌套callback)时。

计算机的抽象思惟和人的具象思惟是彻底不同的,人永远喜欢看起来更加符合逻辑、更加易于阅读的程序,所以如今特别强调代码可读性。而Promise就是一种代码可读性的变化。你们感觉一下这两种不一样(这其中还包括异常处理,加上异常处理会更加复杂)

第一种,传统的callback方式

fs.readFile('some1.json', (err, data) => { fs.readFile('some2.json', (err, data) => { fs.readFile('some3.json', (err, data) => { fs.readFile('some4.json', (err, data) => { }) }) }) })

第二种,Promise方式

readFilePromise('some1.json').then(data => { return readFilePromise('some2.json') }).then(data => { return readFilePromise('some3.json') }).then(data => { return readFilePromise('some4.json') })

这两种方式对于代码可读性的对比,很是明显。可是最后再次强调,Promise只是对于异步操做代码可读性的一种变化,它并无改变 JS 异步执行的本质,也没有改变 JS 中存在callback的现象。

Promise 中不能缺乏 callback

上文已经基本给出了上一节提问的答案,可是这里还须要再加一个补充:Promise不只仅是没有取代callback或者弃而不用,反而Promise中要使用到callback。由于,JS 异步执行的本质,必须有callback存在,不然没法实现。

再次粘贴处以前章节的封装好的一个Promise函数(进行了一点点简化)

const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { resolve(data.toString()) }) }) }

上面的代码中,promise对象的状态要从pending变化为fulfilled,就须要去执行resolve()函数。那么是从哪里执行的 ———— 还得从callback中执行resolve函数 ———— 这就是Promise也须要callback的最直接体现。

接下来...

一块技术“火”的程度和第三方开源软件的数量、质量以及使用状况有很大的正比关系。例如为了简化 DOM 操做,jquery 风靡全世界。Promise 用的比较多,第三方库固然就必不可少,它们极大程度的简化了 Promise 的代码。

接下来咱们一块儿看看Q.js这个库的使用,学会了它,将极大程度提升你写 Promise 的效率。

使用 Q.js 库

若是实际项目中使用Promise,仍是强烈建议使用比较靠谱的第三方插件,会极大增长你的开发效率。除了将要介绍的Q.js,还有bluebird也推荐使用,去 github 自行搜索吧。

另外,使用第三方库不只仅是提升效率,它还让你在浏览器端(不支持Promise的环境中)使用promise

本节展现的代码参考这里

本节内容概述

  • 下载和安装
  • 使用Q.nfcallQ.nfapply
  • 使用Q.defer
  • 使用Q.denodeify
  • 使用Q.allQ.any
  • 使用Q.delay
  • 其余

下载和安装

能够直接去它的 github 地址 (近 1.3W 的 star 数量说明其用户群很大)查看文档。

若是项目使用 CommonJS 规范直接 npm i q --save,若是是网页外链可寻找可用的 cdn 地址,或者干脆下载到本地。

如下我将要演示的代码,都是使用 CommonJS 规范的,所以我要演示代码以前加上引用,之后的代码演示就不重复加了。

const Q = require('q')

使用Q.nfcallQ.nfapply

要使用这两个函数,你得首先了解 JS 的callapply,若是不了解,先去看看。熟悉了这两个函数以后,再回来看。

Q.nfcall就是使用call的语法来返回一个promise对象,例如

const fullFileName = path.resolve(__dirname, '../data/data1.json') const result = Q.nfcall(fs.readFile, fullFileName, 'utf-8')  // 使用 Q.nfcall 返回一个 promise
result.then(data => { console.log(data) }).catch(err => { console.log(err.stack) })

Q.nfapply就是使用apply的语法返回一个promise对象,例如

const fullFileName = path.resolve(__dirname, '../data/data1.json') const result = Q.nfapply(fs.readFile, [fullFileName, 'utf-8'])  // 使用 Q.nfapply 返回一个 promise
result.then(data => { console.log(data) }).catch(err => { console.log(err.stack) })

怎么样,体验了一把,是否是比直接本身写Promise简单多了?

使用Q.defer

Q.defer算是一个比较偏底层一点的 API ,用于本身定义一个promise生成器,若是你须要在浏览器端编写,并且浏览器不支持Promise,这个就有用处了。

function readFile(fileName) { const defer = Q.defer() fs.readFile(fileName, (err, data) => { if (err) { defer.reject(err) } else { defer.resolve(data.toString()) } }) return defer.promise } readFile('data1.json') .then(data => { console.log(data) }) .catch(err => { console.log(err.stack) })

使用Q.denodeify

咱们在很早以前的一节中本身封装了一个fs.readFilepromise生成器,这里再次回顾一下

const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) } else { resolve(data.toString()) } }) }) }

虽然看着不麻烦,可是仍是须要不少行代码来实现,若是使用Q.denodeify,一行代码就搞定了!

const readFilePromise = Q.denodeify(fs.readFile)

Q.denodeify就是一键将fs.readFile这种有回调函数做为参数的异步操做封装成一个promise生成器,很是方便!

使用Q.allQ.any

这两个其实就是对应了以前讲过的Promise.allPromise.race,并且应用起来如出一辙,很少赘述。

const r1 = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') const r2 = Q.nfcall(fs.readFile, 'data2.json', 'utf-8') Q.all([r1, r2]).then(arr => { console.log(arr) }).catch(err => { console.log(err) })

使用Q.delay

Q.delay,顾名思义,就是延迟的意思。例如,读取一个文件成功以后,再过五秒钟以后,再去作xxxx。这个若是是本身写的话,也挺费劲的,可是Q.delay就直接给咱们分装好了。

const result = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') result.delay(5000).then(data => { // 获得结果
 console.log(data.toString()) }).catch(err => { // 捕获错误
 console.log(err.stack) })

其余

以上就是Q.js一些最经常使用的操做,其余的一些很是用技巧,你们能够去搜索或者去官网查看文档。

至此,ES6 Promise的全部内容就已经讲完了。可是异步操做的优化到这里没有结束,更加精彩的内容还在后面 ———— Generator

 

 

 

 传送门:JavaScript 异步编程的前世此生(下)

 

 

文章转载:https://blog.csdn.net/sinat_17775997/article/details/70307956(感谢、尊重做者、鞠躬)

相关文章
相关标签/搜索