gongmw / blog

从现象 看本质
117 stars 12 forks source link

Node.js中异步流程控制解析 #20

Open gongmw opened 4 years ago

gongmw commented 4 years ago

对于异步编程可能很多朋友了解不是很熟悉或者原来同步代码写习惯了,可能会感觉很别扭有些地方不好理解, 其实这是很正常的,毕竟思维惯性不是一下子就可以改变的,下面我通过本篇文章的一些分享和见解 和大家一起熟悉下在Node中异步编程的一些理解和解决方案,希望有所帮助。

在原来我们写的代码都是同步自上而下的运行很清晰明了:例如我们在晚上要做三件事:

log("学习1小时");
log("点外卖-吃外卖");
log("看视频1小时");

假如每执行一件事都要一段时间完成后,最后才可以睡觉,现在我们用异步的思维来稍微调整一下,在学习前两分钟或学习完看视频的时候我们可以点下单,等学习完后外卖来了,在打开手机,空调一开happy一小时,就可以早一点睡觉了节约了很多时间,其实在我们的工作和生活中,到处充满了异步的方式。

然而在开发里面异步的出现带来了一个不太好理解的东西就是回调,回调的出现增加我们对软件设计开发的一些困难和理解,但随着社区的进步和发展 越来越好的解决方案慢慢出现消除了我们很多困惑,下面一起来看看吧。

1.Callback

Callback是比较早的一种解决方案,简洁点的定义就是在一个函数中调用另外一个函数就是callback

笔者早期也经历过一段不堪的使用express框架里面的callback的往事,因为最开始对这种写法也不是很明白,看到很多包文件和函数文件里面返回的都是回调函数,各种callback,搞得自己迷迷糊糊的,但是居然被大家熟知并运用,那我们就要学会尊重它本身,接受这种风格,除非你有更好的选择或者有能力改变它。

我们比较常用的一种node callback处理写法

 fs.readFile(file, function(err, data) {
    if (err) {
      return cb(err);
    }
    cb(null, data);
  });

很多回调回调到最后就成了这种不堪入目的样子了

 step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
           step4(value2, function(value4) {
              step5(value2, function(value5) {
                   // Do something with value3
            });
          });
       });
    });
});

回调的问题在于它会创建一个称为“回调地狱”的东西。基本上,你在函数内开始嵌套函数,并且开始变得很难阅读代码

并且在一些情况下回调函数写的没组合好可能还会导致引用关系错乱而不被释放掉,在V8中是没办对这种对象进行垃圾回收的,所以可能会导致内存泄漏,重复执行的时候内存都会增加。

介于一些写法和易错等一些特点,于是引入了Promises来进一步解决这些问题,但是对于Promise运用上更多的是提供了一种代码结构和流程控制来解决一些问题。

2.Promise

Promise可能是现在大家比较熟知和运用的一种方式了,Promise最早是在commonjs社区提出来的,然后在这个基础上,提出了promise/A+规范,也就是现在的业内推行的规范,在我们的项目开发和面试中Promise都是一个很重要的点,所以深入理解和掌握这个知识点是很有必要的。

2.1 定义

Promise对象表示异步操作的最终完成(或失败)及其结果值

2.2 理解

Promise翻译中文的意思就是承诺,让我想起了一动漫里面的刺客一诺千金,接受任务去刺杀一人,那刺客的结果就可能只有三种情况:

在比如在我们日常生活中的一些案例:

想一下你原来读书的时候,你爸和你保证说,期末考100给你买电脑

但你现在不知道你是否能收到那部电脑,因为对于期末考100这个事你没有明确的把握

所以你爸给了你一个承诺,这个承诺同样有3种结果:

从这些可以看出,Promises(承诺)不会立即返回值。它在等待成功或失败,然后相应地返回。

另外承诺是可以连接后续的故事的,比如你在考试之前告诉了你朋友,当你考了100你爸给你买了电脑,就让他们来你家打游戏,所以我们使用promise.then()后面可以连续的传递下去。

希望我的两个小案例对你理解promise有一些帮助。

2.3 运用

创建 Promise对象本身是一个构造函数,用来生成Promise实例的例如:

new Promise( /* executor */ function(resolve, reject) { ... } );

构造函数接受一个称为执行程序的函数。该executor函数接受两个参数,resolve和reject这两个参数又同样是函数。 通常使用Promise来简化异步操作的处理,例如文件操作,API调用,DB调用,IO调用等。

这些异步操作发生在executor函数中。如果异步操作成功,则resolve通过promise的创建者调用函数来返回预期结果。 同样,如果出现一些意外错误,则通过调用该reject函数来传递原因。

现在我们知道了如何创建承诺。为了更好理解,创建一个简单的承诺。

const isComputer = true
const promise  = new Promise(function(resolve, reject) {
  if (isComputer) {
    resolve({
      name:  '三星',
      version: '10.4'
    };)
  } else {
    reject(new Error('被揍了'))
  }
});

调用执行:

promise
  .then(function(done) {
    //  resolve() 的结果
  })
  .catch(function(error) {
    //  reject() 的结果
  });

由于promise具有连续性所以可以传递下去

promise
  .then(function(done) {
  })
   .then(function(done1) {
  })
    .then(function(done2) {
  })
  .catch(function(error) {
  });

另外在ES2018引入了一种.finally()方法,无论结果如何,该方法都可以运行任何最终逻辑,例如清理,关闭数据库连接等操作可以写在里面

promise
  .then(function(done) {
    //  resolve() 的结果
  })
  .catch(function(error) {
    //  reject() 的结果
  }).finally(function() {
     // 成功失败都会执行
  });

此外Promise对象中还有四种常用的静态方法,可以帮助我们轻松创建成功或失败的promise以及执行多个调用。

  1. 当async1, async2, async3的状态都变成resolve,async的状态才会变成resolve

  2. 只要async1, async2, async之中有一个被reject,async的状态就会变成reject

    • Promise.race(): Promise.race()和Promise.all()很相似,不同之处在于它会在第一个Promise执行成功或失败后立即返回状态。

这里没有对promise的详细知识点进行细分扩展,主要是带大家理解转换和项目开发,想了解运用其他知识点的同学可以查看一些文档: MDN promise- 阮一峰promise对象

2.4 易错点

在使用promise会容易出问题的一些地方,新手和一些有经验的朋友可能都有遇到过。

  1. 虽然使用promise让原来的回调代码不在那么循环嵌套,但他的连续性在.then()里面的方法一样可以无限嵌套,看起来一样凌乱不堪!, 但好在你可以return每个promise结果,这样代码顺序读起来会清晰很多。

  2. 可能一些朋友才开始使用promise的时候会出现一个Uncaught TypeError: undefined is not a promise一个错误,这个错误可能是你没有使用new关键字创建promise,除非你需要直接调用Promise.resolve,Promise.reject,Promise.all,Promise.race()这些方法new是没有必要的

  3. 这一个是大部分初学者刚开始都会犯的错误,把promise一些异步操作和forEach循环放在一起使用,很可能就没有达到期望的结果或者出现错误, 具体案例放在下面async await中一起展示,使用他们都有同样的问题,async await也只是Promises的语法糖封装。

  4. promise错误处理问题是一个比较大的问题,有些朋友会自信的感觉自己写的promise处理是没什么问题的,有时可能会有意识无意识的忘记添加catch,和错误的抛出捕获。

3.async await

随着社区的进步终于出现了一种让异步代码写起来最像同步的方式,它使你的异步代码看起来更像同步过程的代码,让人更容易理解

原来定义一个pormise可能要写这么多

new Promise(function(resolve, reject) {
    resolve()
});

现在要简约清晰很多

async function Fname() {
  return 'Fname';
}

3.1 规则经验

对于async await的理解和使用总结了一些规则和经验分享给大家:

虽然我们说了async await那么多好处,那么难道promise就要被无情的淘汰吗,当然不是了,就像我们每个人一样,可能你代码没别人写得好,但你其他方面比他强啊,比他有才啊,所以大家都要对自己又信心,相信自己是很给力的天生我材必有用

每一个语言和框架都有他适合的常见和运用,但相互合作也可能会擦出不一样的火花,对于async await 与promise也不例外

async/ await仍然依赖于Promises,而Promises最终依赖于回调,因果还是轮回的

在某些时候,您将尝试在同步循环内调用异步函数会出现的问题。例如:

async function main() {
  const slogan = ['yi', 'qi', 'xue', 'node']
  let result = ''
  console.log('start student')
  slogan.forEach(async word => {
    console.log('loading1', word)
    result = await join(result, word)
    console.log('loading2', word)
  })
  console.log('end student')
  console.log('===', result)
}

async function join(result, word) {
  return result + word
}

main()

如果这样使用的话不会正常执行出我们想要的:打印出来的值

start student
loading1 yi
loading1 qi
loading1 xue
loading1 node
end student
=== 
loading2 yi
loading2 qi
loading2 xue
loading2 node

明显是不符合我们的预期的,loading2没有紧接着loading1下面执行result值也没有打印出来,这是因为当我们使用forEach的时候出现的一些问题,可以看下 一个简易的forEach实现

Array.prototype.forEach = function (callback) {
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this);
  }
};

可以看到没有等待callback()返回。因此promise链可能会不连续,导致一些问题的产生。

forEach是在js引入promise或async-await概念之前实现的,因此它不能与promise或async- wait一起工作,这不是错误。这只是使用不兼容结构的一个例子

知道了forEach循环的实现与promise函数和异步函数不兼容。但是可以通过很多方式来实现我们需要的结果

虽然也存在一些缺陷,但async/ await是目前处理异步写法最优雅的一种方式了,有兴趣的同学可以直接下载项目进行测试,代码我都上传上去了。

小结

文章更多的是帮助理解和运用经验的分享,用一些生活的案例来帮助大家理解和一些在实际项目中可能会出现的问题和注意点和大家聊聊,希望可以对有些同学有所帮助,如有补充和指导欢迎在留言区指出。