1 つの記事で Promise/Async await を理解し、70% 以上のフロントエンド担当者に追いつく

青灯夜游
リリース: 2022-09-28 10:39:46
転載
2608 人が閲覧しました

今日は Promise についてお話します。著者は、初期の非同期コードのジレンマから始めて、Promise の出現によってどのような問題が解決されたか、非同期コールバック地獄に対する究極の解決策、そして async await のコア構文を実装します。 、async/await はジェネレーターの Promise の変形にすぎません。

1. 初期の非同期コードのジレンマ

  • 誰もが知っているように、js はシングルスレッドであり、時間のかかる操作は時間が来たら、キューから実行を取り出し、イベント ループの概念を設計します。作者も共有しています。以下で読むことができます。それを理解すると、promise をよりよく理解できます。
  • 要件を開始点として使用し、ネットワーク リクエスト (非同期操作) をシミュレートします
    • ネットワーク リクエストが成功した場合は、成功したことを通知します
    • Ifネットワーク リクエストが失敗しました。失敗したと言われました

##1.1 優れたスマートなアプローチ
function requestData(url) {
  setTimeout(() => {
    if (url === 'iceweb.io') {
      return '请求成功'
    }
    return '请求失败'
  }, 3000)
}

const result = requestData('iceweb.io')

console.log(result) //undefined
ログイン後にコピー

    まず理解する必要があります
  • jsコードの実行順序は当たり前のことではなく、実際にはコードは作成した順序で実行されるわけではありません。
  • では、なぜ
  • 未定義なのでしょうか?
      まず、
    • requestData 関数を実行すると、関数の実行が開始されます。 js はシングルスレッドであるため、非同期操作が発生した場合でも、次のコードの実行はブロックされません。そのため、作成した return は成功しても失敗しても返されません。この関数では、非同期操作ではありません。 戻り値がないため、値は 未定義です。

2.2 初期の正しい実践
function requestData(url, successCB, failureCB) {
  setTimeout(() => {
    if (url === 'iceweb.io') {
      successCB('我成功了,把获取到的数据传出去', [{name:'ice', age:22}])
    } else {
      failureCB('url错误,请求失败')
    }
  }, 3000)
}

//3s后 回调successCB 
//我成功了,把获取到的数据传出去 [ { name: 'ice', age: 22 } ]
requestData('iceweb.io', (res, data) => console.log(res, data), rej => console.log(rej))

//3s后回调failureCB
//url错误,请求失败
requestData('icexxx.io', res => console.log(res) ,rej => console.log(rej))
ログイン後にコピー

    初期の解決策では、2 つのコールバックを渡し、そのうちの 1 つが失敗しました。成功したもの。そうなると、多くの開発者は「これは良いことではないですか?」と尋ねるでしょう。これは非常に単純で、js の関数は第一級市民であり、受け渡すことができますが、これは柔軟性が高すぎて仕様がありません。
  • フレームワークを使用している場合は、フレームワークのソースコードも読んで、失敗した順番に実パラメータを正しく渡す必要がありますが、パラメータを渡す順番を間違えると非常に危険です。

2. Promise

  • Promise (約束)、呼び出し元に約束を与え、データを返すことができます。 Promise オブジェクトを作成します
  • new promise を作成するとき、この時点でコールバック関数を渡す必要があります。この関数は For (executor) と呼ばれてすぐに実行されます。
  • このコールバック関数では、コールバック関数に 2 つのパラメータ
  • reslovereject (関数はパラメータを渡すことができます)
      を渡す必要があります。
    • reslove 関数が実行されると、promise オブジェクトの .then 関数がコールバックされます。
    • reject 関数が実行されると、promise オブジェクトの .cache 関数がコールバックされます。 Promise オブジェクトがコールバックされます

2.1 Executor がすぐに実行されます
new Promise((resolve, reject) => {
  console.log(`executor 立即执行`)
})
ログイン後にコピー

    受信した
  • executor すぐに実行されます

2.2 requestData再構築
function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url === 'iceweb.io') {
        //只能传递一个参数
        resolve('我成功了,把获取到的数据传出去')
      } else {
        reject('url错误,请求失败')
      }
    }, 3000)    
  })
}

//1. 请求成功
requestData('iceweb.io').then(res => {
  //我成功了,把获取到的数据传出去
  console.log(res)
})

//2. 请求失败

//2.2 第一种写法
//url错误,请求失败
requestData('iceweb.org').then(res => {},rej => console.log(rej))

//2.2 第二种写法
//url错误,请求失败
requestData('iceweb.org').catch(e => console.log(e))
ログイン後にコピー

    関数内では、このクラスが新規作成されると、渡されるコールバック関数が呼び出されます
  • executor(Promise クラスで自動的に実行されます)
  • 正しい場合は
  • resolve 関数を呼び出し、失敗した場合は reject 関数を呼び出します必要なパラメータを渡します。
  • 例外処理
    • 2 つのコールバックを
    • .then メソッドに渡すことができます。また、Promise/A 仕様 # を表示することもできます。 ##最初のコールバックは
        fulfilled
      • のコールバックです。 2 つ目は
      • rejected
      のコールバックです
    • ##これにはどのような利点がありますか?初期のソリューションよりも面倒に思えますか?
  • 統一された仕様により可読性と拡張性が向上します
    • コールバック地獄をわずかに軽減します
  • 2.3 Promise ステータス

まず、栗を与え、コードを現実的な栗に抽象化します

    あなたはガールフレンドに、来週末においしいものを食べに連れて行くと約束しました (まだ来週末ではないので、ステータスは
  • 保留中です
      )
    • 時間の経つのは早いですね、今日。今日は週末です。あなたとガールフレンドは一緒にバーベキュー、デザート、ミルクティーを食べました... ( 入金ステータス
    • )
    • 時間が経つのは早いですね、今日は週末で、あなたは計画を立てています出かけること。残念ながら、プロダクト マネージャー、オンラインで緊急の問題が発生したため、問題を解決するために会社に戻る必要があります。(生活のため)、ガールフレンドを丁重に拒否し、その理由を説明することしかできません (拒否されました) Status
    • )
    promise
  • を使用する場合、約束を与えると、3 つの段階に分けることができます
  • pending (未定) )、実行された実行者、ステータス まだ待機中、キャッシュされておらず、拒否されていません
      fulfilled(すでにキャッシュされています)、
    • resolve
    • 関数の実行はキャッシュされたステータスを表します
    • 拒否されました(すでに拒否されています)、reject
    • 関数を実行すると、拒否されたステータスが表示されます
    • まず、ステータスが保留ステータスから他のステータスに変化する限り、ステータスは変更できません
  • 次のコードについて考えてください:
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('失败')
    resolve('成功')
  }, 3000);
})

promise.then(res => console.log(res)).catch(err => console.log(err))

//失败
ログイン後にコピー
  • 当我调用reject之后,在调用resolve是无效的,因为状态已经发生改变,并且是不可逆的。

2.4 resolve不同值的区别

  • 如果resolve传入一个普通的值或者对象,只能传递接受一个参数,那么这个值会作为then回调的参数
const promise = new Promise((resolve, reject) => {
  resolve({name: 'ice', age: 22})
})

promise.then(res => console.log(res))

// {name: 'ice', age: 22}
ログイン後にコピー
  • 如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态
const promise = new Promise((resolve, reject) => {
  resolve(new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('ice')
    }, 3000);
  }))
})

promise.then(res => console.log(res))

//3s后 ice
ログイン後にコピー
  • 如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,then方法会传入resolvereject函数。此时的promise状态取决于你调用了resolve,还是reject函数。这种模式也称之为: thenable
const promise = new Promise((resolve, reject) => {
  resolve({
    then(res, rej) {
      res('hi ice')
    }
  })
})

promise.then(res => console.log(res))

// hi ice
ログイン後にコピー

2.5 Promise的实例方法

  • 实例方法,存放在Promise.prototype上的方法,也就是Promise的显示原型上,当我new Promise的时候,会把返回的改对象的 promise[[prototype]](隐式原型) === Promise.prototype (显示原型)
  • 即new返回的对象的隐式原型指向了Promise的显示原型

2.5.1 then方法

2.5.1.1 then的参数
  • then方法可以接受参数,一个参数为成功的回调,另一个参数为失败的回调,前面重构requestData中有演练过。
const promise = new Promise((resolve, reject) => {
  resolve('request success')
  // reject('request error')
})

promise.then(res => console.log(res), rej => console.log(rej))

//request success
ログイン後にコピー
  • 如果只捕获错误,还可以这样写
    • 因为第二个参数是捕获异常的,第一个可以写个null""占位
const promise = new Promise((resolve, reject) => {
  // resolve('request success')
  reject('request error')
})

promise.then(null, rej => console.log(rej))

//request error
ログイン後にコピー
2.5.1.2 then的多次调用
const promise = new Promise((resolve, reject) => {
  resolve('hi ice')
})

promise.then(res => ({name:'ice', age:22}))
       .then(res => console.log(res))
       
//{name:'ice', age:22}
ログイン後にコピー
ログイン後にコピー
  • 调用多次则会执行多次
2.5.1.3 then的返回值
  • then方法是有返回值的,它的返回值是promise,但是是promise那它的状态如何决定呢?接下来让我们一探究竟。
2.5.1.3.1 返回一个普通值 状态:fulfilled
const promise = new Promise((resolve, reject) => {
  resolve('hi ice')
})

promise.then(res => ({name:'ice', age:22}))
       .then(res => console.log(res))
       
//{name:'ice', age:22}
ログイン後にコピー
ログイン後にコピー
  • 返回一个普通值,则相当于主动调用Promise.resolve,并且把返回值作为实参传递到then方法中。
  • 如果没有返回值,则相当于返回undefined
2.5.1.3.2 明确返回一个promise 状态:fulfilled
const promise = new Promise((resolve, reject) => {
  resolve('hi ice')
})

promise.then(res => {
  return new Promise((resolve, reject) => {
    resolve('then 的返回值')
  })
}).then(res => console.log(res))

//then 的返回值
ログイン後にコピー
  • 主动返回一个promise对象,状态和你调用resolve,还是reject有关
2.5.1.3.3 返回一个thenable对象 状态:fulfilled
const promise = new Promise((resolve, reject) => {
  resolve('hi ice')
})

promise.then(res => {
  return {
    then(resolve, reject) {
      resolve('hi webice')
    }
  }
}).then(res => console.log(res))

//hi webice
ログイン後にコピー
  • 返回了一个thenable对象,其状态取决于你是调用了resolve,还是reject

2.5.2 catch方法

2.5.2.1 catch的多次调用
const promise = new Promise((resolve, reject) => {
  reject('ice error')
})

promise.catch(err => console.log(err))
promise.catch(err => console.log(err))
promise.catch(err => console.log(err))

//ice error
//ice error
//ice error
ログイン後にコピー
2.5.2.2 catch的返回值
  • catch方法是有返回值的,它的返回值是promise,但是是promise那它的状态如何决定呢?接下来让我们一探究竟。
  • 如果返回值明确一个promise或者thenble对象,取决于你调用了resolve还是reject
2.5.2.2.1 返回一个普通对象
const promise = new Promise((resolve, reject) => {
  reject('ice error')
})

promise.catch(err => ({name:'ice', age: 22})).then(res => console.log(res))

//{name:'ice', age: 22}
ログイン後にコピー
2.5.2.2.2 明确返回一个promise
const promise = new Promise((resolve, reject) => {
  reject('ice error')
})

promise.catch(err => {
  return new Promise((resolve, reject) => {
    reject('ice error promise')
  })
}).catch(res => console.log(res))

//ice error promise
ログイン後にコピー
  • 此时new Promise() 调用了reject函数,则会被catch捕获到
2.5.2.2.3 返回thenble对象
const promise = new Promise((resolve, reject) => {
  reject('ice error')
})

promise.catch(err => {
  return {
    then(resolve, reject) {
      reject('ice error then')
    }
  }
}).catch(res => console.log(res))

//ice error then
ログイン後にコピー

2.5.3 finally方法

  • ES9(2018)新实例方法
  • finally(最后),无论promise状态是fulfilled还是rejected都会执行一次finally方法
const promise = new Promise((resolve, reject) => {
  resolve('hi ice')
})

promise.then(res => console.log(res)).finally(() => console.log('finally execute'))

//finally execute
ログイン後にコピー

2.6 Promise中的类方法/静态方法

2.6.1 Promise.reslove

Promise.resolve('ice')
//等价于
new Promise((resolve, reject) => resolve('ice'))
ログイン後にコピー
  • 有的时候,你已经预知了状态的结果为fulfilled,则可以用这种简写方式

2.6.2 Promise.reject

Promise.reject('ice error')
//等价于
new Promise((resolve, reject) => reject('ice error'))
ログイン後にコピー
  • 有的时候,你已经预知了状态的结果为rejected,则可以用这种简写方式

2.6.3 Promise.all

fulfilled 状态

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi ice')
  }, 1000);
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi panda')
  }, 2000);
})

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi grizzly')
  }, 3000);
})


Promise.all([promise1, promise2, promise3]).then(res => console.log(res))

//[ 'hi ice', 'hi panda', 'hi grizzly' ]
ログイン後にコピー
  • all方法的参数传入为一个可迭代对象,返回一个promise,只有三个都为resolve状态的时候才会调用.then方法。
  • 只要有一个promise的状态为rejected,则会回调.catch方法

rejected状态

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi ice')
  }, 1000);
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('hi panda')
  }, 2000);
})

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi grizzly')
  }, 3000);
})

Promise.all([promise1, promise2, promise3]).then(res => console.log(res)).catch(err => console.log(err))

//hi panda
ログイン後にコピー
  • 当遇到rejectd的时候,后续的promise结果我们是获取不到,并且会把reject的实参,传递给catch的err形参中

2.6.4 Promise.allSettled

  • 上面的Promise.all有一个缺陷,就是当遇到一个rejected的状态,那么对于后面是resolve或者reject的结果我们是拿不到的
  • ES11 新增语法Promise.allSettled,无论状态是fulfilled/rejected都会把参数返回给我们

所有promise都有结果

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('hi ice')
  }, 1000);
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi panda')
  }, 2000);
})

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('hi grizzly')
  }, 3000);
})

Promise.allSettled([promise1, promise2, promise3]).then(res => console.log(res))

/* [
  { status: 'rejected', reason: 'hi ice' },
  { status: 'fulfilled', value: 'hi panda' },
  { status: 'rejected', reason: 'hi grizzly' }
] */
ログイン後にコピー
  • 该方法会在所有的Promise都有结果,无论是fulfilled,还是rejected,才会有最终的结果

其中一个promise没有结果

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('hi ice')
  }, 1000);
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi panda')
  }, 2000);
})

const promise3 = new Promise((resolve, reject) => {})


Promise.allSettled([promise1, promise2, promise3]).then(res => console.log(res))
// 什么都不打印
ログイン後にコピー
  • 其中一个promise没有结果,则什么都结果都拿不到

2.6.5 Promise.race

  • race(竞争竞赛)
  • 优先获取第一个返回的结果,无论结果是fulfilled还是rejectd
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('hi error')
  }, 1000);
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi panda')
  }, 2000);
})


Promise.race([promise1, promise2])
       .then(res => console.log(res))
       .catch(e => console.log(e))
       
//hi error
ログイン後にコピー

2.6.6 Promise.any

  • 与race类似,只获取第一个状态为fulfilled,如果全部为rejected则报错AggregateError
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('hi error')
  }, 1000);
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('hi panda')
  }, 2000);
})


Promise.any([promise1, promise2])
       .then(res => console.log(res))
       .catch(e => console.log(e))
       
//hi panda
ログイン後にコピー

3. Promise的回调地狱 (进阶)

  • 我还是以一个需求作为切入点,把知识点嚼碎了,一点一点喂进你们嘴里。
    • 当我发送网络请求的时候,需要拿到这次网络请求的数据,再发送网络请求,就这样重复三次,才能拿到我最终的结果。

3.1 卧龙解法

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}


requestData('iceweb.io').then(res => {
  requestData(`iceweb.org ${res}`).then(res => {
    requestData(`iceweb.com ${res}`).then(res => {
      console.log(res)
    })
  })
})

//iceweb.com iceweb.org iceweb.io
ログイン後にコピー
  • 虽然能够实现,但是多层代码的嵌套,可读性非常差,我们把这种多层次代码嵌套称之为回调地狱

3.2 凤雏解法

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

requestData('iceweb.io').then(res => {
  return requestData(`iceweb.org ${res}`)
}).then(res => {
  return requestData(`iceweb.com ${res}`)
}).then(res => {
  console.log(res)
})

//iceweb.com iceweb.org iceweb.io
ログイン後にコピー
  • 利用了then链式调用这一特性,返回了一个新的promise,但是不够优雅,思考一下能不能写成同步的方式呢?

3.3 生成器+Promise解法

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

function* getData(url) {
  const res1 = yield requestData(url)
  const res2 = yield requestData(res1)
  const res3 = yield requestData(res2)

  console.log(res3)
}

const generator = getData('iceweb.io')

generator.next().value.then(res1 => {
  generator.next(`iceweb.org ${res1}`).value.then(res2 => {
    generator.next(`iceweb.com ${res2}`).value.then(res3 => {
      generator.next(res3)
    })
  })
})

//iceweb.com iceweb.org iceweb.io
ログイン後にコピー
  • 大家可以发现我们的getData已经变为同步的形式,可以拿到我最终的结果了。那么很多同学会问,generator一直调用.next不是也产生了回调地狱吗?
  • 其实不用关心这个,我们可以发现它这个是有规律的,我们可以封装成一个自动化执行的函数,我们就不用关心内部是如何调用的了。

3.4 自动化执行函数封装

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

function* getData() {
  const res1 = yield requestData('iceweb.io')
  const res2 = yield requestData(`iceweb.org ${res1}`)
  const res3 = yield requestData(`iceweb.com ${res2}`)

  console.log(res3)
}

//自动化执行 async await相当于自动帮我们执行.next
function asyncAutomation(genFn) {
  const generator = genFn()

  const _automation = (result) => {
    let nextData = generator.next(result)
    if(nextData.done) return

    nextData.value.then(res => {
      _automation(res)
    })
  }

  _automation()
}

asyncAutomation(getData)

//iceweb.com iceweb.org iceweb.io
ログイン後にコピー
  • 利用promise+生成器的方式变相实现解决回调地狱问题,其实就是async await的一个变种而已
  • 最早为TJ实现,前端大神人物
  • async await核心代码就类似这些,内部主动帮我们调用.next方法

3.5 最终解决回调地狱的办法

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

async function getData() {
  const res1 = await requestData('iceweb.io')
  const res2 = await requestData(`iceweb.org ${res1}`)
  const res3 = await requestData(`iceweb.com ${res2}`)

  console.log(res3)
}

getData()

//iceweb.com iceweb.org iceweb.io
ログイン後にコピー
  • 你会惊奇的发现,只要把getData生成器函数函数,改为async函数,yeild的关键字替换为await就可以实现异步代码同步写法了。

4. async/await 剖析

  • async(异步的)
  • async 用于申明一个异步函数

4.1 async内部代码同步执行

  • 异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行
async function sayHi() {
  console.log('hi ice')
}

sayHi()

//hi ice
ログイン後にコピー

4.2 异步函数的返回值

  • 异步函数的返回值和普通返回值有所区别

    • 普通函数主动返回什么就返回什么,不返回为undefined
    • 异步函数的返回值特点
      • 明确有返回一个普通值,相当于Promise.resolve(返回值)
      • 返回一个thenble对象则由,then方法中的resolve,或者reject有关
      • 明确返回一个promise,则由这个promise决定
  • 异步函数中可以使用await关键字,现在在全局也可以进行await,但是不推荐。会阻塞主进程的代码执行

4.3 异步函数的异常处理

  • 如果函数内部中途发生错误,可以通过try catch的方式捕获异常
  • 如果函数内部中途发生错误,也可以通过函数的返回值.catch进行捕获
async function sayHi() {
  console.log(res)
}
sayHi().catch(e => console.log(e))

//或者

async function sayHi() {
  try {
    console.log(res)
  }catch(e) {
    console.log(e)
  }
}

sayHi()

//ReferenceError: res is not defined
ログイン後にコピー

4.4 await 关键字

  • 异步函数中可以使用await关键字,普通函数不行
  • await特点
    • 通常await关键字后面都是跟一个Promise
      • 可以是普通值
      • 可以是thenble
      • 可以是Promise主动调用resolve或者reject
    • 这个promise状态变为fulfilled才会执行await后续的代码,所以await后面的代码,相当于包括在.then方法的回调中,如果状态变为rejected,你则需要在函数内部try catch,或者进行链式调用进行.catch操作
function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

async function getData() {
  const res = await requestData('iceweb.io')
  console.log(res)
}

getData()

// iceweb.io
ログイン後にコピー

5. 结语

  • 如果现在真的看不到未来是怎样,你就不如一直往前走,不知道什么时候天亮,去奔跑就好,跑着跑着天就亮了。

【相关推荐:javascript视频教程编程基础视频

関連ラベル:
ソース:juejin.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!