JS异步编程中的回调与promise

最近抽空复习了一下之前读过的JS书,看了一下关于回调函数和promise相关部分。

回调函数

提到异步编程,尽管发展到如今,js中解决异步的方式已经出现了很多种,Promise、async/await... 但不可否认,在这些出现之前,我们采用的最常规的方式就是回调函数,可以说,回调函数是js中最基础的异步模式。但尽管如此,回调还是存在着很多不可忽视的缺点。

  • 执行顺序

思考这样一段代码

fs.readFile('file.txt', 'utf-8', functino(data){
    console.log('A');
    setTimeout(function () {
        console.log('B')
    }, 0)
    console.log('C')
})
console.log('D'); 

有经验的同学可能稍加推敲就能得出正确结论,

D A C B

但不可置否,这样一段代码的执行顺序是违背我们大脑的正常思维顺序的,我们在大脑中是不断上下跳跃着的。再有,如果把上面的setTimeout换成一个同步函数呢?那么结果就是D A B C。再如果它只是会视情况而定同步或者异步,也就是我们并不确定它是同步还是异步,这样的情况下,我们如何解决呢?

解决方法或许只能将每个步骤硬编码到前一个步骤中了。

但是上述只是个简单例子,现实中的项目远比这个复杂,嵌套的更深,状态更多,
这种方式使得代码可复用性变差,维护成本变高,与我们现在提倡的低耦合相驳。

  • 控制反转
// 假如doSomeThing()是一个第三方api,负责做某些事情
// 通过传一个callback来执行接下来的步骤
doSomeThing('...', function () {
    // ...
})

上述例子中, callback的执行取决于doSomeThing(),这种现象叫做"控制反转",如果doSomeThing中发生异常,或者说doSomeThing是一个你根本不了解的第三方api,那么你所传的callback可能出现任何你想不到的情况,因为此时callback的控制权并不在你手中, 你不能决定它何时调用,调用次数,是否传参等等等等....

引用《你不知道的JavaScript中卷》

回调最大的问题是控制反转,它会导致信任链的完全断裂。

总而言之,我们需要一种比回调更好的机制,来解决执行顺序、信任的问题。值得欣喜的是,JS目前已经提供了很多更加强大的异步模式,Promise就是其中之一。

Promise

所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。

Promise 对象有以下两个特点。

  • 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

基本用法

// 我们定义三个异步行为A、B、C
function A (cb) {
    setTimeout(function () {
        console.log('执行A')
        cb && cb()
    })
}
function B (cb) {
    setTimeout(function () {
        console.log('执行B')
        cb && cb()
    })
}
function C (cb) {
    setTimeout(function () {
        console.log('执行C')
        cb && cb()
    })
    
}

假设这三个行为是相互依赖关系执行,也就是A执行完再执行B,B执行完再执行C
首先看es5的实现方式

    A(B(C))

在看Promise版本

function A () {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            console.log('执行A')
            resolve()
        })
    })
}
function B () {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            console.log('执行B')
            resolve()
        })
    })
}
function C () {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            console.log('执行C')
            resolve()
        })
    })
}


A().then(B).then(C);

怎么样,是不是觉得清晰了很多?

回想一下我们在上面回调函数中遇到的两个问题 执行顺序控制反转

  • 执行顺序

我们可以看到 在promise中我们可以很清晰的看出来,先执行A接下来是B然后是C,并且我们也不需要关心A或者B中是同步还是异步操作,无论同步异步都不会影响到执行顺序。
这种方式使得我们的代码一眼就可以看清楚他的执行流程,无论维护成本还是清晰程度都比回调函数要好的多,避免了“Callback Hell(回调地狱)”

  • 控制反转

Promise拥有个then方法,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。我们可以根据promise的状态,如果为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。这样我们相当于把控制权重新拿回到我们自己手中。
举个例子

function A () {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            console.log('执行A')
            resolve('a')
        })
    })
}

A().then(function(data){
    // data就是A返回的proise状态成功后所返回的值
    console.log(data); // 'a'
}, function(err) {
    // 如果A的状态变为reject,将会处罚这个回调函数
})


除了then之外,promise还有几个方法。

Promise.prototype.catch();

Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数

promiseFn.then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 promiseFn 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

Promise.all()

Promise.all()用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

返回的结果是一个数组,里面对应参数中的几个promise实例的返回值。
只有当这几个实例的状态都变成成功,或者其中有一个变为失败,才会调用Promise.all方法后面的回调函数。

Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

但是不同于Promise.all的是,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

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

推荐阅读更多精彩内容