Maison > interface Web > js tutoriel > le corps du texte

Utilisation de la promesse, du générateur (générateur), de la fonction asynchrone (asynchrone)

一个新手
Libérer: 2017-10-14 09:16:05
original
1616 Les gens l'ont consulté

Promise

Qu'est-ce que

Promise est une solution de programmation asynchrone. Un objet Promise représente l'état final (achèvement ou échec) d'une opération asynchrone et le résultat renvoyé.

En fait, nous avons déjà vu quelques implémentations de Promise dans l'ajax de jQuery, grâce à Promise, nous pouvons convertir les rappels en appels en chaîne, ce qui joue également un rôle dans le découplage.

Comment utiliser

L'idée de base de l'interface Promise est de laisser les opérations asynchrones renvoyer un objet Promise

Trois états et deux modes de changement

Les objets Promise n'ont que trois états.

  • Opération asynchrone "en attente"

  • Opération asynchrone "résolue" (également appelée remplie)

  • Fonctionnement asynchrone "échoué" (rejeté)

Il n'y a que deux façons de changer ces trois états.

  • L'opération asynchrone passe de "incomplète" à "terminée"

  • L'opération asynchrone passe de "incomplète" à "échouée".

Ce changement ne peut se produire qu'une seule fois. Une fois que le statut actuel passe à "Terminé" ou "Échec", cela signifie qu'il n'y aura pas de nouveau changement de statut. Par conséquent, il n’y a que deux résultats finaux de l’objet Promise.

L'opération asynchrone est réussie, l'objet Promise renvoie une valeur et le statut passe à résolu.

L'opération asynchrone échoue, l'objet Promise génère une erreur et le statut passe à rejeté.

Générer un objet Promise

Générer un objet Promise via une nouvelle Promise :

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

Le constructeur Promise accepte une fonction comme paramètre, et les deux paramètres de la fonction sont résolus et rejeter. Il s'agit de deux fonctions fournies par le moteur JavaScript et qui n'ont pas besoin d'être déployées par vous-même.

resolve changera l'état de l'objet Promise de en attente à résolu, et rejet changera l'état de l'objet Promise de en attente à rejeté.

Le constructeur Promise exécutera la fonction immédiatement après avoir accepté une fonction

var promise = new Promise(function () {    console.log('Hello World')
})// Hello World
Copier après la connexion

then和catch回调

Promise对象生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。第二个函数是可选的。分别称之为成功回调和失败回调。成功回调接收异步操作成功的结果为参数,失败回调接收异步操作失败报出的错误作为参数。

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve('成功')
    }, 3000)
})

promise.then(function (data){    console.log(data)
})// 3s后打印'成功'
Copier après la connexion

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

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        reject('失败')
    }, 3000)
})

promise.catch(function (data){    console.log(data)
})// 3s后打印'失败'
Copier après la connexion

Promise.all()

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

var p = Promise.all([p1, p2, p3])
Copier après la connexion

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成resolved,p的状态才会变成resolved,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被Rejected,p的状态就变成Rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race()

与Promise.all()类似,不过是只要有一个Promise实例先改变了状态,p的状态就是它的状态,传递给回调函数的结果也是它的结果。所以很形象地叫做赛跑。

Promise.resolve()和Promise.reject()

有时需要将现有对象转为Promise对象,可以使用这两个方法。

Generator(生成器)

是什么

生成器本质上是一种特殊的迭代器(参见本文章系列二之Iterator)。ES6里的迭代器并不是一种新的语法或者是新的内置对象(构造函数),而是一种协议 (protocol)。所有遵循了这个协议的对象都可以称之为迭代器对象。生成器对象由生成器函数返回并且遵守了迭代器协议。具体参见MDN。

怎么用

执行过程

生成器函数的语法为function*,在其函数体内部可以使用yield和yield*关键字。

function* gen(x){  console.log(1)  var y = yield x + 2  console.log(2)  return y
}var g = gen(1)
Copier après la connexion

当我们像上面那样调用生成器函数时,会发现并没有输出。这就是生成器函数与普通函数的不同,它可以交出函数的执行权(即暂停执行)。yield表达式就是暂停标志。

之前提到了生成器对象遵循迭代器协议,所以其实可以通过next方法执行。执行结果也是一个包含value和done属性的对象。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。

g.next() 
// 1// { value: 3, done: false }
g.next() 
// 2// { value: undefined, done: true }
Copier après la connexion

for...of遍历

生成器部署了迭代器接口,因此可以用for...of来遍历,不用调用next方法

function *foo() {  yield 1  yield 2  yield 3  return 4
}for (let v of foo()) {  console.log(v)
}// 1// 2// 3
Copier après la connexion

yield*表达式

从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield表达式。yield后面只能跟迭代器,yield*的功能是将迭代控制权交给后面的迭代器,达到递归迭代的目的

function* foo() {  yield 'a'  yield 'b'
}function* bar() {  yield 'x'  yield* foo()  yield 'y'
}for (let v of bar()) {  console.log(v)
}// x// a// b// y
Copier après la connexion

自动执行

下面是使用Generator函数执行一个真实的异步任务的例子:

var fetch = require('node-fetch')function* gen () {  var url = 'https://api.github.com/users/github'  var result = yield fetch(url)  console.log(result.bio)
}
Copier après la connexion

上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从JSON格式的数据解析信息。这段代码非常像同步操作,除了加上了yield命令。

执行这段代码的方法如下

var g = gen()
var result = g.next()

result
  .value
  .then(function (data) {    return data.json()
  })
  .then(function (data) {
    g.next(data)
  })
Copier après la connexion

上面代码中,首先执行Generator函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法。

可以看到,虽然Generator函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

那么如何自动化异步任务的流程管理呢?

Generator函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

  1. 回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权。

  2. Promise对象。将异步操作包装成Promise对象,用then方法交回执行权。

Thunk函数

本节很简略,可能会看不太明白,请参考Thunk 函数的含义和用法

Thunk函数的含义:编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。

JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。

任何函数,只要参数有回调函数,就能写成Thunk函数的形式,可以通过一个Thunk函数转换器来转换。

Thunk函数真正的威力,在于可以自动执行Generator函数。我们可以实现一个基于Thunk函数的Generator执行器,然后直接把Generator函数传入这个执行器即可。

function run(fn) {  
    var gen = fn()  
function next(err, data) {    
    var result = gen.next(data)    
    if (result.done) return
    result.value(next)
  }

  next()
}function* g() {  // ...
}

run(g)
Copier après la connexion

Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制Generator函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise对象也可以做到这一点。

基于Promise对象的自动执行

首先,将方法包装成一个Promise对象(fs是nodejs的一个内置模块)。

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())
}
Copier après la connexion

然后,手动执行上面的Generator函数。

var g = gen()

g.next().value.then(function (data) {
  g.next(data).value.then(function (data) {
    g.next(data)
  })
})
Copier après la connexion

观察上面的执行过程,其实是在递归调用,我们可以用一个函数来实现:

function run(gen){
  var g = gen()  function next(data){
    var result = g.next(data)    if (result.done) return result.value
    result.value.then(function(data){
      next(data)
    })
  }

  next()
}

run(gen)
Copier après la connexion

上面代码中,只要Generator函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。

co模块

co模块是nodejs社区著名的TJ大神写的一个小工具,用于Generator函数的自动执行。

下面是一个Generator函数,用于依次读取两个文件

var gen = function* () {  
var f1 = yield readFile('/etc/fstab')  
var f2 = yield readFile('/etc/shells')  
console.log(f1.toString())  
console.log(f2.toString())
}var co = require('co')
co(gen)
Copier après la connexion

co模块可以让你不用编写Generator函数的执行器。Generator函数只要传入co函数,就会自动执行。co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function () {  
    console.log('Generator 函数执行完成')
})
Copier après la connexion

co模块的原理:其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。如果数组或对象的成员,全部都是Promise对象,也可以使用co(co v4.0版以后,yield命令后面只能是Promise对象,不再支持Thunk函数)。

async(异步)函数

是什么

async函数属于ES7。目前,它仍处于提案阶段,但是转码器Babel和regenerator都已经支持。async函数可以说是目前异步操作最好的解决方案,是对Generator函数的升级和改进。

怎么用

1)语法

async函数声明定义了异步函数,它会返回一个AsyncFunction对象。和普通函数一样,你也可以定义一个异步函数表达式。

调用异步函数时会返回一个promise对象。当这个异步函数成功返回一个值时,将会使用promise的resolve方法来处理这个返回值,当异步函数抛出的是异常或者非法值时,将会使用promise的reject方法来处理这个异常值。

异步函数可能会包括await表达式,这将会使异步函数暂停执行并等待promise解析传值后,继续执行异步函数并返回解析值。

注意:await只能用在async函数中。

前面依次读取两个文件的代码写成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())
}
Copier après la connexion

async函数将Generator函数的星号(*)替换成了async,将yield改为了await。

2)async函数的改进

async函数对Generator函数的改进,体现在以下三点。

(1)内置执行器。Generator函数的执行必须靠执行器,所以才有了co函数库,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

var result = asyncReadFile()
Copier après la connexion

(2)更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

3)基本用法

同Generator函数一样,async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

function resolveAfter2Seconds (x) {  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x)
    }, 2000)
  })
}async function add1 (x) {  var a = resolveAfter2Seconds(20)  var b = resolveAfter2Seconds(30)  return x + await a + await b
}

add1(10).then(v => {  console.log(v)  
})// 2s后打印60async function add2 (x) {  var a = await resolveAfter2Seconds(20)  var b = await resolveAfter2Seconds(30)  return x + a + b
}

add2(10).then(v => {  console.log(v)
})// 4s后打印60
Copier après la connexion

4)捕获错误

可以使用.catch回调捕获错误,也可以使用传统的try...catch。

async function myFunction () {  
    try {    
    await somethingThatReturnsAPromise()
  } catch (err) {    
  console.log(err)
  }
}// 另一种写法async function myFunction () {  await somethingThatReturnsAPromise()
  .catch(function (err) {    console.log(err)
  }
}
Copier après la connexion

5)并发的异步操作

let foo = await getFoo()let bar = await getBar()
Copier après la connexion

多个await命令后面的异步操作会按顺序完成。如果不存在继发关系,最好让它们同时触发。上面的代码只有getFoo完成,才会去执行getBar,这样会比较耗时。如果这两个是独立的异步操作,完全可以让它们同时触发。

// 写法一let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 写法二let fooPromise = getFoo()let barPromise = getBar()let foo = await fooPromiselet bar = await barPromise
Copier après la connexion

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal