JavaScript 异步编程

前言

JavaScript 是一门单线程语言,这意味着它一次只能处理一个任务。这对于处理一些需要等待时间较长的任务是一个很大的问题,比如读写文件、网络请求等。为了解决这个问题,JavaScript 引入了异步编程。

JavaScript 中的异步编程可以通过回调函数、Promise、Generator、async/await、 Async Generator 和 Reactive programming 等方式实现。

回调函数

概念

回调函数是最基本的异步编程方式。通过将一个回调函数作为参数传递给另一个函数,或将一个回调函数注册为事件监听器,最后在回调函数中处理业务逻辑即可完成异步操作。

下面是一个使用回调函数实现异步 HTTP 请求操作的示例代码:

function fetchData(callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', '<https://api.github.com/users/github>');
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(null, xhr.response);
    } else {
      callback(xhr.statusText);
    }
  };
  xhr.onerror = function() {
    callback(xhr.statusText);
  };
  xhr.send();
}

fetchData(function(error, data) {
  if (error) {
    console.log(error);
  } else {
    console.log(JSON.parse(data));
  }
});

在上面的代码中,我们定义了一个函数 fetchData,它使用 XMLHttpRequest 发送一个 GET 请求,并在请求完成后调用注册为事件监听器的回调函数。在事件监听器回调函数中,我们通过判断 HTTP 响应的状态码来处理异步操作的结果,并将结果作为参数传递给回调函数。通过调用 fetchData 函数来获取 GitHub 用户的信息,并在回调函数中处理数据。

错误处理

由于在异步任务中,执行回调函数时任务所在的上下文环境已经结束了。在其之后抛出的错误,原始的上下文环境已经无法捕捉,无法输出有效的错误信息,只能当作参数传入回调函数。

对于回调函数作为事件监听器,通过将回调函数注册给onerror事件,即可在回调函数中处理异常逻辑。

对于回调函数作为函数参数,Nodejs 约定回调函数的第一个参数,必须是错误对象err,通过判断 err 是否为null来处理异常逻辑。

优点

回调函数是一种基础的异步编程方式,非常简单灵活。

缺点

回调函数容易出现回调地狱(callback hell):

  • 多层嵌套代码可读性差。

  • 多个异步操作会形成强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。

Promise

概念

Promise 是一种用于处理异步操作的对象。通过使用 Promise,可以将回调函数的嵌套,改成链式调用,更加清晰地处理异步操作的结果。

Promise 有三种状态:pendingfulfilledrejected。当 Promise 对象刚创建时,它的状态是 pending。当异步操作成功完成时,Promise 的状态会变为 fulfilled,这时会将异步操作的结果作为参数传递给 then 方法的回调函数。当异步操作发生错误时,Promise 的状态会变为 rejected,并将错误信息作为参数传递给 catch 方法。

Promise 对象的状态不受外界影响,从外部无法改变Promise的状态。

Promise 对象的状态一旦改变,就会永久保持该状态,并存储异步操作的结果。

下面是一个使用 Promise 实现异步 HTTP 请求操作的示例代码:

function fetchData() {
  fetch('<https://api.github.com/users/github>')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.log(error));
}

fetchData();

在上面的代码中,我们定义了一个函数 fetchData,它使用 fetch 方法来获取 GitHub 用户的信息。在第一个 then 方法中,我们使用 response.json 方法来解析响应的数据,并在下一个 then 方法中处理数据。在 catch 方法中,处理异步操作的错误。

Promise 的常用方法

  • Promise.prototype.then()为 Promise 实例添加状态改变时的回调函数。第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。它返回一个新的Promise实例,这个新Promise的结果根据then的回调函数返回值决定:

    • 返回一个值:新Promise的状态为 fulfilled,这个值作为结果。

    • 没有返回任何内容:新Promise的状态为 fulfilled,undefined作为结果。

    • 抛出一个错误:新Promise的状态为 rejected,抛出的错误作为结果。

    • 返回一个Promise实例:直接作为新的Promise实例返回。

  • Promise.prototype.catch()then(undefined, rejection)的别名,用于指定发生错误时的回调函数。它也返回一个新的Promise实例,同Promise.prototype.then()方法。

  • Promise.prototype.finally()用于指定不管 Promise 对象最后状态如何,都会执行的操作。它的回调函数不接受任何参数。如果它的回调函数抛出 error 或返回rejected promise,它会返回新的rejected promise,否则默认会直接返回原来的Promise

    const promise = new Promise(function(resolve, reject) {
        resolve('ok');
    });
    promise
        .finally(() => { console.log('finally') })
        .then(function(value) { console.log(value) })
        .catch(function(error) { console.error(error) });
  • Promise.resolve()用于将其参数转换为 Promise 对象。其参数分为三种情况:

    • 如果参数是 Promise 实例,那么Promise.resolve将直接返回这个实例。

    • 如果参数是具有then(onFulfilled, onRejected)方法的对象,Promise.resolve会用这个对象的 then 方法创建新的 Promise 对象,然后返回新的 Promise 对象。

      const p1 = Promise.resolve({
        then(onFulfill, onReject) {
          onFulfill("fulfilled!");
        }
      });
      
      p1.then( (v) => {
          console.log(v); // "fulfilled!"
        }
      );
    • 对于其他情况,Promise.resolve方法会返回一个新的状态为resolvedPromise 对象,结果就为其参数。

  • Promise.reject()会返回一个新的状态为rejectedPromise 实例,其参数为失败的原因,之后直接传递给catch方法。

  • Promise.all()接受元素为Promise实例的数组作为参数(如果不是Promise实例,会调用Promise.resolve方法进行转换),返回一个新的 Promise 实例。新的 Promise 实例的状态根据参数数组中Promise实例的状态决定:

    • 如果数组中实例的状态都变成fulfilled,则新的 Promise 实例的状态就变为fulfilled

    • 如果数组中的实例有一个变为rejected,新 Promise 实例的状态就变为rejected

  • Promise.any()接受 Promise 实例数组作为参数,返回一个新的 Promise 实例。

    • 如果数组中的实例有一个变成fulfilled状态,新 Promise 实例就会变成fulfilled状态。

    • 如果数组中的所有实例都变成rejected状态,新 Promise 实例就会变成rejected状态。

  • Promise.race()接受 Promise 实例数组作为参数,返回一个新的 Promise 实例。新 Promise 实例的状态和数组中最先结束的实例的状态一致。

  • Promise.allSettled()接受 Promise 实例数组作为参数,返回一个新的 Promise 实例。只有等到数组中所有实例都返回结果,不管是fulfilled还是rejected,新 Promise 实例才会结束。一旦结束新 Promise 实例总为fulfilled,结果为{ status: 'fulfilled', value?: value, reason?: 1}对象组成的数组。常用于不关心异步操作的结果,只关心这些操作有没有结束的情况。

错误处理

  • Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。如果一直未捕获,传递到外层,成为未捕获错误,影响程序运行。一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。

  • 如果 Promise 状态已经变成resolved,再抛出错误不会产生任何效果。

    const promise = new Promise(function(resolve, reject) {
      resolve('ok');
      throw new Error('test');
    });
    promise
      .then(function(value) { console.log(value) })
      .catch(function(error) { console.log(error) });
    // ok
  • then()方法指定的回调函数,如果运行中抛出错误,也会被之后的catch()方法捕获。

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

    // bad
    promise.then(function() {
    // 下面一行会报错,因为x没有声明
      x + 2;
      console.log('everything is great');
    }, function(err) {
        console.log(err);
     });
     
     // good
    promise
      .then(function(data) { //cb
        // 下面一行会报错,因为x没有声明
          x + 2;
          console.log('everything is great');
      })
      .catch(function(err) {
        console.log(err);
      });
  • catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。如果没有报错,则会跳过catch()方法。

    Promise.resolve()
    .catch(function(error) {
      console.log('oh no', error);
    })
    .then(function() {
      console.log('carry on');
    });

优点

  • Promise对象可以将异步操作以近似同步操作的形式表达出来,避免了层层嵌套的回调函数。

  • Promise对象提供统一的接口,使得控制异步操作更加容易。

  • Promise 会缓存 resolve 或 reject 的结果,每当它的 then() 被调用,都会将保存的结果返回给 then() 的回调函数。

缺点

  • 最大问题是代码冗余,所有任务都需要被 Promise 包装,不管什么操作,一眼看去都是一堆then,语义变得不清楚。

  • 一旦建立Promise就会立即执行,无法中途取消。

  • 当处于pending状态时,无法得知目前任务的进展。

Generator

概念

Generator 是一种可用于异步编程的函数,它可以在不阻塞主线程的情况下暂停和恢复执行。通过使用 yield 关键字,我们可以将异步操作分成多个步骤进行处理。生成器的形式为:

function* name(params) {
   yield statements
}

调用Generator 函数后,该函数并不执行,而是返回一个迭代器对象,我们可以在迭代器对象上使用 next 方法来控制函数的执行流程。每次调用 next 方法时,函数会执行到下一个 yield 关键字处,并返回对象 { value: 'yield 后的值', done: false }。当函数执行完最后一个 yield 关键字时,返回对象的 done 属性会变为 true,表示遍历结束。

下面是一个 Generator 函数的示例代码:

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 }

在上面的代码中,我们定义了一个 Generator 函数 helloWorldGenerator,它有两个 yield 表达式。然后调用helloWorldGenerator函数获得迭代器对象 hw。最后迭代器对象调用了四次next方法。

  • 第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。返回 { value: 'hello', done: false }

  • 第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。返回{ value: 'world', done: false }

  • 第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句。返回{ value: 'ending', done: true }valuereturn语句后的表达式的值,done: true 表示遍历已经结束。

  • 第四次调用,此时 Generator 函数已经运行完毕,返回{ value: undefined, done: true }。以后再调用next方法,返回的都是这个值。

Generator 的常用方法

  • Generator.prototype.next():可以接收一个参数,作为上一个yield表达式的返回值。

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var b = foo(5);
    b.next(1) // { value:6, done:false }
    b.next(12) // { value:8, done:false }
    b.next(13) // { value:42, done:true }
  • Generator.prototype.throw():用于在Generator函数体外抛出错误,然后在 Generator 函数体内捕获。该方法可以接受一个参数,参数会被catch语句接收,

    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log('内部捕获', e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 内部捕获 a
    // 外部捕获 b
  • Generator.prototype.return():返回给定的值,并且终结遍历 Generator 函数。

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    var g = gen();
    g.next()        // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next()        // { value: undefined, done: true }

Generator 函数的异步编程 —— 协程

协程(coroutine)是一种基于线程之上,由程序员自己写程序来管理的轻量级线程。一个线程可以拥有多个协程。

多个协程互相切换,就可以完成异步任务。

Generator 函数可用于在 JavaScript 中实现协程,因为其可以暂停执行和恢复执行。

使用 Generator 函数,执行一个真实的异步任务:

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result);
}

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

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

上面代码中,Generator 函数封装了一个异步操作,首先执行 Generator 函数,获取迭代器对象g,然后使用g.next()方法,执行异步任务。由于fetch返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

可以看到,Generator 函数中异步操作非常像同步操作,除了加上了yield命令。但其执行却非常复杂,可以使用co 模块使其自动执行。重写上面的例子:

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result);
}

var co = require('co');
// co 函数接受 Generator 函数作为参数,返回一个 Promise 对象。
co(gen).then(function (){
  console.log('Generator 函数执行完成');
});

错误处理

  • 如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部的try...catch代码块捕获。

    var g = function* () {
      while (true) {
        yield;
        console.log('内部捕获', e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 外部捕获 a
  • 如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

  • 迭代器对象的throw方法抛出的错误要被内部捕获,必须至少执行过一次next方法。否则,调用throw方法会直接在外部抛出错误,导致程序中断。

  • throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会自动执行一次next方法。

    var gen = function* gen(){
      try {
        yield console.log('a');
      } catch (e) {
        // ...
      }
      yield console.log('b');
      yield console.log('c');
    }
    var g = gen();
    g.next() // a
    g.throw() // b
    g.next() // c
  • Generator 函数体内抛出的错误,也可以被函数体外的catch语句捕获。

    function* foo() {
      var x = yield 3;
      var y = x.toUpperCase();
      yield y;
    }
    var it = foo();
    it.next(); // { value:3, done:false }
    try {
      it.next(42);
    } catch (err) {
      console.log(err);
    }
  • 如果 Generator 函数内部有try...finally代码块,并且执行在try代码块中,那么调用return方法会立刻进入finally代码块,finally代码块执行完以后,整个函数才会结束。

优点

在 Generator 函数内部,可以用同步的方式编写异步代码,因此语义上比 Promise 写法更清晰。

缺点

需要一个运行器,自动执行 Generator 函数,会引入与项目不相关的代码。

Async/Await

概念

async/await 是 ES2017 引入的一种异步编程方式,可以让异步编程更加简单和直观。它就是 Generator 函数的语法糖,将 Generator 函数的星号(*)替换成async,将yield替换成await,并且它自带执行器,仅此而已。

async/await 的基本使用方式是在函数前加上 async 关键字,表示该函数是一个异步函数。调用该函数时,会立即返回一个 Promise 对象,可以使用then方法添加回调函数。在函数内部,await 关键字后面指定异步操作的 Promise 对象。当执行到 await 关键字时,函数会暂停执行,直到异步操作返回结果。在异步操作返回结果后,该函数会自动继续执行。

正常情况下,await命令后面是一个 Promise 对象。如果不是 Promise 对象,会自动调用 Promise.resolve()将其转化为 Promise 对象。

async函数返回的 Promise 对象,必须等到内部所有的await后的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。

async函数内部return语句返回的值,会成为它返回的 Promise 对象的then方法回调函数的参数。

async 函数有多种使用形式:

// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }
  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};

下面是一个使用 async/await 处理异步操作的示例代码:

async function fetchData() {
  const response = await fetch('<https://api.github.com/users/github>');
  const data = await response.json();
  console.log(data);
}

fetchData();

在上面的代码中,我们定义了一个异步函数 fetchData,它使用 await 调用了 fetch response.json 方法,来获取 GitHub 用户的信息。

错误处理

  • 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。reject的参数会被catch方法的回调函数接收到。

    async function f() {
      await Promise.reject('出错了');
      await Promise.resolve('hello world'); // 不会执行
    
    }
    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // 出错了
  • 如果希望异步操作失败,也不中断后续的操作。可以将await放在try...catch结构里面,或让await后面的 Promise 对象再跟一个catch方法。

    async function f() {
      try {
        await Promise.reject('出错了');
      } catch(e) {
      }
      return await Promise.resolve('hello world');
    }
    // 或
    async function f() {
      await Promise.reject('出错了')
        .catch(e => console.log(e));
      return await Promise.resolve('hello world');
    }
  • async函数内部抛出错误,也会导致它返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法的回调函数接收到。

注意事项

  • await命令只能用在async函数或顶层代码中,如果用在普通函数,就会报错。

  • 对于多个await命令的异步操作,如果不存在顺序关系,最好使用Promise.all让它们并行触发。

    // bad
    let foo = await getFoo();
    let bar = await getBar();
    // good
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

优点

  • 最大的优点就是使用同步代码风格处理异步程序。

缺点

  • async/await只是语法糖,其本质还是 Generator 函数和 Promise。在正确使用 async 之前,需要理解 Promise。

  • async函数内部异步任务的reject处理比较困难,需要使用 try-catch 块进行捕获和处理。

  • 随意使用 await 可能会导致性能问题,因为 await 会阻塞代码,如:注意事项的第二点。

Async Generator

概念

Async Generator 是一种结合了 Generator 和 Async 的异步编程方式。调用AsyncGenerator会返回一个异步迭代器对象。

调用异步迭代器的next方法,返回的是一个 Promise 对象。该 Promise 对象 resolve 的结果为{ value: '异步调用的值', done: false }

AsyncGenerator 中yield命令后面应该是一个 Promise 对象。如果yield命令后面不是一个Promise 对象,会调用 Promise.resolve()自动包装成一个 Promise 对象。

下面是一个使用 Async Generator 处理异步数据流的示例代码:

async function* asyncGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

(async function() {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
})();

在上面的代码中,我们定义了一个 Async Generator 函数 asyncGenerator,它返回一个异步迭代器。在这个异步迭代器中,我们使用 yield 关键字来暂停函数的执行,并在每个 yield 语句后返回一个 Promise 对象。

之后,使用for await...of循环来遍历异步迭代器,它会自动调用异步迭代器的next方法。在每一次循环中,next 返回的Promise 对象一旦resolve,就可以通过 value 变量获取异步操作的结果。

for await...of

for...of循环用于遍历同步迭代器。而for await...of循环,则是用于遍历异步迭代器。

let body = '';
async function f() {
   // req是一个异步迭代器对象,用来异步读取数据。
  for await(const data of req) body += data;
  const parsed = JSON.parse(body);
  console.log('got', parsed);
}

for await...of循环只能用在 async 函数中。for await...of也可以用于同步迭代器。

(async function () {
  for await (const x of ['a', 'b']) {
    console.log(x);
  }
})();
// a
// b

for await...of中的异步迭代器的next方法返回的 Promise 对象变为reject状态,for await...of就会报错,需要用try...catch捕捉。

async function () {
  try {
    for await (const x of createRejectingIterable()) {
      console.log(x);
    }
  } catch (e) {
    console.error(e);
  }
}

错误处理

如果Async Generator 函数抛出错误,会导致next()返回的 Promise 对象的状态变为reject,然后抛出的错误会被器catch方法捕获。

async function* asyncGenerator() {
  throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!

用途

  • 异步迭代器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。

    // map的作用是将iterable每一步返回的值,使用func进行处理
    // 同步 Generator 函数
    function* map(iterable, func) {
      const iter = iterable[Symbol.iterator]();
      while (true) {
        const {value, done} = iter.next();
        if (done) break;
        yield func(value);
      }
    }
    // 异步 Generator 函数
    async function* map(iterable, func) {
      const iter = iterable[Symbol.asyncIterator]();
      while (true) {
        const {value, done} = await iter.next();
        if (done) break;
        yield func(value);
      }
    }
  • 如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以配合for await...of使用异步 Generator 函数。

    e// Nodejs v10 的 Stream 就部署了这个接口。
    async function main(inputFilePath) {
      const readStream = fs.createReadStream(
        inputFilePath,
        { encoding: 'utf8', highWaterMark: 1024 }
      );
      for await (const chunk of readStream) {
        console.log('>>> '+chunk);
      }
      console.log('### DONE ###');
    }

Reactive Programming

概念

Reactive Programming(响应式编程)是一种编程范式,它通过异步数据流来构建基于事件驱动的应用程序。在响应式编程中,数据流是由事件和事件序列组成的,应用程序会对这些事件和事件序列进行响应和处理。

观察者模式允许对象之间建立一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会得到通知并自动更新。因此,响应式编程可以看作是一种使用观察者模式来处理数据流的编程范式。数据流被看作是一个可观察的序列,它可以被多个观察者订阅并接收通知。

RxJS 库是响应式编程的 JavaScript 实现,用于处理异步数据流。它提供了一些基本概念: ObservableObserverOperatorsSubscriptionSubjectScheduler,以便于将异步事件作为集合进行处理。

Observable(可观察者)

Observable可以理解为返回多个值的事件流,observers可以订阅这个流。

返回单个值

返回多个值

同步 / pull

Object

Iterables (Array | Set | Map)

异步 / push

Promise

Observable

Observable可以通知其observers三件事情:

  • 发送一个值:subscriber.next()

  • 发送一个错误:subscriber.error()

  • 发送完成通知:subscriber.complete()

一个Observable可以发送多个值,但只能发送一次错误或完成通知。当Observable发送错误或完成之后,将不能再发送任何通知。

Observer(观察者)

ObserverObservable 发送通知的接收者。它只是具有三个回调函数的对象,分别对应于 Observable 传递的每种类型的通知。

// op1,op2,op3,op4 为 Pipeable Operators
observable.pipe(op1(), op2(), op3(), op4());

Observer 订阅 Observable的例子:

import { Observable } from 'rxjs';

const observable = new Observable(function subscribe(subscriber) {
    let count = 0;
    const id = setInterval(() => {
        subscriber.next(count++);
        if (count === 5) subscriber.complete();
    }, 1000);
});

 const observer = {next: (x) => console.log(x),}
observable.subscribe(observer);

setTimeout(() => {
    observable.subscribe((x) => console.log(x));
}, 5000)
/** 输出
0 1 2 3 4 0 1 2 3 4
**/

Operators(操作符)

多种多样的Operators 可以用于轻松处理复杂异步代码,正是 RxJS 的强大之处。

Operators就是函数,分为两种类型:创建操作符和管道操作符。

创建操作(Creation Operators)符用于创建新的 Observable,比如fromEventintervalof等。

管道操作符(Pipeable Operators)是一个以 Observable 作为输入并返回另一个 Observable 的函数。用于处理Observable事件流中的数据,比如mapfilterconcatMap等。管道操作符的调用形式为:

// op1,op2,op3,op4 为 Pipeable Operators 
observable.pipe(op1(), op2(), op3(), op4());

将上面的例子用Operators重写:

import { Observable, take } from 'rxjs';

const observable = interval(1000).pipe(
     take(5) 
);

observable.subscribe((x) => console.log(x));
/** 输出
0 1 2 3 4  
**/

Subscription(订阅)

Subscription 只有一个 unsubscribe() 方法,用来释放Observer的订阅。

import { interval } from 'rxjs';

const observable = interval(1000);
const subscription = observable.subscribe(x => console.log(x));

subscription.unsubscribe();

Subject

Subject可以认为是提供了一些便利功能的Observable,有:AsyncSubjectBehaviorSubjectReplaySubjectSubject

SubjectObservable的区别是它可以将值共享传播到多个 Observer

// 使用 Observable
import { from } from 'rxjs';
import { switchMap } from 'rxjs/operators';

const url = 'https://api.github.com/users/github';

const posts = from(fetch(url)).pipe(
  switchMap(response => response.json())
);

posts.subscribe(
  data => console.log(data),
  error => console.error(error)
);

// 使用 subject
import { Subject } from 'rxjs';

const url = 'https://api.github.com/users/github';

const posts = new Subject();

fetch(url)
  .then(response => response.json())
  .then(data => posts.next(data))
  .catch(error => posts.error(error));

posts.subscribe(
  data => console.log(data),
  error => console.error(error)
);

Scheduler(调度器)

Scheduler是创建Observable时的一个可选参数,用于控制 Observable 在何时开始发送通知以及在哪个线程上发送通知,优化异步操作的执行性能。调度器有多种类型:

  • Null: 不传递任何调度器,通知将以同步和递归方式传递。

  • queueScheduler:在当前事件框架中的队列上调度。可用于迭代操作。

  • asyncScheduler:使用 setInterval 的调度器。可用于基于时间的操作。

  • asapScheduler: as soon as possible 调度器,它的行为与 asyncScheduler 调度器相同。但是,会尝试尽快执行给定的任务。

  • animationFrameScheduler:调度任务将在下一次浏览器内容重绘之前发生。可用于创建流畅的浏览器动画。

如果不提供调度器,RxJS 会根据最少并发的原则选择一个默认的调度器。例如,对于返回具有有限且少量消息的 observable 操作符,RxJS 不使用调度器,即 null 。对于返回可能大量或无限数量的消息的操作符,会使用 queueScheduler。对于使用计时器的操作符,会使用 asyncScheduler。若为了性能的目的而引入并发,你可以选择一个不同的调度器。

项目实践

RxJS 用于快扫页面的部分代码:

const observable = new ReplaySubject().pipe(
              concatMap((imageId) => DynamsoftHelper.skewCorrection(imageId)),
              concatMap((index) => this.saveScanImageToIndexDB(index)),
              concatMap((image) => this.getPageNumberByQR(image)),
              concatMap((image) => this.getBase64Url(image)),
              bufferCount(2),
              map((images) => images.sort((a, b) => a.page - b.page)),
              concatMap((images) => this.getStudentInfo(images)),
              concatMap((images) => this.uploadToAliCloud(images)),
              concatMap((images) => this.saveToDisk(images)),
              concatMap((images) => this.uploadToServer(images)),
              concatMap((images) => this.updateImageToIndexDB(images)),
              concatMap((images) => this.createPaper(images))
          )
 observable.subscribe({
  next: (paper) => {
    if (paper.status === ImageStatus.success) {
      this.scannedList.push(paper);
    } else if (paper.status === ImageStatus.fail) {
      this.errorList.push(paper);
    }
  },
  error: (err) => {
    console.error(err)
  },
  complete: () => {
    this.scanStatus = ScanStatus.done;
  }
});

优点

  • RxJS 功能强大,使用更简洁、优雅的方式来处理复杂的异步数据流逻辑。

  • RxJS 兼容 Promise 对象,可以在Observable对象和Promise对象之间互相转换。

缺点

  • RxJS 的概念比较抽象,需要一定的学习成本和实践经验才能熟练使用。

  • RxJS 使用了许多中间对象和函数,可能会导致性能问题,需要精细的优化。

总结

在JavaScript 提供的多种异步解决方案中,由于Promise 是多种异步方案的基础,必须熟练掌握。平时养成 Async/AwaitPromise 配合使用的习惯。遇到复杂的异步并发逻辑可以求助于 RxJS 等异步类库。

参考文档

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值