学了那么久JavaScript还不知道异步怎么实现?

JavaScript中的异步编程

ES6 诞生以前,异步编程的方法,大概有下面四种。

回调函数、事件监听、发布/订阅、Promise 对象。

注意这里的Promise对象和ES6里的Promise其实不太一样,因为之前的Promise是由commonJS社区提出的Promise规范,用于统一处理异步回调,之后ECMAscript 6 才原生提供了 Promise 对象。

在ES6中,主要涉及到的异步有Promise对象,Generator函数和async函数。

所以本文就介绍这六种异步实现方法。

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
  • Generator函数
  • async函数

1.回调函数

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.

回调是作为参数传递给另一个函数并在其父函数完成后执行的函数。

回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:

ajax(url, () => {
    // 处理逻辑
})

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

ajax('xxxxxxx.json', function() {
  // doing something 1
  ajax('xxxxxxx.json', function() {
    // doing something 2
    ajax('xxxxxxx.json', function() {
      // doing something 3
      ajax('xxxxxxx.json', function() {
        // doing something 4
      });
    });
  });
});

这是是单纯的嵌套代码,如若再加上业务代码,代码可读性可想而知,如果是开发起来还好,但是后期的维护和修改的难度足以让人疯掉。这就是那个**JQuery时代的“回调地狱”**问题。

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

2.事件监听

另一种思路是采用事件驱动模式。

异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

f1f2为例。首先,为f1绑定一个事件(这里采用的 jQuery 的写法)。

f1.on('done', f2);

上面这行代码给f1注册了done事件,当f1发生done事件,就执行f2。然后,对f1进行改写:

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

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

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

3.发布/订阅

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

发布订阅其实和事件监听非常相似,事件监听的on相当于就是订阅,事件的发生就是发布。

如果不太了解的可以看我的另一篇博客,观察者模式

但是观察者模式一般是用在同步任务中的,发布订阅模式则是异步。

// 主体
let subject = new Subjet();
// 像向体订阅getData通知
subject.subscribe('getData', function(data) {
  // do something...
});

// 获取数据方法
function getDate(params) {
  // do something...
  //   获取数据后主题发布getData通知
  subject.publish('getData');
}

4.Promise对象和then

Promise详细文档请阅读:阮一峰老师的ES6入门:Promise

为了解决“回调地狱”问题,提出了Promise对象,并且后来加入了ES6标准,Promise对象简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

Promise的状态

Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。

Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。

因此,Promise 的最终结果只有两种。

  • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled
  • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected

Promise

首先,Promise 是一个对象,也是一个构造函数。

var f1 = function(resolve, reject) {
  // ... some code
 
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
}
const promise1 = new Promise(f1);

Promise构造函数接受一个回调函数f1作为参数,f1里面是异步操作的代码。然后,返回的promise1就是一个 Promise 实例。

Promise 新建后就会立即执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

注意,调用resolvereject并不会终结 Promise 的参数函数的执行。

then

Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。它的作用是为 Promise 实例添加状态改变时的回调函数。

then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // proceed
});

上面代码中,getJSON的异步操作执行完成,就会执行then的回调函数。

通过 .then 形式添加的回调函数,不论什么时候,都会被调用。

then方法会返回一个新的Promise实例(注意,不是原来那个Promise实例)。

当然也可以自己返回一个Promise对象,达到异步操作的逐步执行的效果。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // 对comments进行处理
});

如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。因此可以采用链式写法,即then方法后面再调用另一个then方法。

catch

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

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

上面代码中,promise抛出一个错误,就被catch()方法指定的回调函数捕获。注意,上面的两种写法是等价的。reject()方法的作用,等同于抛出错误。

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise.then(function(data) { //cb
	// success
}).catch(function(err) {
	// error
});

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

5.Generator和yield

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。 基本用法

Generator 、yield

Generator 有两个区分于普通函数的部分:

  • 一是在 function 后面,函数名之前有个 * ;
  • 函数内部有 yield 表达式。

其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。

function* func(){
 console.log("one");
 yield '1';
 console.log("two");
 yield '2'; 
 console.log("three");
 return '3';
}

调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。

next和return

next方法的作用就是,使当前的Generator 函数返回的Iterator对象从当前指向的状态继续执行,遇到yield或者return就会停止。

next ()函数也可以传入参数,next 传入参数的时候,该参数会作为上一步yield的返回值;如果不提供参数,那么上一步yield的返回值为undefined。

yield的返回值的定义就是x = yield '1';这段代码中x的值。但是它是作为上一步的yield的返回值,你需要尝试去理解。调用next函数 遇到yield会挂起,但是不是这个yield,而是上一个yield。

第一个yield没有上一个,所以第一个next的赋值其实是没有意义的。

return 方法提供参数时,返回该参数;不提供参数时,返回 undefined 。

function* func(){
    console.log("one");
    yield '1';
    console.log("two");
    yield '2'; 
    console.log("three");
    return '3';
   }
f=func();
console.log(f.next());
// one
// {value: "1", done: false}
 
console.log(f.next());
// two
// {value: "2", done: false}

console.log(f.next());
// three
// {value: "3", done: true}

console.log(f.next());
// {value: undefined, done: true}

console.log(f.return(1000));
// { value: 1000, done: true }

//完整运行结果
// one
// { value: '1', done: false }
// two
// { value: '2', done: false }
// three
// { value: '3', done: true }
// { value: undefined, done: true }
// { value: 1000, done: true }

第一次调用 next 方法时,从 Generator 函数的头部开始执行,先是打印了 one ,执行到 yield 就停下来,并将yield 后边表达式的值 ‘1’,作为返回对象的 value 属性值,此时函数还没有执行完, 返回对象的 done 属性值是 false。

第二次调用 next 方法时,同上步 。

第三次调用 next 方法时,先是打印了 three ,然后执行了函数的返回操作,并将 return 后面的表达式的值,作为返回对象的 value 属性值,此时函数已经结束,多以 done 属性值为true 。

第四次调用 next 方法时, 此时函数已经执行完了,所以返回 value 属性值是 undefined ,done 属性值是 true 。如果执行第三步时,没有 return 语句的话,就直接返回{value: undefined, done: true}

第五步调用return方法时,此时函数已经执行完了,本来返回 value 属性值是 undefined,但是return方法提供一个参数之后,就会返回这个参数,所以最后输出的value就是传入的1000

除了使用 next ,还可以使用 for… of 循环遍历 Generator 函数生产的 Iterator 对象

yield* 表达式

yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数。

这里就不过多描述了。

6.async和await

async

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。和 Promise , Generator 有很大关联的。

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

async function name([param[, param[, ... param]]]) { statements }
  • name: 函数名称。
  • param: 要传递给函数的参数的名称。
  • statements: 函数体语句。

async 返回值

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。

async function helloAsync(){
    return "helloAsync";
  }
  
console.log(helloAsync())  // Promise {<resolved>: "helloAsync"}
 
helloAsync().then(v=>{
   console.log(v);         // helloAsync
})

await

await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。

[return_value] = await expression;

async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。

await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误

function testAwait (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}
 
async function helloAsync() {
  var x = await testAwait ("hello world");
  console.log(x); 
}
helloAsync ();
// hello world

await 返回值

await根据后面不同表达式有不同的处理方式:

  • Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。

  • 非 Promise 对象:直接返回对应的值。

正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数

function testAwait(){
    console.log("testAwait");
 }
 async function helloAsync(){
    await testAwait();
    console.log("helloAsync");
 }
 helloAsync();
 // testAwait
 // helloAsync

Promise,Generator函数,async函数理解

关于JavaScript的简单异步编程价绍就这些,说实话我自己看了很久,感觉Promise,Generator函数,async函数是层层递进的。

Promise比较简单,也是最常用的,主要就是将原来的用回调函数的异步编程方法转成用relsove和reject触发事件, 用then和catch捕获成功或者失败的状态执行相应代码的异步编程的方法。

Generator函数可以看成是一个分步函数,只有主动调用next() 才能进行下一步。

async函数相当于自执行的Generator函数,会自己不断的执行,遇到await等待返回结果,然后继续,和普通的同步写法相同,提供效率。

关于三者区别我也专门整理了一下,想了解的可以看阅读。

不能区分Promise,Generator函数,async函数可以进来看看

参考

https://www.ruanyifeng.com/blog/2015/04/generator.html

http://es6.ruanyifeng.com/#docs/promise

https://blog.csdn.net/aaaaaaliang/article/details/89555422

上一篇:Python协程与JavaScript协程的对比


下一篇:PHP生成器 yield