撩课-Web架构师养成系列(第二篇)-async

前言

Web架构师养成系列共15篇,每周更新一篇,主要分享、探讨目前大前端领域(前端、后端、移动端)企业中正在用的各种成熟的、新的技术。部分文章也会分析一些框架的底层实现,让我们做到知其然知其所以然。

本篇为第二篇,上一篇:撩课-Web架构师养成系列第一篇

本篇文章阅读需要时长:约15分钟

一、先了解异步?

关于"异步",我们可以这么理解: 一个任务拆分成两段,先执行第一段,然后转而执行其他任务,等到某个时间点,再回过头执行第二段。

比如,你要做土豆炖牛肉,当开始煮牛肉的时候发现土豆没了,可以先让牛肉煮着,然后去买土豆、洗好、切好,再把土豆放到锅里一起煮。

土豆炖牛肉

这种不连续的执行,就叫做异步。相应地,如果是连续的执行,那么就叫做同步。

1.1 JS中常见的异步编程方式?

异步编程的目标就是让代码的执行更加类似于同步编程,开发中比较常用的方式主要包括:

1) 回调函数实现
2) 发布订阅、通知
3) 事件监听
4)Promise/A+ 和 生成器函数
5)async/await

在ES6之前,我们更多地是使用回调函数来实现异步编程。

1.2 认识回调函数

回调函数就是把任务拆解成两部分,把任务的第二部份单独写在一个函数里面,等到执行完其它任务重新执行这个任务的时候,就直接调用这个函数,从而达到异步效果。

案例如下:

/**
 * 土豆炖牛肉
 * @param step1 牛肉
 * @param callback 回调
 */
let cook = (step1, callback) => {
    // 1. 煮牛肉
    console.log(`烧水煮${step1}`);
    //  2. 放入土豆(5秒后执行)
    setTimeout(() => {
        let step2 = '放入土豆';
        callback(step2);
    }, 5000)
};

// 1. 先煮牛肉  
cook('牛肉', (data) => {
    console.log(data);
});

// 2. 做其它事, 5s后放入土豆 
console.log('买土豆');
console.log('洗土豆');
console.log('切土豆');

运行结果

虽然回调函数能够实现异步,但是回调函数存在以下问题:

1) 回调地狱问题
异步多级依赖的情况下会层层嵌套,代码难以阅读的维护;

2)可能会造成多个异步在某一时刻获取所有异步的结果;

3) 异步不支持try/catch
回调函数是在下一事件环中取出,
所以一般在回调函数的第一个参数都是用来预置错误对象

4)不能通过return返回结果
回调地狱(图片来源于网络)

二、异步改进方案-Promise

2.1 什么是Promise?

promise,承诺。在代码中我们可以这么理解:此处我先许下个承诺,过了一定时间后我带给你一个结果。

那么,在这一段时间中做什么?
我们可以进行异步操作,比如请求网络数据、耗时运算、读写本地文件等

2.2 Promise的三种状态?

1) Pending 
   Promise对象实例创建时候的初始状态
2) Fulfilled 
   成功的状态
3) Rejected 
   失败的状态 

比如:你发邮件给老板说要加工资,这时候你就要"等待"他的邮件回复,他可以立马给你回复,如果同意了,表示"成功";如果不同意,表示"失败",当然他也可以一直不同意;但是这期间不影响你做其它事情。

在实际开发中,我们可以通过then 方法,来指定Promise 对象的状态改变时确定执行的操作,resolve 时执行第一个函数(onFulfilled),reject 时执行第二个函数(onRejected)。

来,一起认识下promise的几种操作方式和常用方法:

构建Promise
//  promise的方法会立刻执行;
//  两个输出都会打印
let promise = new Promise(() => {
     console.log('喜欢IT');
});
console.log('就上撩课');
promise也可以代表未来的一个值

一个promise实例可以多次调用then,当成功后会将结果依次执行。

let  promise = new Promise((resolve, reject) => {
    ajax.get(BASEURL + 'api/goods/', (err, data)=>{
        if (err) return reject(err);
        resolve(data);
    })
});
promise.then(data => {
    console.log(data);
});
promise.then(data => {
    console.log(data);
});
promise也可以代表一个不用返回的值
 // 代表一个用于不会返回的值
let  promise = new Promise((resolve, reject) => { });
promise.then(data => {
    console.log(data);
});
Promise.resolve

返回一个Promise实例,这个实例处于resolve状态。

Promise.resolve('成功获取结果').then(data=>{ 
    console.log(data);
});
Promise.reject

返回一个Promise实例,这个实例处于reject状态。

Promise.reject('获取结果失败').then(data=>{ 
   console.log(data); 
},err=>{ 
console.log(err); 
})
Promise.race

该方法用于接收一个数组,数组内都是Promise实例,返回一个Promise实例,这个Promise实例的状态转移取决于参数的Promise实例的状态变化。

当参数中任何一个实例处于resolve状态时,返回的Promise实例会变为resolve状态。如果参数中任意一个实例处于reject状态,返回的Promise实例变为reject状态。

Promise.race(
    [readFiles('./a.txt'),
    readFiles('./b.txt')]).then(data=>{
    console.log({data})
},(err)=>{
    console.log(err)
});

2.3 案例实操

我们再用promise实现上面发邮件加工资的案例:

情况一 :

在一定时间后(假设5s后),老板回复了邮件,可以是以下两种情况:

let addWages = ()=>{
    return new Promise((resolve, reject) => {
        setTimeout(function  ()  {
            // 公司账户余额
            let  currentMoney = 9999999999;
            // 公司账户余额 > 100w
            if (currentMoney > 1000000) {
                resolve('同意加薪');
            } else {
                resolve('不同意加薪');
            }
        }, 5000)
    })
};
addWages().then(data => {
    console.log(data);
}, data => {
    console.log(data);
});

//  运行结果:同意加薪
情况二 :

公司账户已经没钱,没法加工资了,表现形式如下:

let addWages = ()=>{
    return new Promise((resolve, reject) => {
        throw new Error('你表现不够优秀!');
    })
};
addWages().then(data => {
    console.log(data);
}, data => {
    console.log('这里输出:' + data);
});

我们可以采用then的第二个参数捕获reject返回结果或者捕获失败,当然也可以通过.catch函数进行捕获。

三、promise可以解决回调函数带来的问题

前面的案例描述已经验证了promise支持catch,此外,通过promise也能够返回结果给外部。我们再一起看看promise如何解决回调地狱和同步异步结果。

3.1 解决回调地狱

案例场景:在文档a.txt中存放正文档b.txt的路径,在文档b.txt中存放正文档c.txt的路径, 我们要取出文档c.txt里面的内容。

构造函数实现:

/* a.txt -> b.txt -> c.txt -> 输出内容*/
let fs = require('fs');
let readFiles = ()=>{
    // 回调1
    fs.readFile('./a.txt','utf8', (err,data)=>{
        if(err) return console.log(err);
        // 回调2
        fs.readFile(data,'utf8',function(err,data){
            if(err) return console.log(err);
            // 回调3
            fs.readFile(data,'utf8',function(err,data){
                if(err) return console.log(err);
                console.log(data); 
            })
        })
    })
};

/*
调用输出结果:
喜欢IT, 就上撩课(itlike.com) 
*/
readFiles();

通过promise解决回调地狱:

let fs = require('fs');

// 1. 初始化promise
let readFiles =(filePath)=>{
    return new Promise((resolve,reject)=>{
        fs.readFile(filePath,'utf8',(err,data)=>{
            if(err) return reject(err);
            resolve(data);
        })
    })
};

// 2. 类似于链式的调用方式
readFiles('./a.txt').then((data)=>{
    return readFile(data);
}).then((data)=>{
    // 获取b.txt中内容
    return readFile(data);
}).then((data)=>{
    // 输出c.txt中内容
    console.log(data)  
}).catch((err)=>{
    console.log(err)
});

3.2 在同一时刻同步所有异步产生的结果

该场景在实际开发中有很多应用场景,比如:我们要提交一个操作时,需要结合之前的两个异步请求的结果才能进行。

再比如:你要进行下一个运算时,需要前面两个异步运算的结果才能进行。我们还是通过读取文件的案例来进行举例。

常规方式实现:

let fs = require('fs');
// 1. 统一输出所有异步产生的结果
let allContent = {};
let logAllContent = (key,data)=>{
    allContent[key] = data;
    if(Object.keys(allContent).length === 2){
        console.log(allContent)
    }
};

// 2. 分别异步读取文件中的内容
fs.readFile('./a.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    logAllContent(data);
});
fs.readFile('./b.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    logAllContent(data);
});

这样的方式虽然解决了问题,但是你不知道最终结果是在哪个异步函数中输出,而且你需要在所有的异步函数中都去调用打印方法。

promise方式大大简化:

借助promise.all()方法,不管哪个promise谁先完成,该方法会按照数组里面的顺序将结果返回。

let fs = require('fs');
let readFiles = (filePath)=>{
    return new Promise(function(resolve,reject){
        fs.readFile(filePath,'utf8', (err,data)=>{
            if(err) return reject(err);
            resolve(data);
        })
    })
};

Promise.all(
    [readFiles('./a.txt'), 
     readFiles('./b.txt')]
).then(([data])=>{
    console.log({data})
});

后续

借助Promise已经可以帮助我们很好解决异步编程的问题,但还不是那么的行云流水、一气呵成,我们更希望编写异步代码能够像写同步代码一样直观、简单。

在下一篇我们会讲些更好、更灵活的异步编程方案,敬请期待。获取资料、交流可加我微信:yejh9522 一起探讨学习。

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

推荐阅读更多精彩内容