目录

1 JavaScript的异步执行

1.1 概述

1.2 回调函数

1.3 事件监听

1.4 发布/订阅

1.5 异步操作的流程控制

1.6 串行执行

1.7 并行执行

1.8 并行与串行的结合

2 Promise对象

2.1 简介

2.2 Promise接口

2.2.1 Promise对象的生成

2.2.2 then方法

2.3 用法辨析

3 Promise的应用

3.1 加载图片

3.2 Ajax操作

3.3 小结


Promise是JavaScript异步操作解决方案。介绍Promise之前,先对异步操作做一个详细介绍。

1 JavaScript的异步执行

1.1 概述

Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

JavaScript语言本身并不慢,慢的是读写外部数据,比如等待Ajax请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)

  • "同步模式"就是传统做法,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的。这往往用于一些简单的、快速的、不涉及读写的操作。
  • "异步模式"则完全不同,每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立刻执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据返回了,再由系统通知执行第二段代码。所以,程序的执行顺序与任务的排列顺序是不一致的、异步的。

以下总结了"异步模式"编程的几种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的JavaScript程序。

1.2 回调函数

回调函数是异步编程最基本的方法。

假定有两个函数f1和f2,后者等待前者的执行结果。

f1();
f2();

如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。

function f1(callback){setTimeout(function () {// f1的任务代码callback();}, 1000);
}

执行代码就变成下面这样:

f1(f2);

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),使得程序结构混乱、流程难以追踪(尤其是回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

1.3 事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。

f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

function f1(){setTimeout(function () {// f1的任务代码f1.trigger('done');}, 1000);
}

上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

1.4 发布/订阅

"事件"完全可以理解成"信号",如果存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

这个模式有多种实现

首先,f2向"信号中心"jQuery订阅"done"信号。

jQuery.subscribe("done", f2);

然后,f1进行如下改写:

function f1(){setTimeout(function () {// f1的任务代码jQuery.publish("done");}, 1000);
}

jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。

f2完成执行后,也可以取消订阅(unsubscribe)。

jQuery.unsubscribe("done", f2);

这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

1.5 异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:确定操作执行的顺序,以后如何保证遵守这种顺序。

function async(arg, callback) {console.log('参数为 ' + arg +' , 1秒后返回结果');setTimeout(function() { callback(arg * 2); }, 1000);
}

上面代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。

如果有6个这样的异步任务,需要全部完成后,才能执行下一步的final函数。

function final(value) {console.log('完成: ', value);
}

请问应该如何安排操作流程?

async(1, function(value){async(value, function(value){async(value, function(value){async(value, function(value){async(value, function(value){async(value, final);});});});});
});

上面代码采用6个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护。

1.6 串行执行

我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function series(item) {if(item) {async( item, function(result) {results.push(result);return series(items.shift());});} else {return final(results);}
}
series(items.shift());

上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。

1.7 并行执行

流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];items.forEach(function(item) {async(item, function(result){results.push(result);if(results.length == items.length) {final(results);}})
});

上面代码中,forEach方法会同时发起6个异步任务,等到它们全部完成以后,才会执行final函数。

并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。

1.8 并行与串行的结合

所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务。这样就避免了过分占用系统资源。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;function launcher() {while(running < limit && items.length > 0) {var item = items.shift();async(item, function(result) {results.push(result);running--;if(items.length > 0) {launcher();} else if(running == 0) {final();}});running++;}
}launcher();

上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。

2 Promise对象

2.1 简介

Promise对象是CommonJS工作组提出的一种规范,目的是为异步操作提供统一接口。

那么,什么是Promises?

首先,它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样;其次,它起到代理作用(proxy),充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。

简单说,它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用。

比如,异步操作f1返回一个Promise对象,它的回调函数f2写法如下。

(new Promise(f1)).then(f2);

这种写法对于多层嵌套的回调函数尤其方便。

// 传统写法
step1(function (value1) {step2(value1, function(value2) {step3(value2, function(value3) {step4(value3, function(value4) {// ...});});});
});// Promises的写法
(new Promise(step1)).then(step2).then(step3).then(step4);

从上面代码可以看到,采用Promises接口以后,程序流程变得非常清楚,十分易读。

注意,为了便于理解,上面代码的Promise对象的生成格式,做了简化,真正的语法请参照下文。

总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promises规范就是为了解决这个问题而提出的,目标是使用正常的程序流程(同步),来处理异步操作。它先返回一个Promise对象,后面的操作以同步的方式,寄存在这个对象上面。等到异步操作有了结果,再执行前期寄放在它上面的其他操作。

Promises原本只是社区提出的一个构想,一些外部函数库率先实现了这个功能。ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象。

2.2 Promise接口

前面说过,Promise接口的基本思想是,异步任务返回一个Promise对象。

Promise对象只有三种状态。

  • 异步操作“未完成”(pending)
  • 异步操作“已完成”(resolved,又称fulfilled)
  • 异步操作“失败”(rejected)

这三种的状态的变化途径只有两种。

  • 异步操作从“未完成”到“已完成”
  • 异步操作从“未完成”到“失败”

这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise对象的最终结果只有两种。

  • 异步操作成功,Promise对象传回一个值,状态变为resolved
  • 异步操作失败,Promise对象抛出一个错误,状态变为rejected

2.2.1 Promise对象的生成

ES6提供了原生的Promise构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

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

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由JavaScript引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

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

2.2.2 then方法

Promise对象使用then方法添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时(变为resolved状态)时的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(可以省略)。一旦状态改变,就调用相应的回调函数。

// po是一个Promise对象
po.then(console.log,console.error
);

上面代码中,Promise对象po使用then方法绑定两个回调函数:操作成功时的回调函数console.log,操作失败时的回调函数console.error(可以省略)。这两个函数都接受异步操作传回的值作为参数。

then方法可以链式使用。

po.then(step1).then(step2).then(step3).then(console.log,console.error);

上面代码中,po的状态一旦变为resolved,就依次调用后面每一个then指定的回调函数,每一步都必须等到前一步完成,才会执行。最后一个then方法的回调函数console.logconsole.error,用法上有一点重要的区别。console.log只显示回调函数step3的返回值,而console.error可以显示step1step2step3之中任意一个发生的错误。也就是说,假定step1操作失败,抛出一个错误,这时step2step3都不会再执行了(因为它们是操作成功的回调函数,而不是操作失败的回调函数)。Promises对象开始寻找,接下来第一个操作失败时的回调函数,在上面代码中是console.error。这就是说,Promises对象的错误有传递性。

从同步的角度看,上面的代码大致等同于下面的形式。

try {var v1 = step1(po);var v2 = step2(v1);var v3 = step3(v2);console.log(v3);
} catch (error) {console.error(error);
}

或者:

为了捕获promise中的错误,我们可以使用catch方法。

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数

Promise.reject(123).then(v=>{console.log(v);
}).catch(v=>{console.warn(v);
});

2.3 用法辨析

Promise的用法,简单说就是一句话:使用then方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里? 注意:这个测试中的doSomethingElse是有返回值的return;

// 写法一
doSomething().then(function () {return doSomethingElse();
});// 写法二
doSomething().then(function () {doSomethingElse();
});// 写法三
doSomething().then(doSomethingElse());// 写法四
doSomething().then(doSomethingElse);

为了便于讲解,这四种写法都再用then方法接一个回调函数finalHandler。写法一的finalHandler回调函数的参数,是doSomethingElse函数的运行结果。

doSomething().then(function () {return doSomethingElse();
}).then(finalHandler);

写法二的finalHandler回调函数的参数是undefined

doSomething().then(function () {doSomethingElse();return;
}).then(finalHandler);

写法三的finalHandler回调函数的参数,不是doSomethingElse函数返回的回调函数的运行结果因为doSomethingElse(),函数后面有括号,就会让他自己立即执行,所以finalHandler的参数是doSomething()函数的结果

doSomething().then(doSomethingElse()).then(finalHandler);

写法四与写法三只有一个差别,那就是doSomethingElse会接收到doSomething()返回的结果,并且finalHandler回调函数的参数也会收到doSomethingElse函数的结果。

doSomething().then(doSomethingElse).then(finalHandler);

具体例子如下:

var p = new Promise(function(resolve,reject){resolve("promise测试成功");
});
function dosome(){console.log("回调函数");return true;
}p.then(dosome).then(function (value){console.log("测试,测试====="+value);
});
//运行结果
//回调函数
//测试,测试=====true
p.then(dosome()).then(function (value){console.log("测试,测试====="+value);
});
//运行结果
//回调函数
//测试,测试=====promise测试成功

3 Promise的应用

3.1 加载图片

我们可以把图片的加载写成一个Promise对象。

var preloadImage = function (path) {return new Promise(function (resolve, reject) {var image = new Image();image.onload  = resolve;image.onerror = reject;image.src = path;});
};

3.2 Ajax操作

Ajax操作是典型的异步操作,传统上往往写成下面这样。

function search(term, onload, onerror) {var xhr, results, url;url = 'http://example.com/search?q=' + term;xhr = new XMLHttpRequest();xhr.open('GET', url, true);xhr.onload = function (e) {if (this.status === 200) {results = JSON.parse(this.responseText);onload(results);}};xhr.onerror = function (e) {onerror(e);};xhr.send();
}search("Hello World", console.log, console.error);

如果使用Promise对象,就可以写成下面这样。

function search(term) {var url = 'http://example.com/search?q=' + term;var xhr = new XMLHttpRequest();var result;var p = new Promise(function (resolve, reject) {xhr.open('GET', url, true);xhr.onload = function (e) {if (this.status === 200) {result = JSON.parse(this.responseText);resolve(result);}};xhr.onerror = function (e) {reject(e);};xhr.send();});return p;
}search("Hello World").then(console.log, console.error);

加载图片的例子,也可以用Ajax操作完成。

function imgLoad(url) {return new Promise(function(resolve, reject) {var request = new XMLHttpRequest();request.open('GET', url);request.responseType = 'blob';request.onload = function() {if (request.status === 200) {resolve(request.response);} else {reject(new Error('图片加载失败:' + request.statusText));}};request.onerror = function() {reject(new Error('发生网络错误'));};request.send();});
}

3.3 小结

Promise对象的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它的一整套接口,可以实现许多强大的功能,比如为多个异步操作部署一个回调函数、为多个回调函数中抛出的错误统一指定处理方法等等。

而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是,编写和理解都相对比较难。

JavaScript高级语法之异步Promise(2:深入)相关推荐

  1. 深入 JavaScript 高级语法

    深入 JavaScript 高级语法 点击 28. 深拷贝.事件总线 27. 防抖和节流 26. BOM.DOM.浏览器事件 25. Cookie 24. IndexedDB 23. JSON.Sto ...

  2. JavaScript|JavaScript 高级语法——详细汇总

    JavaScript 高级语法 目录 JavaScript 高级语法 一.变量提升和函数提升 作用域的概念 1. 变量提升 ① 变量提升 ② 变量提升后,与外界同名变量不会相互影响 ③ 多次声明变量 ...

  3. 跟随coderwhy老师 系统学习 深入JavaScript高级语法

    深入JavaScript高级语法 昨天偶然看到这个 没想到codewhy老师还有这系列的课程 果断重新快速学一遍 这里面的内容比较系统 不用我一个一个去找小视频 后续的知识点更新以codewhy及后面 ...

  4. JavaScript高级语法

    目录 1.Exception 1.1.异常概述 1.1.1.Error 对象属性 1.1.2.Error Name Values 1.2.try-catch异常捕捉 1.3.异常演示 1.3.1.Ev ...

  5. JavaScript高级语法之解释器(Interpreter)

    目录 1 词法分析器 2 句法解析器 3 字节码生成器 4 字节码解释器 JavaScript解释器的作用,是执行JavaScript源码.它通常可以包含四个组成部分. 词法分析器(Lexical A ...

  6. JavaScript高级语法之一等公民——函数(高阶函数)

    JavaScript中一等公民 -- 函数 在JavaScript中,函数是非常重要的,并且是一等公民.所谓的一等公民到底是什么呢? 首先,函数的使用是非常灵活的: 其次函数可以作为另外一个函数的参数 ...

  7. JavaScript高级语法打包 - babel插件安装配置报错!Error: Cannot find module ‘@babel/preset-preset.env‘

    目录 1. 插件安装和配置 2. 运行打包 - 报错信息 3. 解决办法 1. 插件安装和配置 安装babel转换器相关的包: npm i babel-loader @babel/core @babe ...

  8. JavaScript高级语法-面向对象编程模式的特性及实现

    @写在前面:对于初学者友好,变量声明没有采取ES6+标准,且学且珍惜

  9. 读书笔记(06) - 语法基础 - JavaScript高级程序设计

    写在开头 本篇是小红书笔记的第六篇,也许你会奇怪第六篇笔记才写语法基础,笔者是不是穿越了. 答案当然是没有,笔者在此分享自己的阅读心得,不少人翻书都是从头开始,结果永远就只在前几章. 对此,笔者换了随 ...

最新文章

  1. 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能
  2. qt 启动画面显示图片_Qt程序起动画面QSplashScreen
  3. 猜拳小程序c语言编程,无聊的时候写的猜拳小程序
  4. response的content-type以及详细列表
  5. 按字符串长度切割字符串(支持汉字占2个长度)
  6. 个人做asp.net时犯过的错或是一点心得什么的(我就经常的更新一下吧)
  7. Luogu2495[SDOI2011]消耗战
  8. Java自引用造成的死循环
  9. php用于字符串函数是,php中用于查找字符串的常用函数
  10. 【转】C#中的弱事件:不同的解决方法
  11. GPON介绍及华为OLT网关注册配置流程
  12. 数据科学学习之统计实验的设计、检验与分析
  13. 抓取青果教务系统信息
  14. 5G消息亮相2021国际数字科技展暨天翼智能生态博览会
  15. 跳槽拿到最高月薪20K 我成为月薪20K的软件测试工程师是一种什么样的体验?
  16. 尚硅谷韩顺平Linux教程学习笔记
  17. 杭州是个技术乐观派的城市
  18. 深度学习进行人体的姿态估计
  19. Ignite VS Redis
  20. ubuntu server 安全模式磁盘检查修复

热门文章

  1. 查看微信好友是否被删除
  2. python 求系数矩阵_python - 如何在数据矩阵中计算nans的相关矩阵
  3. ubuntu16.04一些快捷键和有用的工具
  4. Alios-Thins教程连载 ①如何搭建阿里开源系统Alios-Things环境,烧录到乐鑫esp8266,打印 HelloWorld 。(附带镜像)
  5. 【Tableau Desktop 企业日常技巧6.0】Tableau如何将示例工作簿替换为自定义工作簿?(windows版本)
  6. Shell编程三剑客之awk
  7. 木马防线:一切木马都是纸老虎(下)(转)
  8. 九度 1341 -图最短路径 - 艾薇儿的演唱会
  9. 半径为r的均匀带电球体_放于真空中半径为R,带电量为q的均匀带电球体,求球内外各点电势分布...
  10. 首个完整利用WinRAR漏洞传播的恶意样本分析