异步JavaScript编程(回调,Promise,RxJs)

大家好 请与Omelnitsky Sergey联系。 不久前,我领导了一个反应式编程流,在其中讨论了JavaScript中的异步性。 今天,我想概述一下这些材料。



但是在开始主要材料之前,我们需要做一个介绍性的材料。 因此,让我们从定义开始:什么是堆栈和队列?


堆栈是一个集合,其元素按照“后进先出”的原则进行接收


队列是一个集合,其集合根据原理(“先入先出” FIFO


好的,让我们继续。



JavaScript是一种单线程编程语言。 这意味着它只有一个执行线程和一个堆栈,其中功能排队等待执行。 因此,在某个时间点,JavaScript只能执行一个操作,而其他操作将等待它们在堆栈上打开,直到被调用。


简单来说, 调用堆栈是一种数据结构,用于记录有关程序所在位置的信息。 如果我们进入一个函数,我们会将关于它的记录放在栈顶。 当我们从函数中返回时,我们从堆栈中拉出最上面的元素,并找到我们从中调用此函数的位置。 这是堆栈可以做的所有事情。 现在是一个非常有趣的问题。 异步在JavasScript中如何工作?



实际上,除了堆栈之外,浏览器还有一个特殊的队列来处理所谓的WebAPI。 只有完全清除堆栈后,才能按顺序执行此队列中的功能。 只有在此之后,它们才会从队列中压入堆栈以执行。 如果当前至少有一个元素在堆栈上,则它们将无法进入堆栈。 正是由于这个原因,由于超时无法正常调用时间,因为函数在充满时无法从队列进入堆栈。


考虑下面的示例,并逐步进行“执行”。 另请参阅系统中发生的情况。


console.log('Hi'); setTimeout(function cb1() { console.log('cb1'); }, 5000); console.log('Bye'); 


1)到目前为止,什么都没有发生。 浏览器控制台干净,调用堆栈为空。



2)然后将console.log('Hi')命令添加到调用堆栈中。



3)执行



4)然后从调用堆栈中删除console.log('Hi')。



5)现在转到setTimeout命令(功能cb1(){...})。 它被添加到调用堆栈中。



6)执行setTimeout(功能cb1(){...})命令。 浏览器创建一个计时器,该计时器是Web API的一部分。 他将进行倒计时。



7)setTimeout命令(功能cb1(){...})已完成,已从调用堆栈中删除。



8)将console.log('Bye')命令添加到调用堆栈中。



9)执行console.log('Bye')命令。



10)从调用堆栈中删除console.log('Bye')命令。



11)经过至少5000毫秒后,计时器退出,并将cb1回调放置在回调队列中。



12)事件循环从回调队列中获取cb1函数,并将其放置在调用堆栈中。



13)执行cb1函数,并将console.log('cb1')添加到调用堆栈中。



14)执行console.log('cb1')命令。



15)从调用堆栈中删除了console.log命令('cb1')。



16)cb1函数从调用堆栈中删除。


看一下动态示例:



好了,这里我们研究了如何在JavaScript中实现异步。 现在,让我们简要地讨论异步代码的发展。


异步代码的演变。


 a(function (resultsFromA) { b(resultsFromA, function (resultsFromB) { c(resultsFromB, function (resultsFromC) { d(resultsFromC, function (resultsFromD) { e(resultsFromD, function (resultsFromE) { f(resultsFromE, function (resultsFromF) { console.log(resultsFromF); }) }) }) }) }) }); 

众所周知,异步编程只能通过函数来​​实现。 它们可以像其他变量一样传递给其他函数。 这样回调就诞生了。 而且它很酷,很有趣且令人着迷,直到变成悲伤,渴望和悲伤。 怎么了 是的,一切都很简单:


  • 随着代码复杂性的提高,该项目迅速变成模糊的重复嵌套块-“回调地狱”。
  • 错误处理很容易错过。
  • 您不能使用return返回表达式。

有了Promise,一切都会好起来。


 new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 2000); }).then((result) => { alert(result); return result + 2; }).then((result) => { throw new Error('FAILED HERE'); alert(result); return result + 2; }).then((result) => { alert(result); return result + 2; }).catch((e) => { console.log('error: ', e); }); 

  • 承诺链出现了,从而提高了代码的可读性
  • 出现了单独的错误捕获方法
  • 现在您可以使用Promise.all并行运行
  • 我们可以使用异步/等待来解决嵌套异步

但是承诺有其局限性。 例如,一个没有铃鼓跳舞的诺言就无法撤销,最重要的是,它只具有一种价值。


好吧,我们顺利地进行了反应式编程。 你累吗 好吧,好消息是您可以去做些海鸥,三思而后行,然后再继续阅读。 我会继续。


响应式编程基础


响应式编程是一种编程范例,专注于数据流和变更的传播。 让我们仔细看看什么是数据流。


 //     const input = ducument.querySelector('input'); const eventsArray = []; //      eventsArray input.addEventListener('keyup', event => eventsArray.push(event) ); 

假设我们有一个输入字段。 我们创建一个数组,对于输入事件的每次键入,我们都将事件保存在数组中。 在这种情况下,我想指出我们的数组是按时间排序的,即 较晚事件的索引大于较早事件的索引。 这样的数组是简化的数据流模型,但是还不是流。 为了将该数组安全地称为流,它必须能够以某种方式通知订户它已收到新数据。 因此,我们来定义流程。


数据流


 const { interval } = Rx; const { take } = RxOperators; interval(1000).pipe( take(4) ) 


是按时间排序的数据数组,可以指示数据已更改。 现在想象一下编写代码变得多么方便,您需要在一个动作中在代码的不同部分触发多个事件。 我们只订阅该信息流,当更改发生时,他会通知我们。 RxJs库可以做到这一点。



RxJS是一个使用可观察序列处理异步和基于事件的程序的库。 该库提供了Observable的主要类型,几种辅助类型( Observer,Scheduler,Subjects )以及与集合( map,filter,reduce以及JavaScript Array中的所有类似对象 )一起处理事件的运算符。


让我们看一下这个库的基本概念。


可观察者,观察者,生产者


可观察是我们将要研究的第一个基本类型。 此类包含RxJs的大部分实现。 它与可观察的流相关联,您都可以使用subscription方法进行订阅。


Observable实现了用于创建更新的辅助机制,即所谓的Observer 。 Observer的值源称为Producer 。 它可以是数组,迭代器,Web套接字,某种事件等。 因此,可以说可观察是生产者和观察者之间的导体。


Observable处理三种类型的Observer事件:


  • 下一个-新数据
  • error-如果序列由于异常而结束,则为错误。 此事件还涉及序列的完成。
  • complete-发出有关序列完成的信号。 这意味着将没有新数据。

让我们看一下演示:



在开始时,我们将处理值1、2、3,并在1秒后处理。 我们将得到4并结束流程。


耳边的想法

然后我意识到讲故事比写文章有趣得多。 :D


订阅方式


订阅流时,我们创建一个新的订阅类,该类允许我们使用unsubscribe方法取消订阅 。 我们还可以使用add方法对订阅进行分组。 好吧,我们可以使用remove取消线程分组是合乎逻辑的。 添加和删​​除输入法接受另一个订阅。 我想指出,当我们取消订阅时,我们取消所有子订阅的订阅,就好像它们调用了unsubscribe方法一样。 来吧


流的类型


热门
生产者创建的外部可观察生产者内部创建的可观察
在创建可观察对象时传输数据。订阅时报告数据
需要更多逻辑来退订线程自行终止
使用一对多通讯使用一对一关系
所有订阅具有相同的值。订阅是独立的
如果没有订阅,数据可能会丢失重新发布新订阅的所有流值

打个比方,我会想象像电影院里的电影一样的热流。 从那一刻开始,您是什么时候来的,开始观看。 我会比较冷打和打个电话。 支持。 每个呼叫者从头到尾都收听答录机,但是您可以使用取消订阅挂断电话。


我想指出的是,仍然存在所谓的暖流(我很少遇到这种定义,并且仅在外国社区中遇到过)-这是一种从冷流变成热流的流。 问题出现了-在哪里使用))我将通过实践给出一个例子。


我工作有角度。 他积极使用rxjs。 为了将数据发送到服务器,我希望有一个冷流,然后使用asyncPipe在模板中使用此流。 如果我多次使用此管道,那么回到冷流的定义,每个管道都会从服务器请求数据,至少可以这样说。 如果将冷流转换为暖流,则请求将发生一次。


通常,对于初学者来说,了解流程的形式非常复杂,但是很重要。


经营者


 return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`) .pipe( tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))), map(({ data }: TradeCompanyList) => data) ); 

运营商使我们能够使用流。 它们有助于控制可观察对象中发生的事件。 我们将考虑几个最受欢迎的服务,并且可以使用有用信息中的链接来更详细地找到操作员。


运营商-的


我们从辅助运算符开始。 它基于简单值创建一个Observable。



运算符-过滤器



顾名思义,滤波器运算符filter对流信号进行滤波。 如果运算符返回true,则进一步跳过。


运算符-取



take-获取发射数量的值,此后流结束。


运算符-debounceTime



debounceTime-丢弃落入输出数据之间指定时间段内的发射值-在时间间隔过去之后,它将发射最后一个值。


 const { Observable } = Rx; const { debounceTime, take } = RxOperators; Observable.create((observer) => { let i = 1; observer.next(i++); //     1000 setInterval(() => { observer.next(i++) }, 1000); //     1500 setInterval(() => { observer.next(i++) }, 1500); }).pipe( debounceTime(700), //  700     take(3) ); 


运算符-takeWhile



它发出值,直到takeWhile返回false,此后它将取消订阅流。


 const { Observable } = Rx; const { debounceTime, takeWhile } = RxOperators; Observable.create((observer) => { let i = 1; observer.next(i++); //     1000 setInterval(() => { observer.next(i++) }, 1000); }).pipe( takeWhile( producer => producer < 5 ) ); 


运算符-CombineLatest


combin CombineLatest运算符有点类似于promise.all。 它将多个线程合并为一个。 在每个线程至少发出一个信号之后,我们以数组的形式从每个线程中获取最后一个值。 此外,在合并流中发出任何排放之后,它将给出新的值。



 const { combineLatest, Observable } = Rx; const { take } = RxOperators; const observer_1 = Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next('a: ' + i++); }, 1000); }); const observer_2 = Observable.create((observer) => { let i = 1; //     750 setInterval(() => { observer.next('b: ' + i++); }, 750); }); combineLatest(observer_1, observer_2).pipe(take(5)); 


操作员-拉链


Zip-等待每个流中的值,并根据这些值形成一个数组。 如果该值不是来自任何流,那么将不会形成该组。



 const { zip, Observable } = Rx; const { take } = RxOperators; const observer_1 = Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next('a: ' + i++); }, 1000); }); const observer_2 = Observable.create((observer) => { let i = 1; //     750 setInterval(() => { observer.next('b: ' + i++); }, 750); }); const observer_3 = Observable.create((observer) => { let i = 1; //     500 setInterval(() => { observer.next('c: ' + i++); }, 500); }); zip(observer_1, observer_2, observer_3).pipe(take(5)); 


操作员-forkJoin


forkJoin还可以连接线程,但仅在所有线程完成时才赋值。



 const { forkJoin, Observable } = Rx; const { take } = RxOperators; const observer_1 = Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next('a: ' + i++); }, 1000); }).pipe(take(3)); const observer_2 = Observable.create((observer) => { let i = 1; //     750 setInterval(() => { observer.next('b: ' + i++); }, 750); }).pipe(take(5)); const observer_3 = Observable.create((observer) => { let i = 1; //     500 setInterval(() => { observer.next('c: ' + i++); }, 500); }).pipe(take(4)); forkJoin(observer_1, observer_2, observer_3); 


运营商-地图


地图转换运算符将发射值转换为新值。



 const { Observable } = Rx; const { take, map } = RxOperators; Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next(i++); }, 1000); }).pipe( map(x => x * 10), take(3) ); 


运营商-共享,点击


点击运算符-允许您产生副作用,即不影响序列的任何动作。


公用事业共享运营商可以通过冷流使其变热。



随着操作员完成。 让我们继续主题。


耳边的想法

然后我去喝一些海鸥。 这些例子让我感到厌烦:D


主题族


主题族是热流的主要例子。 这些类是混合的,同时充当可观察者和观察者。 由于主题是热门新闻,因此您需要取消订阅。 如果我们谈论基本方法,那么这:


  • 下一步-将新数据传输到流
  • 错误-流的错误和终止
  • 完成-流的终止
  • 订阅-订阅流
  • 取消订阅-取消订阅信息流
  • asObservable-转换为观察者
  • toPromise-转化为承诺

分配 4 5种主题。


耳边的想法

他在视频流4上发言,但事实证明他们又添加了一个。 俗话说,生活和学习。


简单主题new Subject()是最简单的主题。 它是不带参数的。 传递仅在订阅后出现的值。


BehaviorSubject new BehaviorSubject( defaultData<T> ) -在我看来是最常见的主题类型。 输入接受默认值。 它始终保存最后一次发射的数据,在订阅时会传输该数据。 该类还有一个有用的value方法,该方法返回流的当前值。


ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) -输入可以选择接受第一个参数作为其自身存储的值的缓冲区大小,以及第二次需要更改时。


AsyncSubject new AsyncSubject() -订阅时没有任何反应,只有完成后才返回该值。 仅返回最后一个流值。


WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) -该文档对此保持沉默,这是我第一次看到它。 谁知道他在做什么,写作,补充。


FWF。 好了,这里我们考虑了我今天想讲的所有内容。 我希望这些信息对您有所帮助。 您可以在有用的信息选项卡中熟悉参考文献列表。


有用信息


Source: https://habr.com/ru/post/zh-CN462355/


All Articles