JavaScript异步编程的6种方法
栏目: JavaScript · 发布时间: 5年前
内容简介:你应该知道,Javascript语言的执行环境是"所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
前言
你应该知道,Javascript语言的执行环境是" 单线程
"(single thread)。
所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的
异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
本文总结了"异步模式"编程的4种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的Javascript程序。
1、回调函数
这是异步编程最基本的方法。
假定有两个函数f1和f2,后者等待前者的执行结果。
f1(); f2();复制代码
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback){ setTimeout(function () { // f1的任务代码 callback(); }, 1000); }复制代码
执行代码就变成下面这样:
f1(f2);复制代码
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合( Coupling
),流程会很混乱,而回调函数有一个致命的弱点,就是容易写出 回调地狱
2、事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以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),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
2、发布/订阅
上一节的"事件",完全可以理解成"信号"。
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,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);复制代码
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行
4、Promises对象
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。 Promise
对象代表一个异步操作,
有三种状态: Pending
(进行中)、 Resolved
(已完成,又称Fulfilled)和 Rejected
(已失败)。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
这也是 Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从 Pending
变为 Resolved
和从 Pending
变为 Rejected
。
只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise
对象添加回调函数,也会立即得到这个结果。
这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了 Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外, Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
也有一些缺点。首先,无法取消 Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数, Promise
内部抛出的错误,不会反应到外部。第三,当处于 Pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
基本用法
var promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });复制代码
Promise实例生成以后,可以用 then
方法分别指定 Resolved
状态和 Reject
状态的回调函数。
promise.then(function(value) { // success }, function(error) { // failure });复制代码
Promise.prototype.then()
Promise实例具有 then
方法,也就是说, then
方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过, then
方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。
then
方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即 then
方法后面再调用另一个 then
方法。
getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... });复制代码
Promise.prototype.catch()
Promise.prototype.catch
方法是 .then(null, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON("/posts.json").then(function(posts) { // ... }).catch(function(error) { // 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error); });复制代码
5、Generator 函数
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同
Generator函数有多种理解角度。从语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
形式上,Generator函数是一个普通函数,但是有两个特征。一是, function
关键字与函数名之间有一个星号;二是,函数体内部使用 yield
语句,定义不同的内部状态(yield语句在英语里的意思就是“产出”)。
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();复制代码
上面代码定义了一个Generator函数 helloWorldGenerator
,它内部有两个 yield
语句“hello”和“world”,即该函数有三个状态:hello,world和return语句(结束执行)。
然后,Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用 next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield
语句(或 return
语句)为止。换言之,Generator函数是分段执行的, yield
语句是暂停执行的标记,而 next
方法可以恢复执行。
hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }复制代码
上面代码一共调用了四次 next
方法。
第一次调用,Generator函数开始执行,直到遇到第一个 yield
语句为止。 next
方法返回一个对象,它的 value
属性就是当前 yield
语句的值hello, done
属性的值false,表示遍历还没有结束。
第二次调用,Generator函数从上次 yield
语句停下的地方,一直执行到下一个 yield
语句。 next
方法返回的对象的 value
属性就是当前 yield
语句的值world, done
属性的值false,表示遍历还没有结束。
第三次调用,Generator函数从上次 yield
语句停下的地方,一直执行到 return
语句(如果没有return语句,就执行到函数结束)。 next
方法返回的对象的 value
属性,就是紧跟在 return
语句后面的表达式的值(如果没有 return
语句,则 value
属性的值为undefined), done
属性的值true,表示遍历已经结束。
第四次调用,此时Generator函数已经运行完毕, next
方法返回对象的 value
属性为undefined, done
属性为true。以后再调用 next
方法,返回的都是这个值。
总结一下,调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的 next
方法,就会返回一个有着 value
和 done
两个属性的对象。 value
属性表示当前的内部状态的值,是 yield
语句后面那个表达式的值; done
属性是一个布尔值,表示是否遍历结束
yield语句
由于Generator函数返回的遍历器对象,只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。 yield
语句就是暂停标志。
遍历器对象的 next
方法的运行逻辑如下。
(1)遇到 yield
语句,就暂停执行后面的操作,并将紧跟在 yield
后面的那个表达式的值,作为返回的对象的 value
属性值。
(2)下一次调用 next
方法时,再继续往下执行,直到遇到下一个 yield
语句。
(3)如果没有再遇到新的 yield
语句,就一直运行到函数结束,直到 return
语句为止,并将 return
语句后面的表达式的值,作为返回的对象的 value
属性值。
(4)如果该函数没有 return
语句,则返回的对象的 value
属性值为 undefined
。
需要注意的是, yield
语句后面的表达式,只有当调用 next
方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* gen() { yield 123 + 456; }复制代码
上面代码中,yield后面的表达式 123 + 456
,不会立即求值,只会在 next
方法将指针移到这一句时,才会求值。
yield
语句与 return
语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到 yield
,函数暂停执行,下一次再从该位置继续向后执行,而 return
语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个) return
语句,但是可以执行多次(或者说多个) yield
语句。正常函数只能返回一个值,因为只能执行一次 return
;Generator函数可以返回一系列的值,因为可以有任意多个 yield
。从另一个角度看,也可以说Generator生成了一系列的值,这也就是它的名称的来历(在英语中,generator这个词是“生成器”的意思)。
6、async与await
ES7提供了 async
函数,使得异步操作变得更加方便。 async
函数是什么?一句话, async
函数就是Generator函数的语法糖。
依次读取两个文件。
var fs = require('fs'); var readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) reject(error); resolve(data); }); }); }; var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };复制代码
写成 async
函数,就是下面这样。
var asyncReadFile = async function (){ var f1 = await readFile('/etc/fstab'); var f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };复制代码
一比较就会发现, async
函数就是将Generator函数的星号( *
)替换成 async
,将 yield
替换成 await
,仅此而已。
async
函数对 Generator 函数的改进,体现在以下四点
(1)内置执行器。Generator函数的执行必须靠执行器,所以才有了 co
模块,而 async
函数自带执行器。也就是说, async
函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。 async
和 await
,比起星号和 yield
,语义更清楚了。 async
表示函数里有异步操作, await
表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co
模块约定, yield
命令后面只能是Thunk函数或Promise对象,而 async
函数的 await
命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是Promise。 async
函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了。你可以用 then
方法指定下一步的操作。
进一步说, async
函数完全可以看作多个异步操作,包装成的一个Promise对象,而 await
命令就是内部 then
命令的语法糖。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
VISUAL FOXPRO程序设计实用教程习题解答与实验指导
康萍 / 中国经济出版社 / 2006-6 / 22.80元
本书为康萍等主编的《VisUal FosxPro程序设计实用教程》的配套习题解答与实验指导教材,由常年工作在计算机教学第一线的教师组织编写。全书共分为三部分:第1部分为实验指导,包括33个实验,分别讲述了每个实验的实验目的.实验内容和操作步骤;第2部分为开发实例,介绍了图书营理系统的分析、设计与实现过程;第3部分为配套教材各章节习题参考答案。 本书实验部分可操作性及实用性较强。系统开发案......一起来看看 《VISUAL FOXPRO程序设计实用教程习题解答与实验指导》 这本书的介绍吧!