回调

在某些不确定时长的工作完成之后才执行对应的逻辑(函数),这些函数就是回调。

比如onload

  1. function loadScript(src, callback) {
  2. let s = document.create('script')
  3. s.src = src
  4. // 不是s.onload = callback
  5. s.onload = () => {
  6. callback(s)
  7. }
  8. document.head.append(s)
  9. }
  10. loadScript('xxx.js', () => {
  11. // do some
  12. })

回调中回调

如果需要加载2个脚本,有序,第一个,第二个。需要把加载第二个的逻辑卸载第一个的callback里

  1. loadScript('1.js', () => {
  2. alert('1.js is ok')
  3. loadScript('2.js', () => {
  4. // 这里安全访问1和2的脚本内容
  5. alert('2.js is ok')
  6. })
  7. })

处理Error

上例没有处理失败情况

  1. // 经典的Error First
  2. function loadScript(src, callback) {
  3. let script = document.createElement('script')
  4. script.src = scr
  5. script.onload = () => callback(null, script)
  6. script.onerror = (e) => callback(e)
  7. document.head.append(script)
  8. }
  9. loadScript('xxx', (error, sciprt) => {
  10. if(error) {
  11. // 处理错误
  12. } else {
  13. // 脚本逻辑
  14. }
  15. })

嵌套地狱

回调逻辑如果变多,则是一种不可维护的代码。

  1. loadScript('1', (err, script) => {
  2. if(err){
  3. // do
  4. } else {
  5. loadScript('2', (err, script) => {
  6. if(err){
  7. // do
  8. } else {
  9. loadScript('3', (err, script) => {
  10. if(err) {
  11. // do
  12. } else {
  13. //
  14. }
  15. })
  16. }
  17. })
  18. }
  19. })

image.png

优化,独立函数,缓解

将异步任务拆成一个个函数,每个函数中调用另一个异步任务函数,达到拆分代码的效果。

  1. loadScript('1', step1)
  2. function step1(err, script) {
  3. if(err) {
  4. } else {
  5. loadScript('2', step2)
  6. }
  7. }
  8. function step2(err, script) {...}

Promise

语法

  1. let promise = new Promise(function(resolve, reject) {
  2. // executor 生产者代码
  3. })
  1. 创建Promise的同时,executor会自动执行。
  2. 任务成功,resolve(value)
  3. 任务失败,reject(error)

    resolve和reject,表明该promise被settled了

内部属性

一个Promise具有以下内部属性,不可访问
state

  • 初始pending
  • resolve调用,则变为 fulfilled
  • reject调用,则变为 rejected

result

  • 初始为undefined
  • resolve(value),变为value
  • reject(error)调用,变为error

    只能有一个终态,确定后不可变

    ```javascript let p = new Promise((resolve, reject) => { resolve(1) resolve(2) // 忽略 reject(new Error(‘h’)) // 忽略 })

// 只能通过then或catch访问到结果 p.then(r => console.log(r)) // 1

// 之后的任何时候访问,都是1

  1. <a name="uwNSw"></a>
  2. ### 消费者
  3. 通过 `then` `catch` `finally` 来访问一个Promise的结果。
  4. ```javascript
  5. // then的完整语法
  6. p.then(function(r) {
  7. // 处理resolve的结果
  8. }, function(err) {
  9. // 处理reject的结果
  10. })
  11. // then函数的参数不是必须的,且错误处理通常用catch。上面then完整写法中处理错误了,但不等于.catch
  12. p.then(r => {
  13. // 处理resolve
  14. }).catch(e => {
  15. // 处理reject
  16. })
  17. p.finally() // 同try finally, 最开始由于兼容性用的比较少。 node 10+支持,ie完全不支持。

Promise链

异步任务一个接一个的执行,我们可以利用链的特性。

  1. new Promise((resolve, reject) => {
  2. resolve(1)
  3. }).then(r => {
  4. return r * 2 // return 2
  5. }).then(r => {
  6. return r * 2 // return 4
  7. }).then(r => {
  8. console.log(r) // 4
  9. })

能这么写的原因?

  1. then本身返回的也是一个promise,所以我们可以继续.then
  2. return的值将作为当前promise的result(当前promise状态已经resolved了),所以可以对当前Promise使用then获取其result。达到传递效果。

    return一个新Promise

    .then(handler) 中handler可以继续创建并返回一个promise,这种情况下,后续处理程序将等这个新的promise被settled后再获取其结果。
    1. new Promise((resolve, reject) => {
    2. resolve(1)
    3. }).then(r => {
    4. return new Promise((resolve, reject) => {
    5. setTimeout(() => {
    6. resolve(r + 2)
    7. }, 1000)
    8. })
    9. }).then(r => {
    10. console.log(3)
    11. })

    避免代码向右增长

    假设有一个用promise实现的loadScript函数
    1. loadScript('a.js').then(a => {
    2. loadScript('b.js').then(b => {
    3. loadScript('c.js').then(c => {
    4. a();
    5. b();
    6. c()
    7. })
    8. })
    9. })
    要使用链式
    1. loadScript('a.js')
    2. .then(a => loadScript('b.js'))
    3. .then(b => loadScript('c.js'))
    4. .then(c => {console.log(c)})
    5. // 注意,这里只能拿到c了

    thenable

    鸭子类型的独享,需要实现 .then 方法,第三方可借助此实现自己的Promise兼容对象。 ```javascript class Thenable { constructor(n) { this.n = n }
    then(resolve, reject) { setTimeout(() => {
    1. resolve(this.n * 2)
    }, 1000)
    } }

new Promise(resolve => resolve(1)).then(r => { return new Thenable(r) }).then(r => { console.log(r)
})

  1. <a name="sMbW6"></a>
  2. ## promise状态图
  3. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/241313/1627442987078-30a3a8d6-f2c0-4735-bf84-e416511c88fb.png#height=375&id=CPfv8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=750&originWidth=1360&originalType=binary&ratio=1&size=84605&status=done&style=none&width=680)
  4. <a name="W8oHU"></a>
  5. ## 注意:then(f1, f2)不等于catch
  6. 如果f1出现错误,f2是捕获不到的,只能靠catch,因此通常我们用catch来兜底。
  7. <a name="ZqKq7"></a>
  8. # Promise的错误处理
  9. <a name="3j6iW"></a>
  10. ## Promise链+错误处理
  11. Promise一旦被rejected,会将控制权交给最近的处理错误函数(通常是.catch)。<br />多个异步任务构成的异步链,我们不用每个异步任务单独处理报错,可以在最后处理。
  12. ```javascript
  13. asyncByPromise(...)
  14. .then(r => {...})
  15. .then(r => {...})
  16. .then(r => {...})
  17. .catch(e => {...}) // 可以接收到上面3个then中任意一个错误,进行处理

隐式try…catch

如果Promise发生异常,或主动throw error,周围会有一个隐式的try…catch,捕获错误,并将Promise视为 rejection 进行处理。

  1. new Promise(resolve => {
  2. throw new Error('抓我')
  3. }).catch(e => {
  4. console.log(e) // 抓我
  5. })
  6. // 等同
  7. new Promise((resolve, reject) => {
  8. reject('抓我')
  9. }).catch(e => {
  10. console.log(e) // 抓我
  11. })

上面代码异常是发生在 executor 里,也就是Promise接收的函数中。对 handler 函数中异常,同样

  1. new Promise((resolve, reject) => {
  2. resolve("ok");
  3. }).then((result) => {
  4. throw new Error("Whoops!"); // reject 这个 promise
  5. }).catch(alert); // Error: Whoops!

再次抛出

上面在Promise链式最后的catch,类似于在最外层包裹了 try catch ,同 try catch 中讲过的,我们可以在 catch 中分析错误,如果处理不了,就继续抛出。

错误的正常处理和再次抛出

如果错误被正常处理,则会将控制权移交到最近的 .then 处理程序,继续走下去。
如果错误被再次抛出,则会将错误移交到下一个最近的error处理程序。

  1. // 错误正常处理了,交给最近的then
  2. // 执行流:catch -> then
  3. new Promise(resolve => {
  4. throw new Error('whoops')
  5. }).catch(e => {
  6. // 这里正常处理掉
  7. console.log('错误被处理了,继续吧')
  8. }).then(r => {
  9. console.log('第1个then')
  10. console.log(r)
  11. }).then(r => {
  12. console.log('第2个then')
  13. })
  14. // 错误没有被处理,抛出,继续给下面的catch来
  15. // 执行流:catch -> catch
  16. new Promise(resolve => {
  17. throw new Error('错了')
  18. }).catch(e => {
  19. throw e // 处理不动了
  20. }).then(r => {
  21. // then这里拿不到(如果你提供给then第二个函数呢?也可以处理这个错误,就用不到后面的catch了)
  22. }, e => {
  23. console.log(`then第二个处理函数有的话,错误就接住了`, e)
  24. }).catch(error => {
  25. console.log('兜底了 ' + error)
  26. })

未处理的rejection

如果有未处理的Promise rejection,JS引擎将会跟踪此类 rejection ,生成一个全局的 error ,类似 window.onerror ,我们也有 unhandledrejection 全局事件来捕获这类错误。

  1. window.addEventListener('unhandledrejection', function(event) {
  2. // 这个事件对象有两个特殊的属性:
  3. alert(event.promise); // [object Promise] - 生成该全局 error 的 promise
  4. alert(event.reason); // Error: Whoops! - 未处理的 error 对象
  5. });

注意:隐式try…catch

try catch只能捕获同步错误,异步错误无法捕获。同理在Promise中,抛出一个错误,交给隐式try…catch来处理的话,只能捕获同步错误

  1. new Promise(resolve => {
  2. setTimeout(() => {
  3. // 异步抛出
  4. throw new Error('catch捕获不到')
  5. }, 1000)
  6. }).catch(e => {
  7. console.log('这里抓不到异步的错误,除非你用reject啊')
  8. })
  9. // 错误会溢出到全局,未捕获。

Promise其他API

Promise类还有5个静态方法

all

Promise.all([...promiseArray])

  • 当给定的所有Promise都被 resolved ,新的promise才会被resolved,其结果将成为新的 promise 的结果(一个数组)。
  • 但如果有一个Promise被rejected,则新的promise会rejected,且error就是这个被rejected的promise,其他的promise也不再理会。

    allSettled

    Promise.all不同,会等待所有promise都被settled的,而不是一个rejected,整体都rejected

    最近的提案,有些浏览器还不支持,我们可以模拟下

  1. // 统一转换为一个对象值,这里是 => ({}),直接返回了这个对象值,这步很重要!
  2. const rejectHandler = reason => ({status: 'rejected', reason})
  3. const resolveHandler = value => ({status: 'fulfilled', value})
  4. Promise.allSettled = function(promises) {
  5. // 记得我们前面说过的,then 返回的是一个新的Promise,前一个Promise return的值会传递到下一个Promise里
  6. // 所以这里拿到的新的promises数组,rejected的都被转换为了一个正常的对象值,都是fulfilled的状态。
  7. const convertedPromises = Promise.resolve(item).then(resolveHandler,rejectHandler)
  8. // 这里利用all等到所有promise都resolved特性。
  9. return Promise.all(convertedPromises)
  10. }

race

只等待promise数组中第一个settled的promise,不管是结果还是错误。

resolve/reject

resolve(value)用value创建一个resolved的promise
reject(error)用error创建一个rejected的promise

Promisfication

很多三方库和函数都是基于回调的,为了方便使用Promise,我们通常会将基于回调的函数和库转换为promise形式的。
假设有一个读取脚本的函数

  1. // Error First形式的callback很常见
  2. function loadScript(src, callback) {
  3. // 创建script标签,插入doc中
  4. let script = ...
  5. script.onload = () => callback(null, script)
  6. script.onload = () => callback(new Error(`${src}加载失败`)
  7. }
  8. loadScript('a.js', (err, s) => {...})
  9. // 转换后
  10. function loadScriptPromise = function(src) {
  11. return new Promise((resolve, reject) => {
  12. loadScript(src, (err, script) => {
  13. if(err) reject(err)
  14. else resolve(script)
  15. })
  16. })
  17. }
  18. loadScriptPromise('a.js').then(...)

通用的promisfy(f)

思路:

  1. 因为是包装callback(error, value)形式的函数,所以我们接受一个函数f,运行后生成一个新的函数newF
  2. newF就是我们真正业务使用的,接受我们业务的参数,比如loadScript接收的业务参数就是src,但要注意参数可能并不唯一,所以我们需要收集到所有参数,使用ES6的rest参数来拿到所有参数。至于callback,我们需要统一处理为上面loadScript中那样,封装resolve和reject,所以内部实现,无需关注
  3. 当我们调用newFn时,应该是这样的newFn(arg1, arg2).then(res => {}).catch(err => {})。所以我们需要newFn返回一个Promise
  4. 在Promise中,executor函数(也就是包装的fn)需要执行,

    1. 注意this指向问题,我们可能是对象调用如obj.newFn(arg1, arg2, callback),也可能是newFn(arg1, arg2, callback),也可能是newFn.bind(obj)生成又一个新函数调用,或者newFn.call(obj, arg1, arg2, callback)
    2. 注意callback永远都是在最后面的,所以我们可以统一封装一个callback来以前代理用户自己传递 ```javascript // 第一步,包装callback(err,value)形式的函数f function Promisfy(f) { // 吐出一个新的函数newFn // 第二步,接收所有的参数 return function(…args) { // 第三步,返回一个promise return new Promise((resolve, reject) => { function callback(err, value) {

      1. if(err) {
      2. reject(err)

      } else {

      1. resolve(value)

      } }

      // 第四步,执行f,或者f.apply(this, args) f.call(this, …args, callback) }) } }

// 验证OK function callbackAdd(a, b, callback) { setTimeout(() => { const sum = a + b; if (sum > 10) { callback(new Error(‘大于10’)); } else { callback(sum); } }, 1000); }

const newCallbackAdd = Promisfy(callbackAdd); newCallbackAdd(1, 3) .then((r) => { console.log(r); // 4 }) .catch((err) => { console.log(err); });

  1. 有没有问题?<br />有,入参我们已经支持多个了,出参我们目前不支持多个,我们无法支持一个多个出参的callback,如`callback(err, res1, res2, ...)`。我们的内置callback只能支持1个出参。
  2. ```javascript
  3. // 改进,将callback的出参支持多个
  4. function Promisfy(f) {
  5. // 吐出一个新的函数newFn
  6. // 第二步,接收所有的参数
  7. return function(...args) {
  8. // 第三步,返回一个promise
  9. return new Promise((resolve, reject) => {
  10. function callback(err, ...value) {
  11. if(err) {
  12. reject(err)
  13. } else {
  14. // 这里根据callback的接收到的除err之外的参数来决定返回数组还是单值
  15. if(value.length === 0) {
  16. resolve(value[0])
  17. } else {
  18. // resolve 只能接收单值
  19. resolve(value)
  20. }
  21. }
  22. }
  23. // 第四步,执行f,或者f.apply(this, args)
  24. f.call(this, ...args, callback)
  25. })
  26. }
  27. }
  28. // 验证OK
  29. function compute(a, b, callback) {
  30. setTimeout(() => {
  31. const sum = a + b;
  32. const product = a * b;
  33. if (sum > 10 || product > 100) {
  34. callback(new Error('错误'));
  35. } else {
  36. callback(null, sum, product);
  37. }
  38. }, 1000);
  39. }
  40. const newComp = Promisfy(compute);
  41. newComp(1, 4)
  42. .then((r) => {
  43. console.log(r); // [5, 4]
  44. })
  45. .catch((err) => {
  46. console.log(err);
  47. });

微任务

Promise.then/catch/finaly 都是异步的。JS引擎中有微任务队列负责控制,当微任务队列清空后,继续执行其他任务。

  1. let p = Promise.resolve(1)
  2. p.then(r => {console.log(r)})
  3. console.log('code done')
  4. // code done
  5. // 1

async/await

使用关键字async标识函数,函数总返回一个promise
使用关键字await获取promise的值,并且会等待promise状态变为settled

await 也接受thenable,就像promise支持thenable一样

error处理

使用try catch包裹住async内部的代码。