promise 和 async/await

本章视频讲解在这里,连续看6个视频

callback hell

看一个例子,要求代码实现:

等待2秒后执行操作1,完成后再等待2秒执行操作2 ,完成后再等待2秒执行操作3 ,

如果是 Python 语言代码是这样写的

import time

print('这里是等待2秒前的代码')
time.sleep(2) # 等待2秒
print('这里是执行操作1代码')
time.sleep(2) # 等待2秒
print('这里是执行操作2代码')
time.sleep(2) # 等待2秒
print('这里是执行操作3代码')

而 js是异步架构,需要使用回调

代码是这样写的

setTimeout(
    
  function() {
    console.log("这里是执行操作1代码");

    setTimeout(

      function() {
        console.log("这里是执行操作2代码"); 

        setTimeout(  

          function() {
            console.log("这里是执行操作3代码"); 
          }, 
          2000 
        )

      }, 
      2000 
    )

  }, 
  2000 
)

console.log("这里是设置后的代码");

很容易搞错匹配的符号和参数。

可见,回调这种写法也有缺点,主要是使得代码看起来凌乱复杂了。

这种多层的回调也被称为 戏称为 回调地狱 (callback hell)


为了解决这个问题, ES6 引进了 promiseasync、await

创建 Promise

js中有 Promise构造函数 ,用来创建 Promise 类型的对象

常见的用法是,传入一个函数作为该构造函数的参数,如下

var p = new Promise(
  function(resolve, reject) {
    console.log('执行函数1')
    resolve(1);
  }
)
console.log(p)


var p = new Promise(
  function(resolve, reject) {
    console.log('执行函数1')
    reject('错误原因:服务端返回异常');
  }
)
console.log(p)


var p = new Promise(
  function(resolve, reject) {
    console.log('执行函数2')
    setTimeout(() => resolve(2), 1000);
  }
)
console.log(p)


var p = new Promise(
  function(resolve, reject) {
    console.log('执行函数3')
    setTimeout(() => resolve({name:'白月黑羽'}), 1000);
  }
)
console.log(p)


var p = new Promise(
  function(resolve, reject) {
    console.log('执行函数4')
    setTimeout(() => reject(2), 1000);
  }
)
console.log(p)

里面的这个参数函数,被称之为 executor, 这里我们暂时叫 执行函数

执行函数 有两个参数 resolve, reject

resolve, reject也是函数,是由js引擎实现的,在回调执行 执行函数时传入

执行函数 作为构造函数的参数, 在创建Promise对象后,会被立即执行


在执行执行函数过程中:


这个两个函数参数 resolve, reject 其实是包含了创建的promise对象的闭包。

所以调用时,自然知道应该设置 哪个promise对象的 resolve、reject结果

这样,不管执行函数里面的 resolve、reject函数 是立即执行、还是延期执行(后续的底层循环触发回调),都能设置好promise对象的结果

Promise的then方法

当一个 promise 成功了, 或者失败了, 就是有了最终的结果,我们的代码怎么对这个最终的结果进行处理呢?

就是通过这个 promise的 then 方法

参数 - 处理函数

then 方法接受 两个函数参数 ,

第1个函数参数,称之为 onFulfilled ,是用来处理 成功结果的函数。接受的参数,自然就是 primise resolve 的结果

第2个函数参数,称之为 onRejected ,是用来处理 失败结果的函数。接受的参数,自然就是 primise reject 的结果

如下

// 示例1
new Promise(
  function(resolve, reject) {
    setTimeout(() => resolve(2), 1000);
  }
)
.then(
  // 第1个参数函数,onFulfilled,处理成功结果
  function(result) {
    console.log(result);
  },
  // 第2个参数函数,onRejected,处理失败结果
  function(reason) {
    console.log(reason);
  }
)


// 示例2
new Promise(
  function(resolve, reject) {
    setTimeout(() => reject({msg:'参数错误', details:'用户名不存在'}), 1000);
  }
)
.then(
  // onFulfilled
  function(result) {
    console.log(result);
  },
  // onRejected
  function(reason) {
    console.log(reason);
  }
)


// 示例3
new Promise(
  function(resolve, reject) {
    resolve(1)
  }
)
.then(
  // onFulfilled
  function(result) {
    console.log(result);
  },
  // onRejected
  function(reason) {
    console.log(reason);
  }
)


执行到 .then(...) 时 ,Promise对象添加了2个新的属性,onFulfilled, onRejected,分别对应resolve时执行的函数、 reject时执行的函数

这时,


这里有个注意点:

当Promise对象被调用了 resolve或者reject有结果了, 并不会立即调用 对应的 onFulfilled 或者 onRejected 属性对应的处理函数。

而是安排在下个js底层循环调用

所以,运行下面代码

new Promise(
  function(resolve, reject) {
    console.log('1');
    resolve('b')
  }
)
.then(
  function(resolve, reject) {
    console.log('2');
  }
)
console.log('3')

结果是:

1
3
2

而不是

1
2
3

返回值 - Promise

特别要注意的是:

then方法的 的返回值 也是一个promise ,为了方便表述,我们称之为 then promise ,then属于的promise 称之为 原promise

原promise 有了结果后, 根据成功还是失败,会选择它then的两个参数 处理函数 之一执行,

执行 处理函数 的过程中,如果



then 方法的详细描述可以参考MDN文档

Promise 链

既然then 返回的也是一个promise,那么自然也可以对它调用 then 方法了。

第2次调用then返回的也是promise,自然也可以继续对它调用 then 方法了,

依次类推, 可以一直调用下去,形成一个 Promise 链

直到 某个环节promise的 then 处理函数, 处理完这个promise的结果后,不需要再 return 新的结果 供后续处理了。

处理函数不返回任何结果(也不throw结果),虽然对应的then promise 还是会有一个值为undefined的成功结果,但是没有有处理它的意义, 后续也就不需要继续再加 then 方法了。

示例代码如下

var p = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(2), 1000);
})
.then(function(result) {
  console.log(result);
  return result * 2;
})
.then(function(result) {
  console.log(result);
  return result * 2;
})
.then(function(result) {
  console.log(result);
})

console.log('后续代码执行')

执行结果是

后续代码执行
2
4
8

说明了Promise链所有 then 里面的代码都是后面的事件循环执行的

所有 then 里面的代码 都是在 promise链后面的代码执行 之后 再执行的


当然,then里面的函数可以是另外的延时处理,比如

new Promise( (r,j) => {setTimeout(() => r(2), 1000) })
.then(function(ret) {
  console.log(ret);
  return new Promise((r,j)=>{setTimeout(() => r(ret*2), 1000);})
})
.then(function(ret) {
  console.log(ret);
  return new Promise((r,j)=>{setTimeout(() => r(ret*2), 1000);})
})
.then(function(ret) {
  console.log(ret);
})

这样就避免了前面说的层层缩进的回调地狱的问题

错误处理

参考 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#error_propagation

primise 如果reject,结果失败,本质上就是会抛出错误,如果then没定义 第2个参数函数,像下面这样

new Promise(
  function(resolve, reject) {
    reject({msg:'参数错误'});
  }
)
.then(
  // 第1个参数函数,处理成功结果
  function(result) {
    console.log(result);
  },
)

console.log('执行后续代码')

就会在promise reject 时,抛出错误。

但是这个错误不会中止影响下面的这条语句的执行

console.log('执行后续代码')

为什么?

因为 前面讲过, Promise对象resolve,reject了,对应的处理会在下个js底层循环执行

而当前继续 new Promise 后面的代码。



要对可能出现的错误情况做处理,

就需要 定义 then的 第2个参数函数 进行错误处理, 或者, 调用 catch 方法,


就像错误捕获那样,promise 链上 任意一个环节出现reject,

js引擎都会:


如果,某个then 已经有错误处理,并且resolve当前then Promise, 后续继续正常处理

如下

new Promise(function(resolve, reject) {
  setTimeout(() => reject(1), 1000);
})
.then(function(result) {
  console.log('then1-正确处理',result); 
  return result * 2;
})
.then(
  function(result) {
    console.log('then2-正确处理',result);
    return result * 2;
  },
  // 这里处理了
  function(result) {
    console.log('then2-错误处理',result);
    return result * 2;
  }
)
.then(function(result) {
  console.log('then3-正确处理',result);
})
.catch(reason =>{
  console.log('catch中捕获的错误:',reason)
})

如果,一直没有 then 有错误处理,就会由最终的catch方法捕获

如下

new Promise(function(resolve, reject) {
  setTimeout(() => reject(1), 1000);
})
.then(function(result) {
  console.log('then1-正确处理',result); 
  return result * 2;
})
.then(function(result) {
  console.log('then2-正确处理',result);
  return result * 2;
})
.then(function(result) {
  console.log('then3-正确处理',result);
})
.catch(reason =>{
  console.log('catch中捕获的错误:',reason)
})

async await

asyncawait 是ES6引进的异步关键字, 是 Promise 的语法糖

让异步处理写起来更简单

function foo() {
  // doSomething()返回一个promise
  doSomething()
  .then(result => doSomethingElse(result))
  .then(newResult => doThirdThing(newResult))
  .then(finalResult => console.log(`Got the final result: ${finalResult}`))
  .catch(failureCallback);
}

可以写成

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

函数定义时,前面 加上 async ,就是声明,这是一个 异步函数

只要函数内部要使用 await ,必须定义为异步函数

await 后面跟着的,通常是一个 promise对象, 比如返回 promise 的函数调用,

执行到await这里时,其实js引擎会从代码处返回 底层js循环,继续进行其它处理。

等 await的 promise 被resolve了,resolve的值作为await 的返回值,

然后再接着执行后面的代码。


看一个具体的例子

前面的promise示例,如果放在函数中调用,如下

function delayCall(){
  new Promise( (r,j) => {setTimeout(() => r(2), 1000) })
  .then(function(ret) {
    console.log(ret);
    return new Promise((r,j)=>{setTimeout(() => r(ret*2), 1000);})
  })
  .then(function(ret) {
    console.log(ret);
    return new Promise((r,j)=>{setTimeout(() => r(ret*2), 1000);})
  })
  .then(function(ret) {
    console.log(ret);
  })
}

delayCall()

使用 async,await 可以写成

async function delayCall(){
  var ret = await new Promise( (r,j)=>{setTimeout(()=>r(2), 1000) })
  console.log(ret);
  var ret = await new Promise( (r,j)=>{setTimeout(()=>r(ret*2), 1000) })
  console.log(ret);
  var ret = await new Promise( (r,j)=>{setTimeout(()=>r(ret*2), 1000) })
  console.log(ret);
}

delayCall()

效果相同,但是写法更容易理解。


ES2020支持了顶层代码直接使用 await,目前主流浏览器都已经支持

如下

var ret = await new Promise( (r,j)=>{setTimeout(()=>r(2), 1000) })
console.log(ret);
var ret = await new Promise( (r,j)=>{setTimeout(()=>r(ret*2), 1000) })
console.log(ret);
var ret = await new Promise( (r,j)=>{setTimeout(()=>r(ret*2), 1000) })
console.log(ret);

注意:顶层代码直接使用 await, 一定要放在模块化的js代码中。

如果是直接嵌入html,要加 type="module" 声明。

比如

<script type="module">
var ret = await new Promise( (r,j)=>{setTimeout(()=>r(2), 1000) })
console.log(ret);
</script>

否则会报错:await is only valid in async functions and the top level bodies of modules


如果await 后面的不是Promise对象,而是一个其它数据,

js引擎会把它转化为resolve结果为该值的Promise对象

如下

var ret = await 'HELLO'
console.log(ret)

调用异步函数,如果前面没有await, 那就只是调用,不等待其异步的返回值

如下

async function f1() {
  console.log('f1')
  return 1;
}


async function f2() {
  console.log('f2-1')
  ret = await f1()
  console.log('f2-2')
}


console.log('m1')
f2() // 不等待f2 resolve
console.log('m2')

运行结果是

m1
f2-1
f1
m2
f2-2

对比await等待返回值的代码如下

async function f1() {
  console.log('f1')
  return 1;
}


async function f2() {
  console.log('f2-1')
  ret = await f1()
  console.log('f2-2')
}


console.log('m1')
await f2()  // 等待f2 resolve
console.log('m2')

运行结果是

m1
f2-1
f1
f2-2
m2