Promise

2018/01/16 探索

JavaScript语言的最大特点是单线程,也就是说在同一时间内只能执行一个事务。Node.js中为JavaScript语言提供了非阻塞型IO机制,所有的操作都是异步的。在实际编写代码的时候会出现一个问题,那就是代码的执行顺序与预期不同。这篇文章总结Promise的使用方法,以及如何使用Promise来编写并发的代码。

知识准备

JavaScript一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML网页上使用,用来给HTML网页增加动态功能。

Node.js提供一种简单的、用于创建高性能服务器及可在该服务器中运行各种应用程序的开发工具。Node.js的特点是改进了客户端到服务端的连接方式,为每个客户端连接触发一个在Node.js内部进行处理的事件,因此Node.js天生适用于大量用户的并发链接的Web应用程序。

什么是非阻塞型IO机制

IO是指Input/Output,泛指文件读写操作、请求响应动作等。一般而言IO操作需要消耗大量时间,阻塞型IO机制指后续操作必须等待当前IO操作执行完毕之后才能继续。例如下面的代码:

console.log('第1行代码');
setTimeout(function () {
	console.log('IO操作');
}, 1000);
console.log('第2行代码');

阻塞型IO机制要求在“IO操作”执行完后,才能继续执行“第2行代码”,注意到其中setTimeout函数用于模拟IO操作的耗时。我们对这段代码的预期输出是:

1行代码
IO操作
2行代码

如果你将上述代码用node.js运行的话,你会发现它实际上的输出结果是:

1行代码
2行代码
IO操作

这是因为node.js在执行IO操作的代码之后,立刻执行后续的代码,并将IO操作的返回结果放在回调函数中处理,这个过程就是非阻塞型IO机制。

阻塞型与非阻塞型的区别在于程序在等待调用时的结果。阻塞型在调用结果返回之前挂起当前线程,只有在结果返回之后才能继续线程,而非阻塞型则继续进行后续线程,尽管调用结果并没有立刻返回。

用一个简单的例子来形容:假如你正在打电话给书店需要购买一本书,书店老板需要去仓库找到这本书。如果是阻塞型调用,就好比你必须要等待书店老板找到这本书并告知你结果,这时你不能做任何事情。如果是非阻塞型调用,就好比你可以挂起电话然后做别的事情,偶尔需要打个电话再问问这本书找到没有。

Promise,承诺?

这让人感到难过,因为代码的编写方式与预期的执行结果不一致。我们在编写代码的时候期望是同步执行,而实际上JavaScript的代码是异步执行的。异步是指当一个调用执行完毕之后,在结果未返回之前就执行下一句代码,而结果将会在回调函数中返回。

类似的,“承诺”返回结果的对象在JacaScript中称为Promise对象。

语法

如何创建一个Promise对象?

new Promise( /* executor */ function(resolve, reject) { ... } );

Promise有两个变量,一个是resolve,另一个是reject,定义Promise对象的时候必须要这两个变量。其中resolve用于返回成功的结果,而reject用于返回失败的结果。我们用一个订外卖的例子来理解:

function 烧菜 () {
  return yes || no;
}

function 订外卖 () {
  let result = 烧菜()
  return new Promise(function(resolve, reject) {
    if (result == yes) {
      resolve(菜已经准备好,可以送出)
    } else {
      reject(菜还没准备好,抱歉)
    }
  })
}

上面有两个函数,其中烧菜会返回yes或者是no,表示烧菜已经完成或者是还没完成。而订外卖会返回一个Promise对象,如果烧菜已经完成,就会使用resolve返回可以送出的结果,如果烧菜没有完成,就会使用reject返回抱歉的结果。也就是说,调用订外卖这个过程,无论结果如何,都会承诺告知你一个结果。

那么我们怎么调用这个过程以及接收这个结果呢?

订外卖().then(function (resolve) {
  console.log(resolve)
}).catch(reject) {
  console.log(reject)
}

如果烧菜成功了,那么then过程会接收到这个resolve结果,如果烧菜没有完成,那么catch过程会接收到这个reject结果。

Promise的串行

假如我有多个Promise过程,并且它们之间的运行是相互依赖的,后面的Promise依赖前面的Promise的结果,那么怎么编写嵌套的Promise呢?举一个例子:

function multiply (input) {
    return new Promise(function (resolve, reject) {
        console.log('计算 ' + input + ' x ' + input + '...')
        setTimeout(resolve, 500, input * input)
    });
}

function add(input) {
    return new Promise(function (resolve, reject) {
        console.log('计算 ' + input + ' + ' + input + '...')
        setTimeout(resolve, 500, input + input)
    });
}

var p = new Promise(function (resolve, reject) {
    resolve(1234)
});

p.then(multiply).then(add).then(multiply).then(add).then(function (result) {
    console.log('结果是: ' + result)
})

上面定义了两个方法,分别是multiply乘法函数以及add加法函数,p是一个Promise对象,它传入1234这个数值,通过上述then的拼接有序的完成整个复合调用过程,这种语法能够实现同步式的编程。

Promise的并发

有的时候我们希望函数的调用是并发型的,例如网站在加载图片、视频等资源的时候,不同图片之间的加载并不存在前后的依赖关系,我们希望越快地完成加载。Promise提供两个方法实现并发,分别是Promise.all与Promise.race。

Promise.all适用于不同类型、没有依赖的函数调用,它将会返回一个数组,里面保存着每个Promise对象返回的结果,例如:

function a () {
	return new Promise(function (resolve, reject) {
		resolve(10)
	})
}

function b () {
	return new Promise(function (resolve, reject) {
		resolve(20)
	})
}

Promise.all([a(), b()]).then(function(result) {
	console.log(result)
}).catch(function (error) {
	console.log(error)
})

a过程以及b过程都被执行,并且返回结果[10, 20]。注意到其中任何一个Promise发生reject或者是error,那么都会触发Promise.all这个对象返回reject。

Promise.race适用于同类型、没有依赖的函数调用,它返回的结果取决于其中最快完成的Promise,如果最快完成的是resolve结果,那么Promise.race返回resolve,否则返回reject。

var p1 = new Promise(function (resolve, reject) { 
  setTimeout(resolve, 500, "one"); 
})

var p2 = new Promise(function (resolve, reject) { 
  setTimeout(resolve, 100, "two"); 
})

Promise.race([p1, p2]).then(function (value) {
  console.log(value);
})

上面输出的结果是two,因为p2只需要100毫秒,而p1需要500毫秒,所以p2更快返回了结果。这里的Promise.race同时完成了p1与p2的过程,但是返回的结果是选择了p2的结果,因为p2更快。

总结

这是关于Promise的基础用法,它可以帮助改善编写同步、异步的JS代码。关于Promise的使用现在有很多开源的高级封装,这里介绍的是JS原生支持的语法。

ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现。其中,Promise对象在 ES6 版本中得以规范。更进一步的,async语法封装了Promise对象,用来取代回调函数、解决异步操作的问题,async语法在 ES7 版本中得以体现。

Search

    Table of Contents