前言
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 有三种状态:pending
、fulfilled
和 rejected
。当 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
方法会返回一个新的状态为resolved
的Promise
对象,结果就为其参数。
-
-
Promise.reject()
会返回一个新的状态为rejected
的Promise
实例,其参数为失败的原因,之后直接传递给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 }
,value
即return
语句后的表达式的值,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 实现,用于处理异步数据流。它提供了一些基本概念: Observable
、Observer
、Operators
、Subscription
、Subject
和Scheduler
,以便于将异步事件作为集合进行处理。
Observable
(可观察者)
Observable
可以理解为返回多个值的事件流,observers
可以订阅这个流。
返回单个值 | 返回多个值 | |
同步 / pull | Object | Iterables (Array | Set | Map) |
异步 / push | Promise | Observable |
Observable
可以通知其observers
三件事情:
-
发送一个值:
subscriber.next()
-
发送一个错误:
subscriber.error()
-
发送完成通知:
subscriber.complete()
一个Observable
可以发送多个值,但只能发送一次错误或完成通知。当Observable
发送错误或完成之后,将不能再发送任何通知。
Observer(观察者)
Observer
是 Observable
发送通知的接收者。它只是具有三个回调函数的对象,分别对应于 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
,比如fromEvent
、interval
、of
等。
管道操作符(Pipeable Operators)是一个以 Observable
作为输入并返回另一个 Observable
的函数。用于处理Observable
事件流中的数据,比如map
、filter
、concatMap
等。管道操作符的调用形式为:
// 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
,有:AsyncSubject
、BehaviorSubject
、ReplaySubject
、Subject
。
Subject
和Observable
的区别是它可以将值共享传播到多个 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/Await
和 Promise
配合使用的习惯。遇到复杂的异步并发逻辑可以求助于 RxJS 等异步类库。