浅谈JavaScript中的异步处理

在 JavaScript 的世界中,所有代码都是单线程执行的,由于这个“缺陷”,导致 JavaScript 的所有网络操作,浏览器事件,都必须是异步执行。

异步执行可以用回调函数实现。

异步操作会在将来的某个时间点触发一个函数调用。

主流的异步处理方案主要有:回调函数 (CallBack) 、 Promise 、 Generator 函数、 async/await 。

一、回调函数(CallBack)

这是异步编程最基本的方法

假设我们有一个 getData 方法,用于异步获取数据,第一个参数为请求的 url 地址,第二个参数是回调函数,如下:


  1. function getData(url, callBack){ 
  2.     // 模拟发送网络请求 
  3.     setTimeout(()=> { 
  4.         // 假设 res 就是返回的数据 
  5.         var res = { 
  6.             url: url, 
  7.             data: Math.random() 
  8.         } 
  9.         // 执行回调,将数据作为参数传递 
  10.         callBack(res) 
  11.     }, 1000) 

我们预先设定一个场景,假设我们要请求三次服务器,每一次的请求依赖上一次请求的结果,如下:


  1. getData('/page/1?param=123', (res1) => { 
  2.     console.log(res1) 
  3.     getData(`/page/2?param=${res1.data}`, (res2) => { 
  4.         console.log(res2) 
  5.         getData(`/page/3?param=${res2.data}`, (res3) => { 
  6.             console.log(res3) 
  7.         }) 
  8.     }) 
  9. }) 
  • 通过上面的代码可以看出,第一次请求的 url 地址为: /page/1?param=123 ,返回结果为 res1 。
  • 第二个请求的 url 地址为: /page/2?param=${res1.data} ,依赖第 一次请求的 res1.data ,返回结果为 res2`。
  • 第三次请求的 url 地址为: /page/3?param=${res2.data} ,依赖第二次请求的 res2.data ,返回结果为 res3 。

由于后续请求依赖前一个请求的结果,所以我们只能把下一次请求写到上一次请求的回调函数内部,这样就形成了常说的:回调地狱。

二、发布/订阅

我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”( publish )一个信号,其他任务可以向信号中心”订阅”(
subscribe )这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe
pattern),又称”观察者模式”(observer pattern)

这个模式有多种实现,下面采用的是Ben Alman的 Tiny Pub/Sub ,这是 jQuery 的一个插件

首先, f2 向”信号中心” jQuery 订阅” done “信号


  1. jQuery.subscribe("done", f2); 

f1进行如下改写


  1. function f1(){ 
  2. setTimeout(function(){ 
  3. // f1的任务代码 
  4. jQuery.publish("done"); 
  5. }, 1000); 

jQuery.publish("done") 的意思是, f1 执行完成后,向”信号中心 "jQuery 发布 "done" 信号,从而引发f2的执行。 此外,f2完成执行后,也可以取消订阅( unsubscribe )


  1. jQuery.unsubscribe("done", f2); 

这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

三、Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大

所谓 Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说, Promise 是一个对象,从它可以获取异步操作的消息。 Promise 提供统一的 API ,各种异步操作都可以用同样的方法进行处理

简单说,它的思想是,每一个异步任务返回一个 Promise 对象,该对象有一个 then 方法,允许指定回调函数。

现在我们使用 Promise 重新实现上面的案例,首先,我们要把异步请求数据的方法封装成 Promise


  1. function getDataAsync(url){ 
  2.     return new Promise((resolve, reject) => { 
  3.         setTimeout(()=> { 
  4.             var res = { 
  5.                 url: url, 
  6.                 data: Math.random() 
  7.             } 
  8.             resolve(res) 
  9.         }, 1000) 
  10.     }) 

那么请求的代码应该这样写


  1. getDataAsync('/page/1?param=123') 
  2.     .then(res1=> { 
  3.         console.log(res1) 
  4.         return getDataAsync(`/page/2?param=${res1.data}`) 
  5.     }) 
  6.     .then(res2=> { 
  7.         console.log(res2) 
  8.         return getDataAsync(`/page/3?param=${res2.data}`) 
  9.     }) 
  10.     .then(res3=> { 
  11.         console.log(res3) 
  12.     }) 

then 方法返回一个新的 Promise 对象, then 方法的链式调用避免了 CallBack 回调地狱

但也并不是完美,比如我们要添加很多 then 语句, 每一个 then 还是要写一个回调。

如果场景再复杂一点,比如后边的每一个请求依赖前面所有请求的结果,而不仅仅依赖上一次请求的结果,那会更复杂。 为了做的更好, async/await 就应运而生了,来看看使用 async/await 要如何实现

四、async/await

getDataAsync 方法不变,如下


  1.  function getDataAsync(url){ 
  2.     return new Promise((resolve, reject) => { 
  3.         setTimeout(()=> { 
  4.             var res = { 
  5.                 url: url, 
  6.                 data: Math.random() 
  7.             } 
  8.             resolve(res) 
  9.         }, 1000) 
  10.     }) 

业务代码如下


  1. async function getData(){ 
  2.     var res1 = await getDataAsync('/page/1?param=123') 
  3.     console.log(res1) 
  4.     var res2 = await getDataAsync(`/page/2?param=${res1.data}`) 
  5.     console.log(res2) 
  6.     var res3 = await getDataAsync(`/page/2?param=${res2.data}`) 
  7.     console.log(res3) 

可以看到使用 async\await 就像写同步代码一样

对比 Promise 感觉怎么样?是不是非常清晰,但是 async/await 是基于 Promise 的,因为使用 async
修饰的方法最终返回一个 Promise , 实际上, async/await 可以看做是使用 Generator
函数处理异步的语法糖,我们来看看如何使用 Generator 函数处理异步

五、Generator

首先异步函数依然是


  1. function getDataAsync(url){ 
  2.     return new Promise((resolve, reject) => { 
  3.         setTimeout(()=> { 
  4.             var res = { 
  5.                 url: url, 
  6.                 data: Math.random() 
  7.             } 
  8.             resolve(res) 
  9.         }, 1000) 
  10.     }) 

使用 Generator 函数可以这样写


  1. function*getData(){ 
  2.     var res1 = yield getDataAsync('/page/1?param=123') 
  3.     console.log(res1) 
  4.     var res2 = yield getDataAsync(`/page/2?param=${res1.data}`) 
  5.     console.log(res2) 
  6.     var res3 = yield getDataAsync(`/page/2?param=${res2.data}`) 
  7.     console.log(res3)) 

然后我们这样逐步执行


  1. var g = getData() 
  2. g.next().value.then(res1=> { 
  3.     g.next(res1).value.then(res2=> { 
  4.         g.next(res2).value.then(()=> { 
  5.             g.next() 
  6.         }) 
  7.     }) 
  8. }) 

上面的代码,我们逐步调用遍历器的 next() 方法,由于每一个 next() 方法返回值的 value 属性为一个 Promise 对象

所以我们为其添加 then 方法, 在 then 方法里面接着运行 next 方法挪移遍历器指针,直到 Generator 函数运行完成,实际上,这个过程我们不必手动完成,可以封装成一个简单的执行器


  1. function run(gen){ 
  2.     var g = gen() 
  3.  
  4.     function next(data){ 
  5.         var res = g.next(data) 
  6.         if (res.done) return res.value 
  7.         res.value.then((data) => { 
  8.             next(data) 
  9.         }) 
  10.     } 
  11.  
  12.     next() 
  13.  

run 方法用来自动运行异步的 Generator 函数,其实就是一个递归的过程调用的过程。这样我们就不必手动执行 Generator 函数了。 有了 run 方法,我们只需要这样运行 getData 方法


  1. run(getData) 

这样,我们就可以把异步操作封装到 Generator 函数内部,使用 run 方法作为 Generator
函数的自执行器,来处理异步。其实我们不难发现, async/await 方法相比于 Generator
处理异步的方式,有很多相似的地方,只不过 async/await 在语义化方面更加明显,同时 async/await
不需要我们手写执行器,其内部已经帮我们封装好了,这就是为什么说 async/await 是 Generator 函数处理异步的语法糖了。

作者:佚名

来源:51CTO

时间: 2024-09-13 11:47:00

浅谈JavaScript中的异步处理的相关文章

浅谈Jquery中Ajax异步请求中的async参数的作用_jquery

之前不知道这个参数的作用,上网找了前辈的博客,在此收录到自己的博客,希望能帮到更多的朋友: test.html <a href="javascript:void(0)" onmouseover="testAsync()"> asy.js function testAsync{ var temp; $.ajax({ async: false, type : "GET", url : 'tet.php', complete: functi

浅谈JavaScript中的对象及Promise对象的实现_javascript技巧

JavaScript 中的所有事物都是对象:字符串.数值.数组.函数.下面小编给大家收集整理些javascript中的对象及promise对象的实现.具体内容如下: 到处都是对象 window对象 常用的属性和方法介绍 location 包含页面的URL,如果改变这个属性,浏览器会访问新的URL status 包含将在浏览器状态去显示的一个串.一般在浏览器左下角 onload: 包含了需要在页面完全加载后调用的函数 document: 包含DOM alert方法: 显示一个提醒 prompt方法

浅谈javascript中call()、apply()、bind()的用法

  浅谈javascript中call().apply().bind()的用法         一直对Javascript中的apply/call/bind的用法很模糊,恰好看到了这篇文章.对三者之间的区别与联系算是有了比较清晰的认识.这里记录下来,分享给大家. call(thisObj,arg1,arg2...).apply(thisObj,[obj1,obj2...])这二个方法是每个函数都包含的非继承的方法 call(thisobj[, args])和apply(thisobj[, arg

浅谈javascript中this在事件中的应用

 这篇文章主要介绍了浅谈javascript中this在事件中的应用实例,非常有助于我们对this关键字的理解,这里推荐给大家.     this关键字在javascript中是非常强大的,但是如果你不清楚它是怎么工作的就很难使用它.   代码如下: function dosomething(){ this.style.color="#fff"; }   上面这段代码中的this指向什么呢,运行dosomething()会输出什么呢? 在javascript中,this总是指向当前执行

浅谈javascript中new操作符的原理_基础知识

javascript中的new是一个语法糖,对于学过c++,java 和c#等面向对象语言的人来说,以为js里面是有类和对象的区别的,实现上js并没有类,一切皆对象,比java还来的彻底 new的过程实际上是创建一个新对象,把新象的原型设置为构造器函数的原型,在使用new的过程中,一共有3个对象参与了协作,构造器函数是第一个对象,原型对象是二个,新生成了一个空对象是第三个对象,最终返回的是一个空对象,但这个空对象不是真空的,而是已经含有原型的引用(__proto__) 步骤如下: (1) 创建一

浅谈javascript中的三种弹窗_javascript技巧

js中三种弹窗 1)alert 弹出警告 无返回值---------alert('第一行\n第二行'); 2)confirm()选择确定或取消,返回t或f----var result = confirm('是否删除!'); 3)prompt()弹出输入框,返回输入内容----var value = prompt('输入你的名字:', '请在这里输入名字'); 当然也可以自定义好看的样式.下面代码有问题明天再改. <script> //window.confirm //prompt window

浅谈Javascript中的函数、this以及原型_javascript技巧

关于函数 在Javascript中函数实际上就是一个对象,具有引用类型的特征,所以你可以将函数直接传递给变量,这个变量将表示指向函数"对象"的指针,例如: function test(message){ alert(message); } var f = test; f('hello world'); 你也可以直接将函数申明赋值给变量: var f = function(message){ alert(message); }; f('hello world'); 在这种情况下,函数申明

浅谈JavaScript中的分支结构_javascript技巧

说到JavaScript中的分支结构,我们就不得不提到流程控制这个词,我们所有的程序都是由数据和算法组成的. 程序=数据+算法 通常我们所说的算法都可以通过"顺序","分支","循环"三种结构来组合完成. 在ECMA中规定了一些语句(也称为流程控制语句,分支结构语句),从本质上来说,这些语句定义了ECMAScript中的主要语法,语句通常使用一个或者多个关键字来完成给定任务. 1.1 if 语句 if 语句 - 只有当指定条件为 true 时,使

浅谈javascript中的Function和Arguments_javascript技巧

javascript的Function 属性: 1.Arguments对象 2.caller  对调用单前函数的Function的引用,如果是顶层代码调用,  则返回null(firefox返回undefined).  注:只有在代码执行时才有意义 3.length  声明函数是指定的命名参数的个数(函数定义是,定义参数的个数) 4.prototype  一个对象,用于构造函数,这个对象定义的属性和方法  由构造函数创建的所有对象共享. 方法:  applay() --> applay(this