前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Node.js异步编程进化论

Node.js异步编程进化论

作者头像
童欧巴
修改2020-05-08 17:13:16
8360
修改2020-05-08 17:13:16
举报
文章被收录于专栏:前端食堂前端食堂

微信搜索【前端食堂】你的前端食堂,记得按时吃饭。

本文已收录在前端食堂 Github https://github.com/Geekhyt/front-end-canteen,感谢Star。

Node.js异步编程callback

我们知道,Node.js中有两种事件处理方式,分别是callback(回调)和EventEmitter(事件发射器)。本文首先介绍的是callback

error-first callback 错误优先是Node.js回调方式的标准。

第一个参数是error,后面的参数才是结果。

我们以现实生活中去面试来举个?,面试成功我们漏出洋溢的笑容,面试失败我们就哭并尝试找到失败的原因。

代码语言:javascript
复制
try {
    interview(function() {
    	console.log('smile');
    });
} catch(e) {
    console.log('cry', e);
}
function interview(callback) {
    setTimeout(() => {
        if (Math.random() < 0.1) {
            callback('success');
        } else {
            throw new Error('fail');
        }
    }, 500);
}

如上代码运行后,try/catch并不像我们所想,它并没有抓取到错误,错误反而被抛到了Node.js全局,导致程序崩溃。(是由于Node.js的每一个事件循环都是一个全新的调用栈Call Stack

为了解决上面的问题,Node.js官方形成了如下规范:

代码语言:javascript
复制
interview(function (res) {
    if (res) {
        return console.log('cry');
    }
    console.log('smile');
})
function interview (callback) {
    setTimeout(() => {
        if (Math.random() < 0.8) {
            callback(null, 'success');
        } else {
            callback(new Error('fail'));
        }
    }, 500);
}

回调地狱Callback hell

XX大厂有三轮面试,看下面的?

代码语言:javascript
复制
interview(function (err) {
    if (err) {
        return console.log('cry at 1st round');
    }
    interview(function (err) {
        if (err) {
            return console.log('cry at 2nd round');
        }
        interview(function (err) {
            return console.log('cry at 3rd round');
        })
        console.log('smile');
    })
})
function interview (callback) {
    setTimeout(() => {
        if (Math.random() < 0.1) {
            callback(null, 'success');
        } else {
            callback(new Error('fail'));
        }
    }, 500);
}

我们再来看并发情况下callback的表现。

同时去两家公司面试,当两家面试都成功时我们才会开心,看下面这个?

代码语言:javascript
复制
var count = 0;
interview(function (err) {
    if (err) {
        return console.log('cry');
    }
    count++;
})
interview(function (err) {
    if (err) {
        return console.log('cry');
    }
    count++;
    if (count) {
        //当count满足一定条件时,面试都通过
        //...
        return console.log('smile');
    }
})
function interview (callback) {
    setTimeout(() => {
        if (Math.random() < 0.1) {
            callback(null, 'success');
        } else {
            callback(new Error('fail'));
        }
    }, 500);
}

异步逻辑的增多随之而来的是嵌套深度的增加。如上的代码是有很多缺点的:

  • 代码臃肿,不利于阅读与维护
  • 耦合度高,当需求变更时,重构成本大
  • 因为回调函数都是匿名函数导致难以定位bug

为了解决回调地狱,社区曾提出了一些解决方案。

1.async.js npm包,是社区早期提出的解决回调地狱的一种异步流程控制库。 2.thunk 编程范式,著名的co模块在v4以前的版本中曾大量使用Thunk函数。Redux中也有中间件redux-thunk

不过它们都退出了历史舞台。

毕竟软件工程没有银弹,取代他们的方案是Promise

Promise

Promise/A+规范镇楼,ES6采用的这个规范实现的Promise。

Promise 是异步编程的一种解决方案,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

简单说,Promise就是当前事件循环不会得到结果,但未来的事件循环会给到你结果。

毫无疑问,Promise是一个渣男。

Promise也是一个状态机,只能从pending变为以下状态(一旦改变就不能再变更)

  • fulfilled(本文称为resolved)
  • rejected
代码语言:javascript
复制
// nodejs 不会打印状态
// Chrome控制台中可以
var promise = new Promise(function(resolve, reject){
    setTimeout(() => {
        resolve();
    }, 500)
})
console.log(promise);
setTimeout(() => {
    console.log(promise);
}, 800);
// node.js中
// promise { <pending> }
// promise { <undefined> }
// 将上面代码放入闭包中扔到google控制台里
// google中
// Promise { <pending> }
// Promise { <resolved>: undefined }

Promise

  • then
  • catch

resolved状态的Promise会回调后面的第一个.then rejected状态的Promise会回调后面的第一个.catch 任何一个rejected状态且后面没有.catch的Promise,都会造成浏览器/node环境的全局错误。

Promise比callback优秀的地方,是可以解决异步流程控制问题。

代码语言:javascript
复制
(function(){
    var promise = interview();
    promise
        .then((res) => {
            console.log('smile');
        })
        .catch((err) => {
            console.log('cry');
        });
    function interview() {
        return new Promise((resoleve ,reject) => {
            setTimeout(() => {
               if (Math.random() > 0.2) {
                   resolve('success');
               } else {
                   reject(new Error('fail'));
               }
            }, 500);
        });
    }
})();

执行thencatch会返回一个新的Promise,该Promise最终状态根据thencatch的回调函数的执行结果决定。我们可以看下面的代码和打印出的结果:

代码语言:javascript
复制
(function(){
  var promise = interview();
  var promise2 = promise
      .then((res) => {
          throw new Error('refuse');
      });
      setTimeout(() => {
          console.log(promise);
          console.log(promise2);
      }, 800);
  function interview() {
      return new Promise((resoleve ,reject) => {
          setTimeout(() => {
             if (Math.random() > 0.2) {
                 resolve('success');
             } else {
                 reject(new Error('fail'));
             }
          }, 500);
      });
  }
})();
// Promise { <resolved>: "success"}
// Promise { <rejected>: Error:refuse }

如果回调函数最终是throw,该Promise是rejected状态。 如果回调函数最终是return,该Promise是resolved状态。 但如果回调函数最终return了一个Promise,该Promise会和回调函数return Promise状态保持一致。

Promise解决回调地狱

我们来用Promise重新实现一下上面去大厂三轮面试代码。

代码语言:javascript
复制
(function() {
    var promise = interview(1)
        .then(() => {
            return interview(2);
        })
        .then(() => {
            return interview(3);
        })
        .then(() => {
            console.log('smile');
        })
        .catch((err) => {
            console.log('cry at' + err.round + 'round');
        });
    function interview (round) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.random() > 0.2) {
                    resolve('success');
                } else {
                    var Error = new Error('fail');
                    error.round = round;
                    reject(error);
                }
            }, 500);
        });
    }
})();

与回调地狱相比,Promise实现的代码通透了许多。

Promise在一定程度上把回调地狱变成了比较线性的代码,去掉了横向扩展,回调函数放到了then中,但其仍然存在于主流程上,与我们大脑顺序线性的思维逻辑还是有出入的。

Promise处理并发异步

代码语言:javascript
复制
(function() {
    Promise.all([
        interview('Alibaba'),
        interview('Tencent')
    ])
    .then(() => {
        console.log('smile');
    })
    .catch((err) => {
        console.log('cry for' + err.name);
    });
    function interview (name) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.random() > 0.2) {
                    resolve('success');
                } else {
                    var Error = new Error('fail');
                    error.name = name;
                    reject(error);
                }
            }, 500);
        });
    }
})();

上面代码中的catch是存在问题的。注意,它只能获取第一个错误

Generator

Generator和Generator Function是ES6中引入的新特性,是在Python、C#等语言中借鉴过来。

生成器的本质是一种特殊的迭代器。

代码语言:javascript
复制
function * doSomething() {}

如上所示,函数后面带“*”的就是Generator。

代码语言:javascript
复制
function * doSomething() {
    interview(1);
    yield; // Line (A)
    interview(2);
}
var person = doSomething();
person.next();  // 执行interview1,第一次面试,然后悬停在Line(A)处
person.next();  // 恢复Line(A)点的执行,执行interview2,进行第二次次面试

next的返回结果

第一个person.next()返回结果是{value:'', done:false} 第二个person.next()返回结果是{value:'', done:true}

关于next的返回结果,我们要知道,如果done的值为true,即代表Generator里的异步操作全部执行完毕。

为了可以在Generator中使用多个yield,TJ Holowaychuk编写了co这个著名的ES6模块。co的源码有很多巧妙的实现,大家可以自行阅读。

async/await

Generator的弊端是没有执行器,它本身是为了计算而设计的迭代器,并不是为了流程控制而生。co的出现较好的解决了这个问题,但是为什么我们非要借助于co而不直接实现呢?

async/await被选为天之骄子应运而生。

async function 是一个穿越事件循环存在的function。

async function实际上是Promise的语法糖封装。它也被称为异步编程的终极方案-以同步的方式写异步

await关键字可以"暂停"async function的执行。

await关键字可以以同步的写法获取Promise的执行结果。

try/catch可以获取await所得到的任意错误,解决了上面Promise中catch只能获取第一个错误的问题。

async/await解决回调地狱

代码语言:javascript
复制
(async function () {
  try {
      await interview(1);
      await interview(2);
      await interview(3);
  } catch (e) {
      return console.log('cry at' + e.round);
  }
  console.log('smile');
})();

async/await处理并发异步

代码语言:javascript
复制
(async function () {
    try {
        await Promise.all([interview(1), interview(2)]);
    } catch (e) {
        return console.log('cry at' + e.round);
    }
    console.log('smile');
})();

无论是相比callback,还是Promiseasync/await只用短短几行代码便实现了异步流程控制。

遗憾的是,async/await最终没能进入ES7规范(只能等到ES8),但在Chrome V8引擎里得以实现,Node.js v7.6也集成了async函数。

实践经验总结

在常见的Web应用中,在DAO层使用Promise较好,在Service层使用async函数较好。

参考:

  • 狼书-更了不起的Node.js
  • Node.js开发实战

微信搜索【前端食堂】你的前端食堂,记得按时吃饭。

本文已收录在前端食堂 Github https://github.com/Geekhyt/front-end-canteen,感谢Star。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端食堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Node.js异步编程callback
    • 回调地狱Callback hell
    • Promise
      • Promise解决回调地狱
        • Promise处理并发异步
        • Generator
        • async/await
          • async/await解决回调地狱
            • async/await处理并发异步
              • 实践经验总结
              相关产品与服务
              消息队列 TDMQ
              消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档