js总结之异步机制

120 阅读5分钟

【异步机制】

javascript提供了丰富的异步机制提升处理性能,大概有下面几种:

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
  • Generator 函数 其中最后两种是ES6提出的语法,也是目前很常用的异步方案。此外还有基于Generator 的语法糖 async/await关键字,也非常好用。

Promise

简单说就是一个容器对象,里面保存着某个未来才会结束的事件(通常是一个异步操作)的状态,状态包含已完成、已失败,在定义Promise时,需要注明什么情况下对应什么状态,并调用相应的回调方法,在使用Promise时则要通过.then和.catch方法分别传入已完成和已失败状态的回调方法(建议用.catch代替.then可传入的reject方法)。其好处是统一了语法格式,更好的语义,避免回调地域。

我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:

function test(resolve, reject) {
    var timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}

这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve('200 OK'),如果执行失败,我们将调用reject('timeout in ' + timeOut + ' seconds.')。可以看出,test()函数只关心自身的逻辑,并不关心具体的resolve和reject将如何处理结果。

有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:

var p1 = new Promise(test);
var p2 = p1.then(function (result) {
    console.log('成功:' + result);
});
var p3 = p2.catch(function (reason) {
    console.log('失败:' + reason);
});

变量p1是一个Promise对象,它负责执行test函数。注意,在new Promise时,test函数就已经开始执行了,通常会在该函数中执行一些异步逻辑并在异步完成时调用resole/reject从而执行绑定的回调函数(此时早已退出new Promise的执行过程)。正是基于这种语法逻辑,使用Promise可以避免多层异步操作的回调地域。

由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:

// 如果成功,执行这个函数:
p1.then(function (result) {
    console.log('成功:' + result);
});
当test函数执行失败时,我们告诉Promise对象:
 
p2.catch(function (reason) {
    console.log('失败:' + reason);
});

// Promise对象可以串联起来,所以上述代码可以简化为:
new Promise(test).then(function (result) {
    console.log('成功:' + result);
}).catch(function (reason) {
    console.log('失败:' + reason);
});

Promise.all

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 获得一个Array: ['P1', 'P2']
});

Promise.race

// 有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。或者增加一个Promise指定超时时间。这种情况下,用Promise.race()实现:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P1'
});
// 由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。

    Util.loading();
    Promise.race([
      window.fetch(this.qryServer.url, {
        method: "POST",
        headers: {
          "mode": "cors",
          "Content-Type": "application/json",
          "Authorization": this.qryServer.authorization
        },
        cache: 'no-cache',
        body: JSON.stringify({
          "uid": this.telUid,
          "calledNbr": phone
        })
      }),
      new Promise((resolve, reject) => {
        setTimeout(reject, 20 * 1000, new Error('服务连接超时'));
      })
    ]).then(res => {
      if (res.status !== 200) {
        reject(new Error(`Response Error: ${res.statusText}`));
      }else {
        return res.json();
      }
    }).then(data => {
      if (data.code === "200") {
        this.setState({
          voiceRecords: data.data
        });
      } else {
        Util.alert("查询失败" + data.msg);
      }
    }).catch(err => {
      console.error(err);
      Util.alert(err.message);
    }).finally(Util.unloading);

如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。

Generator

简单说就是一个状态机,封装了多个内部状态并提供一个遍历器对象。形式上,Generator 是一个带关键字 * 的函数,内部通过yield定义内部状态。调用 Generator 函数后将暂停执行,等待调用next方法一步一步的执行并返回相应状态。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

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,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。(注意表达式已求值,基于此特性可完成异步操作) 第二次调用next ,从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。 第三次调用next ,继续执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。 第四次调用next ,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。通过next方法的参数向函数体内部注入值,从而调整函数行为。 上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

由于生成器函数内部有遍历器对象,因此可以用for、while循环自动调用next执行,这个特性可以用来自动执行。

对比各种异步机制

// **异步回调**
const fs = require('fs');
fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

// **Promise实现的异步**
const fs = require('fs');
const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

// **生成器实现的异步**
// 好处是可以从外部读取yield值并且通过next传入参数
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// **async/await语法糖实现的异步**
// 好处是简洁直观,异步操作像串行执行一样
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};