关于js中异步问题的解决方案

在js中有一个始终无法绕过的问题,如何优雅地解决异步问题。实际上,js在执行过程中,每遇到一个异步函数,都会将这个异步函数放入一个异步队列中,只有当同步线程执行结束之后,才会开始执行异步队列中的函数,这个是讨论解决异步方案的前提。

解决问题的方法

主流的解决方法主要有以下几种:

  1. 回调函数
  2. 事件触发
  3. 发布/订阅者模式
  4. promise
  5. generate

方法介绍

回调函数

回调函数应该属于最简单粗暴的一种方式,主要表现为在异步函数中将一个函数进行参数传入,当异步执行完成之后执行该函数

回调函数

这种写法最大的问题是:如果存在这样的一个业务场景,有三个异步函数A,B,C,其中B的执行需要在A执行结束之后,C的执行需要在B之后,这样的场景模拟成代码就是(jquery中ajax方法为例)
回调函数地狱

试想,如果再多几个异步函数,代码整体的维护性,可读性都变的极差,如果出了bug,修复的排查过程也变的极为困难,这个便是所谓的 回调函数地狱

事件监听

事件监听最常用的常见在于DOM元素事件绑定触发,如果我们想在DOM元素与用户进行鼠标或其他交互之后执行某些逻辑,就可以使用事件监听了


事件监听

引申开来,我们可以创建一个Event类,定义on和emit方法来绑定和触发自定义事件


Event类

同样回到在回调函数上遇到的难题,遇到多层嵌套的异步问题,使用事件触发的方法如何进行解决?
事件监听

嗯,从写法上来看,相比于回调函数,整洁了一些。

发布/订阅者模式

订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某发布者对象。这个发布者对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。vue就是基于发布/订阅者模式。
如何通过代码实现一个发布/订阅者模式?
引用了《Pro JavaScript Design Patterns》第15章的例子

//创建一个主题发布类
var Publisher=function(){
    this.subscribers=[]
}


Publisher.prototype.publish=function(data){
    this.subscribers.forEach(function(fn){
        fn(data)
    })
}

/*
    在Function上挂载这个些方法,所有的函数都可以调用这些方法
    表示所有函数都可以订阅/取消订阅相关的主题发布
*/

//订阅
Function.prototype.subscribe=function(publisher){
    var that=this;
    var isExist=publisher.subscribers.some(function(el){
        if(el===that){
            return true
        }
    })
    if(!isExist){
        publisher.subscribers.push(that)
    }
    //return this是为了支持链式调用
    return this
}

//取消订阅
Function.prototype.unsubscribe=function(publisher){
    var that=this;

    //就是将函数从发布者的订阅者列表中进行删除
    publisher.subscribers=publisher.subscribers.filter(function(el){
        if(el!==that){
            return true
        }
    })

    return this
}

var publisher=new Publisher();
var subscriberObj=function(data){
    console.log(data)
}
subscriberObj.subscribe(publisher)

这样就实现了一个简单的发布订阅者模式,每次发布者发布新内容时,就会调用publish方法,然后将内容作为参数,依次调用订阅者函数(subscribers)。
其实,发布/订阅模式与事件监听很类似,

  • 事件监听是将一个回调函数事件绑定在一起,触发了相应事件,就会执行相应的回调函数
  • 发布/订阅模式是将订阅函数放入了发布者的订阅者列表中,更新时,遍历订阅者列表,执行所有的订阅者函数

promise

promise由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。


阮大神对promise的描述

简单来讲,就还是promise中有着三种状态pending,fulfilled,rejected。在代码中我们可以控制状态的变更

Promise

创建一个Promise对象需要传入一个函数,函数的参数是resolve和reject,在函数内部调用时,就分别代表状态由pending=>fulfilled(成功),pending=>rejected(失败)
一旦promise状态发生变化之后,之后状态就不会再变了。比如:调用resolve之后,状态就变为fulfilled,之后再调用reject,状态也不会变化

Promise对象在创建之后会立刻执行,因此一般的做法是使用一个函数进行包装,然后return一个promise对象

function timeout(){
        return new Promise(function(resolve,reject){
            ...//异步操作
        })
    }

在使用时可以通过promise对象的内置方法then进行调用,then有两个函数参数,分别表示promise对象中调用resolve和reject时执行的函数

function timeout(){
        return new Promise(function(resolve,reject){
            setTimeout(function(){
                resolve();
            },1000)
        })
    }


    timeout()
    .then(function(){
        ...//对应resolve时执行的逻辑
    },function(){
        ...//对应reject时执行的逻辑
    })

可以使用多个then来实现链式调用,then的函数参数中会默认返回promise对象

timeout()
    .then(function(){
        ...//对应resolve时执行的逻辑
    },function(){
        ...//对应reject时执行的逻辑
    })
    .then(function(){
        ...//上一个then返回的promise对象对应resolve状态时执行的逻辑
    },function(){
        ...//上一个then返回的promise对象对应reject状态时执行的逻辑
    })

使用promise来解决回调地狱的做法就是使用then的链式调用

function fnA(){
        return new Promise(resolve=>{
            ...//异步操作中resolve
        })
    }
    function fnB(){
        return new Promise(resolve=>{
            ...//异步操作中resolve
        })
    }
    function fnC(){
        return new Promise(resolve=>{
            ...//异步操作中resolve
        })
    }

    fnA()
    .then(()=>{
        return fnB()
    })
    .then(()=>{
        return fnC()
    })

清晰直观了许多

generate函数

创建一个generate函数很简单

function* gen(){
        yield 1
        yield 2
        return 3
    }

区别于普通函数的地方在于function后面的*号,以及函数内部的yield。
*号是定义方式,带有 * 号表示是一个generate函数,yield是其内部独特的语法。

function* gen(){
        yield 1
        yield 2
        return 3
    }

    let g=gen();
    console.log(g.next())//{value:1,done:false}
    console.log(g.next())//{value:2,done:false}
    console.log(g.next())//{value:3,done:true}
    console.log(g.next())//{value:undefined,done:true}

调用generate函数会生成一个遍历器对象,不会立即执行,需要调用next执行,执行到带有yield的那一步,next会返回一个对象,对象中value表示yield或return后的值,done表示函数是否已经执行结束(是否已经执行到return)。之后每次执行next都会从上一个yield开始继续执行

function* gen(){
        let res=yield 1
        yield res
        return 3
    }
    let g=gen();
    console.log(g.next())//{value:1,done:false}
    console.log(g.next(333))//{value:333,done:false}

在next中传入参数会作为上一次yield的返回值(会忽略第一个next中传递的参数)

但是如何使用generate函数来进行异步编程?
这里可以使用ES2017中的async和await语法(其实属于generate函数的语法糖)
使用async替换*号,使用await替换yield就将generate函数改造成了一个async函数

async function test(){
        await 1
        await asyncFn()
    }
test()

其中await后面可以跟promise和原始数据类型(相当与同步操作),而async返回一个promise对象(因此可以使用then进行链式调用),使用时直接调用就行了

我们再来看看如何解决回调地狱的问题

function fnA(){
        return new Promise(resolve=>{
            ...//异步操作中resolve
        })
    }
    function fnB(){
        return new Promise(resolve=>{
            ...//异步操作中resolve
        })
    }
    function fnC(){
        return new Promise(resolve=>{
            ...//异步操作中resolve
        })
    }

    async function gen(){
        let resA=await fnA()
        let resB=await fnB(resA)
        let resC=await fnC(resB)
    }
    gen()

相比于之前的方法更简单直观,也更容易理解整体的代码流程。

(关于generate部分语法可以参考 Generator 函数的语法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,708评论 0 5
  • 前言 编程语言很多的新概念都是为了更好的解决老问题而提出来的。这篇博客就是一步步分析异步编程解决方案的问题以及后续...
    李向_c52d阅读 1,066评论 0 2
  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,302评论 5 22
  • 本文首发在个人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云阅读 1,680评论 0 3
  • 这周完成的 后台: 1 复现并解决线上数据接口在高并发的情况下锁竞争导致耗时异常的问题。 2 针对招生活动复杂版进...
    heartshapebox阅读 549评论 0 0