Js 异步处理演进,Callback=>Promise=>Observer

简介: 异步调用就像是接水管,相互缠绕的管道越多,就越容易漏水。如何将水管巧妙连通,使整个系统有足够的弹性,需要去认真思考 🤔对于 JavaScript 异步的理解,不少人感到过困惑:Js 是单线程的,如何做到异步的呢?实际上,Js 引擎通过混用 2 种内存数据结构:栈和队列,来实现的。栈与队列的交互也就是大家所熟知的 Js 事件循环~~

image.png

异步调用就像是接水管,相互缠绕的管道越多,就越容易漏水。如何将水管巧妙连通,使整个系统有足够的弹性,需要去认真思考 🤔


对于 JavaScript 异步的理解,不少人感到过困惑:Js 是单线程的,如何做到异步的呢?实际上,Js 引擎通过混用 2 种内存数据结构:栈和队列,来实现的。栈与队列的交互也就是大家所熟知的 Js 事件循环~~


举个栗子🌰


function fooB(){
    console.log('fooB: called');
}
function fooA(){
    fooB();
    console.log('fooA: called');
}
fooA();
// -> fooB: called
// -> fooA: called


Js 引擎解析如下:


1. push fooA to stack
<stack>
|fooA| <- push
2. push fooB to stack
<stack>
|fooB| <- push
|fooA|
3. pop fooB from stack and execute
<stack>
|fooB| <- pop
|fooA|
// -> fooB: called
<stack>
|fooA|
4. pop fooA from stack and execute
<stack>
|fooA| <- pop
// -> fooA: called
<stack>
|   | <- stack is empty


从以上代码可以看出,fooAfooB 两个同步函数都被压入 中,那么什么样的函数会被放入 队列 中呢?


当然就是包含异步操作的函数了:

  • setTimeout
  • setInterval
  • promise
  • ajax
  • DOM events


举个栗子🌰


function fooB(){
    setTimeout(()=>console.log('API call B'));
    console.log('fooB: called');
}
function fooA(){
    setTimeout(()=>console.log('API call A'));
    fooB();
    console.log('fooA: called');
}
fooA();
// -> fooB: called
// -> fooA: called
// -> API call A
// -> API call B


Js 引擎解析如下:


1. push fooA to stack
<stack>
|fooA| <- push
2. push 'API call A' to queue
<queue>|'API call A'| <- push
3. push fooB to stack
<stack>
|fooB| <- push
|fooA|
4. push 'API call B' to queue
<queue>|'API call A'|'API call B'| <- push
5. pop fooB from stack and execute
<stack>
|fooB| <- pop
|fooA|
// -> fooB: called
<stack>
|fooA|
6. pop fooA from stack and execute
<stack>
|fooA| <- pop
// -> fooA: called
<stack> <- stack is empty
|   |
7. pop 'API call A' from queue and execute
<queue>|'API call A'| <- pop |'API call B'|
// -> API call A
<queue>|'API call B'|
8. pop 'API call B' from queue and execute
<queue>|'API call B'| <- pop
// -> API call B
<queue>|   | <- queue is empty

gif 动图释义如下:

image.png


通过简单的回顾 Js 内存中栈和队列是如何交互后(没有细说微任务、宏任务),再看目前我们是如何去组织这种交互的~


没错,就是以下 3 种组织方式,也是本篇核心重点:

  • Callback
  • Promise
  • Observer

Callback=>Promise=>Observer,后一个都是基于前一个的演进~


Callback



怎么理解 Callback ?以打电话给客服为例,有两种选择:

  1. 排队等待客服接听;
  2. 选择客服有空时回电给你。


第 2 种选择就是 JavaScript Callback 回调模式,在等待客服回复的同时,可以做其它事情,一旦客服有空,会主动回电给你~


function success(res){
    console.log("API call successful");
}
function fail(err){
    console.log("API call failed");
}
function callApiFoo(success, fail){
    fetch(url)
      .then(res => success(res))
      .catch(err => fail(err));
};
callApiFoo(success, fail);


Callback 缺点是:嵌套调用会形成回调地狱,如下;


callApiFooA((resA)=>{
    callApiFooB((resB)=>{
        callApiFooC((resC)=>{
            console.log(resC);
        }), fail);
    }), fail);
}), fail);


Promise



众所周知,Promise 就是来解决回调地狱的~

function callApiFooA(){
    return fetch(url); // JS fetch method returns a Promise
}
function callApiFooB(resA){
    return fetch(url+'/'+resA.id);  
}
function callApiFooC(resB){
    return fetch(url+'/'+resB.id);  
}
callApiFooA()
    .then(callApiFooB)
    .then(callApiFooC)
    .catch(fail)


与此同时,Promise 还提供了很多其它更具扩展性的解决方案,比如 Promise.allPromise.race 等;


// Promise.all:并发执行,全部变为 resolve 或 有 reject 状态出现的时候,它才会去调用 .then 方法;

function callApiFooA(){
    return fetch(urlA); 
}
function callApiFooB(){
    return fetch(urlB);  
}
function callApiFooC([resA, resB]){
    return fetch(url+'/'+resA.id+'/'+resB.id);  
}
function callApiFooD(resC){
    return fetch(url+'/'+resC.id);  
}
Promise.all([callApiFooA(), callApiFooB()])
    .then(callApiFooC)
    .then(callApiFooD)
    .catch(fail)


Promise 让代码看起来更简洁,但是演进还没结束;如果想处理复杂的数据流,用 Promise 将会很麻烦......


Observer



处理多个异步操作数据流是很复杂的,尤其是当它们之间相互依赖时,我们必须以更巧妙的方式将它们组合;Observer 登场!


observer 创建(发布)需更改的数据流,subscribe 调用(订阅消费)数据流;以 RxJs 举例:


function callApiFooA(){
    return fetch(urlA); 
 } 
 function callApiFooB(){
    return fetch( urlB );  
 }
 function callApiFooC( [resAId, resBId] ){
    return fetch(url +'/'+ resAId +'/'+ resBId);  
 } 
 function callApiFooD( resC ){
    return fetch(url +'/'+ resC.id);  
 } 
 Observable.from(Promise.all([callApiFooA() , callApiFooB() ])).pipe(
    map(([resA, resB]) => ([resA.id, resB.id])), // <- extract ids
    switchMap((resIds) => Observable.from(callApiFooC( resIds ) )),
    switchMap((resC) => Observable.from(callApiFooD( resC ) )),
    tap((resD) => console.log(resD))
).subscribe();


详细过程:


  • Observable.from 将一个 Promises 数组转换为 Observable,它是基于 callApiFooA 和 callApiFooB 的结果数组;
  • map — 从 API 函数 A 和 B 的 Respond 中提取 ID;
  • switchMap — 使用前一个结果的 id 调用 callApiFooC,并返回一个新的 Observable,新 Observable 是 callApiFooC( resIds ) 的返回结果;
  • switchMap — 使用函数 callApiFooC 的结果调用 callApiFooD;
  • tap — 获取先前执行的结果,并将其打印在控制台中;
  • subscribe — 开始监听 observable;


Observable是多数据值的生产者,它在处理异步数据流方面更加强大和灵活,它在 Angular 等前端框架中被使用~~


敲!这写法,这模式不就是函数式编程中的函子吗?Observable 就是被封装后的函子,不断传递下去,形成链条,最后调用 subscribe 执行,也就是惰性求值,到最后一步才执行、消费!

这样做有何好处?


核心原因就是分离创建(发布)调用(订阅消费)


再举个栗子 🌰

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});
console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');


observable 发布(同步地123 三个值;1秒之后,继续发布4这个值,最后结束;


subscribe 订阅,调用执行;subscription.unsubscribe() 可以在过程中中止执行;

控制台打印结果:

just before subscribe
got value 1
got value 2
got value 3
just after subscribe
got value 4
done


小感:Js 异步处理演进分为 3 个阶段:Callback=>Promise=>Observer,重点理解也就是 Observer,Observer 就像是函数编程的函子,封装、传递链、延迟执行,几乎一摸一样,不过它更加强调发布和订阅的思想!分割函数的创建和执行为两个独立的域,对于弹性组装异步水管至关重要!!


以上! 后续会带来 Rx.js Observer 实战~~ 之前的文章就提过,惰性求值似乎能连接 js 最重要的闭包和异步两个要点,现在看来更是如此,敬请期待~~

看到这里,不如点个赞吧~👍👍👍

我是掘金安东尼,公众号同名,日拱一卒、日掘一金,再会~


本篇参考:


相关文章
|
2月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
15 1
|
2月前
|
前端开发 JavaScript
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
25 4
|
2月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:Promise 和 Async/Await
在现代的 JavaScript 开发中,异步编程是至关重要的。本文将介绍 JavaScript 中的异步编程概念,重点讨论 Promise 和 Async/Await 这两种常见的处理异步操作的方法。通过本文的阐述,读者将能够更好地理解和应用这些技术,提高自己在 JavaScript 开发中处理异步任务的能力。
|
6天前
|
前端开发 JavaScript
js开发中的异步处理
JavaScript中的异步处理包括回调函数、Promise和async/await。回调函数是早期方法,将函数作为参数传递给异步操作并在完成后执行。Promise提供链式处理,通过resolve和reject管理异步操作的成功或失败。async/await基于Promise,允许写更简洁的同步风格代码,通过try-catch处理错误。Promise和async/await是现代推荐的异步处理方式。
|
2月前
|
前端开发 JavaScript
js开发:请解释Promise是什么,以及它如何解决回调地狱(callback hell)问题。
Promise是JavaScript解决异步操作回调地狱的工具,代表未来可能完成的值。传统的回调函数嵌套导致代码难以维护,而Promise通过链式调用`.then()`和`.catch()`使异步流程清晰扁平。每个异步操作封装为Promise,成功时`.then()`传递结果,出错时`.catch()`捕获异常。ES6的`async/await`进一步简化Promise的使用,使异步代码更接近同步风格。
19 1
|
2月前
|
存储 JavaScript 前端开发
js开发:请解释什么是回调函数(callback function),并给出一个示例。
回调函数是JavaScript中处理异步编程的一种常见模式,常用于事件驱动和I/O操作。它们作为参数传递给其他函数,在特定条件满足或任务完成后被调用。例如,`asyncOperation`函数接受回调函数`handleResult`,在模拟的异步操作完成后,调用`handleResult`并传递结果。这使得程序员能在操作完成后执行后续任务。
22 1
|
3月前
|
前端开发 JavaScript API
JavaScript学习笔记(一)promise与async
JavaScript学习笔记(一)promise与async
|
3月前
|
前端开发 JavaScript UED
JavaScript中的异步编程和Promise
【2月更文挑战第3天】在Web开发中,JavaScript是一门非常重要的编程语言,而异步编程是JavaScript中的一个关键概念。本文将介绍JavaScript中的异步编程特点,以及如何使用Promise来更加优雅地处理异步操作,帮助开发者更好地理解和应用这一技术。
17 3
|
3月前
|
前端开发 JavaScript 数据处理
JavaScript中的异步编程及Promise对象
【2月更文挑战第3天】 传统的JavaScript编程模式在处理异步任务时常常会导致回调地狱和代码可读性较差的问题,而Promise对象的引入为解决这一问题提供了一种优雅的解决方案。本文将介绍JavaScript中的异步编程方式以及Promise对象的使用方法和优势,帮助读者更好地理解和运用异步编程技术。
21 8
|
3月前
|
前端开发 JavaScript
JavaScript中的异步编程与Promise
【2月更文挑战第1天】在Web开发中,JavaScript是一种常用的编程语言,而异步编程是其重要特点之一。本文将介绍JavaScript中的异步编程机制,探讨Promise对象的使用以及如何通过Promise处理异步操作,帮助读者更好地理解和应用JavaScript异步编程技术。