[小白教程] Javascript Callback以及Promise/async/await 一文通

在这里插入图片描述

一、最初

一切从 Javascript 是一门异步编程语言说起,比如这种最简单的:

  let n = 0
  function f1() {
    
    
    setTimeout(function () {
    
    
      n++
    }, 1000)
  }

  f1()
  console.log(n)

可能直觉上会觉得最终n=1,但实际上打印出来的是0,因为尽管调用了f1函数,但主进程并不会等待其内部的定时器到时之后再继续往下执行,而是直接就console.log(n)了,而此时n=0。


二、Callback (回调函数) 方案

为了解决这个问题,人们想出了回调函数,也就是说当一个函数需要一定的时间去运行,那么就等它执行完毕之后再反向调用外部的某个函数,将上例改写如下:

  let n = 0
  function f1(function_name) {
    
    
    setTimeout(() => {
    
    
      n++
      function_name(n);
    }, 1000);
  }

  function f2 (ret) {
    
    
    console.log(ret)
  }

  f1(f2)

这次打印出来就是n=1了,f1函数多了一个参数function_name,这个参数是告诉f1,当函数运行完毕后,将返回值通过function_name这个函数携带出来,这是一种反向调用,也就是所谓的 “回调函数 (callback function)”。

  • 匿名函数
    在调用回调函数的时候,可以不指定函数名,将函数体直接嵌到形参里面,还是上面那个例子,可以写成这样:
  let n = 0
  function f1(function_name) {
    
    
    setTimeout(() => {
    
    
      n++
      function_name(n);
    }, 1000);
  }

  f1(function (ret) {
    
     
    console.log(ret) 
  })

这种写法并不难理解,就是将函数f2直接嵌入了f1的参数列表里。


三、回调地狱

尽管回调函数解决了异步执行返回值的确定性问题,但是如果程序逻辑需要进行多重异步操作,就会导致经典的 “回调地狱”,比如这样:

  let n = 0
  function f1(function_name) {
    
    
    setTimeout(() => {
    
    
      n++
      function_name(n);
    }, 1000);
  }

  const f3 = f2 = f1

  f1(function (ret) {
    
    
    f2(function (ret) {
    
    
      f3(function (ret) {
    
    
        console.log(ret)
      })
    })
  })

最终结果是n=3,以上演示只写了三层,实际上超过3层的逻辑在实际生活中是很常见的,回调地狱将会让代码的可读性降低,复杂性大大增加。


四、Promise 方案

Promise是ES6中的新特性,其最主要是目的就是要解决“老式”的回调函数所产生的“地狱”问题。将上例改写如下:

  let n = 0
  function f1() {
    
    
    return new Promise((resolve, reject) => {
    
    
      setTimeout(() => {
    
    
        n++
        resolve(n)
      }, 1000);
    })
  }

  const f3 = f2 = f1

  f1()
    .then(() => f2())
    .then(() => f3())
    .then(
      ret => {
    
    
        console.log(ret)
      })

最终结果和回调版本是一样的,依然是 n=3 ,但书写和理解性就比前者容易多了,通过.then()来形成所谓的 “链式调用“,即在上一个操作执行成功之后,开始下一个的操作。

使用上需要注意的有两点,首先用 return new Promise((resolve, reject) => { ... }) 将耗时操作包裹起来,而 resolve (ret) 的意义则是将返回值携带出来,类似 return (ret)

PS:上例中没有用到第二个参数 reject ,通过它将抛出一个拒绝执行的信息,比如我们将上例的resolve(n) 改为 reject(n) ,链式调用将会中断, f2、f3将不会被执行。reject 一般与catch 错误捕捉机制协同使用,当然如果没有这个需求的话,reject 可以省略。

  • 箭头函数
    上例中采用了很多 => 这样的 ”箭头“ 函数,对应老式的function函数声明方式,以下是对照:

  • 带名字的函数
    不带参数: abc = () => { 函数体 } 等于 function abc() { 函数体 }
    带参数: abc = (v1, v2) => { 函数体 } 等于 function abc(v1, v2) { 函数体 }

  • 匿名函数
    不带参数: () => { 函数体 } 等于 function () { 函数体 }
    带参数: (v1, v2) => { 函数体 } 等于 function (v1, v2) { 函数体 }
    PS: 如果只有一个变量,可以不带括号,比如:

  abc = v1 => {
    
    
    console.log(v1)
  }

或者如上例的:

  ret => {
    
    
    console.log(ret)
  }

五、async 与 await

通过async 和 await,可以写出更加像 “同步式” 编程语言那样的代码,例如:

  let n = 0
  function f1() {
    
    
    return new Promise((resolve) => {
    
    
      setTimeout(() => {
    
    
        n++
        resolve(n)
      }, 1000);
    })
  }

  const f3 = f2 = f1

  async function run() {
    
    
    await f1()
    console.log(n)
    await f2()
    console.log(n)
    await f3()
    console.log(n)
  }

  run()

注意,async/wait 这种方案不能直接在主程序的最外层调用,而是必须指定一个 “执行函数”, 并在它前面添加一个 async 关键字,本例中是 async function run() ,而在这个执行函数体内,需要调用的函数前面加一个await关键字,表示将同步执行该函数,上例中f1、f2、f3前面都添加了await,则表示这三个函数是一个挨着一个按顺序执行的。想象一下,在最初的Javascript例子中,这段代码一定是一次性输出3个0,而不是像现在这样间隔1秒输出:1、2、3。这种体验其实跟同步式编程语言已经非常接近了。


后记

本文对Javascript的异步执行机制,回调函数,Promise机制以及箭头函数等内容只讲了一点皮毛,纯属抛砖引玉。本文的目的也并非深入讨论这些知识点,而是想让完全不熟悉的同学能尽快先 “体验” 一下,并对这些内容产生一些感性认识,需要深入学习的同学可以参考以下链接:

Promise:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arrow_functions

https://juejin.cn/post/7108187709076111367

https://juejin.cn/post/7235177983312216125

Guess you like

Origin blog.csdn.net/rockage/article/details/131016870