前端面试-JS-Promise原理

149 阅读43分钟

1、异步编程主要考点

并发(concurrency)和并行(parallelism)区别

涉及面试题:并发与并行的区别?

并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。

并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

类似于上电梯

回调函数(Callback)

涉及面试题:什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

回调函数就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。这个过程就叫做回调。

回调地狱的根本问题就是:


1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身


2. 嵌套函数一多,就很难处理错误

Generator

涉及面试题:你理解的 Generator 是什么?

Generator 算是 ES6 中难理解的概念之一了,Generator 最大的特点就是可以控制函数的执行。在这一小节中我们不会去讲什么是 Generator,而是把重点放在 Generator 的一些容易困惑的地方。

function *foo(x) {
    let y = 2 * (yield (x + 1))
    let z = yield (y / 3)
    return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8,
done: false}
console.log(it.next(13)) // => {value:
42, done: true}复制代码

你也许会疑惑为什么会产生与你预想不同的值,接下来就让我为你逐行代码分析原因

  • 首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42

Generator 函数一般见到的不多,其实也于他有点绕有关系,并且一般会配合 co 库去使用。当然,我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:

function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()复制代码

常用定时器函数

涉及面试题:setTimeout、setInterval、requestAnimationFrame 各有什么特点?

异步编程当然少不了定时器了,常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame。我们先来讲讲最常用的setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。

其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,我们可以通过代码去修正 setTimeout,从而使定时器相对准确

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
    count++
    // 代码执行所消耗的时间
    let offset = new Date().getTime() - (startTime + count *interval);
    let diff = end - new Date().getTime()
    let h = Math.floor(diff / (60 * 1000 * 60))
    let hdiff = diff % (60 * 1000 * 60)
    let m = Math.floor(hdiff / (60 * 1000))
    let mdiff = hdiff % (60 * 1000)
    let s = mdiff / (1000)
    let sCeil = Math.ceil(s)
    let sFloor = Math.floor(s)
    // 得到下一次循环所消耗的时间
    currentInterval = interval - offset 
    console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) 
    setTimeout(loop, currentInterval)
}
setTimeout(loop,currentInterval)复制代码

接下来我们来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码

function demo() {
    setInterval(function(){
        console.log(2)
    },1000)
    sleep(2000)
}
demo()复制代码

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

如果你有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现

function setInterval(callback, interval) {
    let timer
    const now = Date.now
    let startTime = now()
    let endTime = startTime
    const loop = () => {
        timer = window.requestAnimationFrame(loop)
        endTime = now()
        if (endTime - startTime >=interval) {
            startTime = endTime = now()
            callback(timer)
        }
    }
timer = window.requestAnimationFrame(loop)
    return timer
}
let a = 0
setInterval(timer=> {
    console.log(1)
    a++
    if (a === 3) cancelAnimationFrame(timer)
}, 1000)复制代码

首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。

2、详解Promise

  • 什么是promise
    • Js中进行异步编程的新的解决方案,用于表示一个异步操作的最终完成 (或失败), 及其结果值.。
      语法上:promise是一个构造函数
      简单来说,promise对象用来封装一个异步操作并可以获取其结果
  • 有几种状态,分别代表什么
  • 什么是链式调用

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。

一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

基本过程:

  1. 初始化 Promise 状态(pending)等待
  2. 立即执行 Promise 中传入的 fn 函数,将Promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理
  3. 执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  4. Promise里的关键是要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行。

真正的链式Promise是指在当前promise达到执行态fulfilled状态后,即开始进行下一个promise.

参考资料

完整 Promise 模型

function Promise(fn) {
  let state = 'pending'
  let value = null
  const callbacks = []
  this.then = function (onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      handle({
        onFulfilled,
        onRejected,
        resolve,
        reject,
      })
    })
  }
  this.catch = function (onError) {
    return this.then(null, onError)
  }
  this.finally = function (onDone) {
    this.then(onDone, onError)
  }
  this.resolve = function (value) {
    if (value && value instanceof Promise) {
      return value
    } if (value && typeof value === 'object' && typeof value.then === 'function') {
      const { then } = value
      return new Promise((resolve) => {
        then(resolve)
      })
    } if (value) {
      return new Promise(resolve => resolve(value))
    }
    return new Promise(resolve => resolve())
  }
  this.reject = function (value) {
    return new Promise(((resolve, reject) => {
      reject(value)
    }))
  }
  this.all = function (arr) {
    const args = Array.prototype.slice.call(arr)
    return new Promise(((resolve, reject) => {
      if (args.length === 0) return resolve([])
      let remaining = args.length
      function res(i, val) {
        try {
          if (val && (typeof val === 'object' || typeof val === 'function')) {
            const { then } = val
            if (typeof then === 'function') {
              then.call(val, (val) => {
                res(i, val)
              }, reject)
              return
            }
          }
          args[i] = val
          if (--remaining === 0) {
            resolve(args)
          }
        } catch (ex) {
          reject(ex)
        }
      }
      for (let i = 0; i < args.length; i++) {
        res(i, args[i])
      }
    }))
  }
  this.race = function (values) {
    return new Promise(((resolve, reject) => {
      for (let i = 0, len = values.length; i < len; i++) {
        values[i].then(resolve, reject)
      }
    }))
  }
  function handle(callback) {
    if (state === 'pending') {
      callbacks.push(callback)
      return
    }
    const cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected
    const next = state === 'fulfilled' ? callback.resolve : callback.reject
    if (!cb) {
      next(value)
      return
    }	
    let ret;
    try {
     ret = cb(value)
    } catch (e) {
      callback.reject(e)
    }
	callback.resolve(ret);
  }
  function resolve(newValue) {
    const fn = () => {
      if (state !== 'pending') return
      if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
        const { then } = newValue
        if (typeof then === 'function') {
          // newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
          // 相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
          then.call(newValue, resolve, reject)
          return
        }
      }
      state = 'fulfilled'
      value = newValue
      handelCb()
    }
    setTimeout(fn, 0)
  }
  function reject(error) {
    const fn = () => {
      if (state !== 'pending') return
      if (error && (typeof error === 'object' || typeof error === 'function')) {
        const { then } = error
        if (typeof then === 'function') {
          then.call(error, resolve, reject)
          return
        }
      }
      state = 'rejected'
      value = error
      handelCb()
    }
    setTimeout(fn, 0)
  }
  function handelCb() {
    while (callbacks.length) {
      const fn = callbacks.shift()
      handle(fn)
    }
  }
  try {
  fn(resolve, reject)
  } catch(ex) {
	reject(ex);
  }
}

对比 Callback、Promise、Generator、Async 几个异步 API 的优劣?

在 JavaScript 中利用事件循环机制 [12] (Event Loop)可以在单线程中实现非阻塞式、异步的操作。例如

  • Node.js 中的 Callback、EventEmitter [13]Stream [14]
  • ES6 中的 Promise [15]Generator [16]
  • ES2017 中的 Async [17]
  • 三方库 RxJS、Q [18]Co、 [19] Bluebird [20]

我们重点来看一下常用的几种编程方式(Callback、Promise、Generator、Async)在语法糖上带来的优劣对比。

Callback

Callback(回调函数)是在 Web 前端开发中经常会使用的编程方式。这里举一个常用的定时器示例:

export interface IObj {
  value: string;
  deferExec(): void;
  deferExecAnonymous(): void;
  console(): void;
}
export const obj: IObj = {
  value: 'hello',
  deferExecBind() {
    // 使用箭头函数可达到一样的效果
    setTimeout(this.console.bind(this), 1000);
  },
  deferExec() {
    setTimeout(this.console, 1000);
  },
  console() {
    console.log(this.value);
  },
};
obj.deferExecBind(); // hello
obj.deferExec(); // undefined
复制代码

回调函数经常会因为调用环境的变化而导致 this 的指向性变化。除此之外,使用回调函数来处理多个继发的异步任务时容易导致回调地狱(Callback Hell):

fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    fs.readFile(fileC, 'utf-8', function (err, data) {
      fs.readFile(fileD, 'utf-8', function (err, data) {
        // 假设在业务中 fileD 的读写依次依赖 fileA、fileB 和 fileC
        // 或者经常也可以在业务中看到多个 HTTP 请求的操作有前后依赖(继发 HTTP 请求)
        // 这些异步任务之间纵向嵌套强耦合,无法进行横向复用
        // 如果某个异步发生变化,那它的所有上层或下层回调可能都需要跟着变化(比如 fileA 和 fileB 的依赖关系倒置)
        // 因此称这种现象为 回调地狱
        // ....
      });
    });
  });
});
复制代码

回调函数不能通过 return 返回数据,比如我们希望调用带有回调参数的函数并返回异步执行的结果时,只能通过再次回调的方式进行参数传递:

// 希望延迟 3s 后执行并拿到结果
function getAsyncResult(result: number) {
  setTimeout(() => {
    return result * 3;
  }, 1000);
}
// 尽管这是常规的编程思维方式
const result = getAsyncResult(3000);
// 但是打印 undefined
console.log('result: ', result);
function getAsyncResultWithCb(result: number, cb: (result: number) => void) {
  setTimeout(() => {
    cb(result * 3);
  }, 1000);
}
// 通过回调的形式获取结果
getAsyncResultWithCb(3000, (result) => {
  console.log('result: ', result); // 9000
});
复制代码

对于 JavaScript 中标准的异步 API 可能无法通过在外部进行 try...catch... 的方式进行错误捕获:

try {
  setTimeout(() => {
    // 下述是异常代码
    // 你可以在回调函数的内部进行 try...catch...
    console.log(a.b.c)
  }, 1000)
} catch(err) {
  // 这里不会执行
  // 进程会被终止
  console.error(err)
}
复制代码

上述示例讲述的都是 JavaScript 中标准的异步 API ,如果使用一些三方的异步 API 并且提供了回调能力时,这些 API 可能是非受信的,在真正使用的时候会因为执行反转(回调函数的执行权在三方库中)导致以下一些问题:

  • 使用者的回调函数设计没有进行错误捕获,而恰恰三方库进行了错误捕获却没有抛出错误处理信息,此时使用者很难感知到自己设计的回调函数是否有错误
  • 使用者难以感知到三方库的回调时机和回调次数,这个回调函数执行的权利控制在三方库手中
  • 使用者无法更改三方库提供的回调参数,回调参数可能无法满足使用者的诉求
  • ...

举个简单的例子:

interface ILib<T> {
  params: T;
  emit(params: T): void;
  on(callback: (params: T) => void): void;
}
// 假设以下是一个三方库,并发布成了npm 包
export const lib: ILib<string> = {
  params: '',
  emit(params) {
    this.params = params;
  },
  on(callback) {
    try {
      // callback 回调执行权在 lib 上
      // lib 库可以决定回调执行多次
      callback(this.params);
      callback(this.params);
      callback(this.params);
      // lib 库甚至可以决定回调延迟执行
      // 异步执行回调函数
      setTimeout(() => {
        callback(this.params);
      }, 3000);
    } catch (err) {
      // 假设 lib 库的捕获没有抛出任何异常信息
    }
  },
};
// 开发者引入 lib 库开始使用
lib.emit('hello');
lib.on((value) => {
  // 使用者希望 on 里的回调只执行一次
 // 这里的回调函数的执行时机是由三方库 lib 决定
  // 实际上打印四次,并且其中一次是异步执行
  console.log(value);
});
lib.on((value) => {
  // 下述是异常代码
  // 但是执行下述代码不会抛出任何异常信息
  // 开发者无法感知自己的代码设计错误
  console.log(value.a.b.c)
});
复制代码

Promise

Callback 的异步操作形式除了会造成回调地狱,还会造成难以测试的问题。ES6 中的 Promise (基于 Promise A + [21] 规范的异步编程解决方案)利用有限状态机 [22] 的原理来解决异步的处理问题,Promise 对象提供了统一的异步编程 API,它的特点如下:

  • Promise 对象的执行状态不受外界影响。Promise 对象的异步操作有三种状态: pending(进行中)、 fulfilled(已成功)和 rejected(已失败) ,只有 Promise 对象本身的异步操作结果可以决定当前的执行状态,任何其他的操作无法改变状态的结果
  • Promise 对象的执行状态不可变。Promise 的状态只有两种变化可能:从 pending(进行中)变为 fulfilled(已成功)或从 pending(进行中)变为 rejected(已失败)

温馨提示:有限状态机提供了一种优雅的解决方式,异步的处理本身可以通过异步状态的变化来触发相应的操作,这会比回调函数在逻辑上的处理更加合理,也可以降低代码的复杂度。

Promise 对象的执行状态不可变示例如下:

const promise = new Promise<number>((resolve, reject) => {
  // 状态变更为 fulfilled 并返回结果 1 后不会再变更状态
  resolve(1);
  // 不会变更状态
  reject(4);
});
promise
  .then((result) => {
    // 在 ES 6 中 Promise 的 then 回调执行是异步执行(微任务)
    // 在当前 then 被调用的那轮事件循环(Event Loop)的末尾执行
    console.log('result: ', result);
  })
  .catch((error) => {
    // 不执行
    console.error('error: ', error);
  });
复制代码

假设要实现两个继发的 HTTP 请求,第一个请求接口返回的数据是第二个请求接口的参数,使用回调函数的实现方式如下所示(这里使用 setTimeout 来指代异步请求):

// 回调地狱
const doubble = (result: number, callback: (finallResult: number) => void) => {
  // Mock 第一个异步请求
  setTimeout(() => {
    // Mock 第二个异步请求(假设第二个请求的参数依赖第一个请求的返回结果)
    setTimeout(() => {
      callback(result * 2);
    }, 2000);
  }, 1000);
};
doubble(1000, (result) => {
  console.log('result: ', result);
});
复制代码

温馨提示:继发请求的依赖关系非常常见,例如人员基本信息管理系统的开发中,经常需要先展示组织树结构,并默认加载第一个组织下的人员列表信息。

如果采用 Promise 的处理方式则可以规避上述常见的回调地狱问题:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Mock 异步请求
    // 将 resolve 改成 reject 会被 catch 捕获
    setTimeout(() => resolve(result), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Mock 异步请求
    // 将 resolve 改成 reject 会被 catch 捕获
    setTimeout(() => resolve(result * 2), 1000);
  });
};
firstPromise(1000)
  .then((result) => {
    return nextPromise(result);
  })
  .then((result) => {
    // 2s 后打印 2000
    console.log('result: ', result);
  })
  // 任何一个 Promise 到达 rejected 状态都能被 catch 捕获
  .catch((err) => {
    console.error('err: ', err);
  });
复制代码

Promise 的错误回调可以同时捕获 firstPromisenextPromise 两个函数的 rejected 状态。接下来考虑以下调用场景:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Mock 异步请求
    setTimeout(() => resolve(result), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Mock 异步请求
    setTimeout(() => resolve(result * 2), 1000);
  });
};
firstPromise(1000)
  .then((result) => {
    nextPromise(result).then((result) => {
      // 后打印
      console.log('nextPromise result: ', result);
    });
  })
  .then((result) => {
    // 先打印
    // 由于上一个 then 没有返回值,这里打印 undefined
    console.log('firstPromise result: ', result);
  })
  .catch((err) => {
    console.error('err: ', err);
  });
复制代码

首先 Promise 可以注册多个 then(放在一个执行队列里),并且这些 then 会根据上一次返回值的结果依次执行。除此之外,各个 Promise 的 then 执行互不干扰。我们将示例进行简单的变换:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Mock 异步请求
    setTimeout(() => resolve(result), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Mock 异步请求
    setTimeout(() => resolve(result * 2), 1000);
  });
};
firstPromise(1000)
  .then((result) => {
    // 返回了 nextPromise 的 then 执行后的结果
    return nextPromise(result).then((result) => {
      return result;
    });
  })
  // 接着 nextPromise 的 then 执行的返回结果继续执行
  .then((result) => {
    // 2s 后打印 2000
    console.log('nextPromise result: ', result);
  })
  .catch((err) => {
    console.error('err: ', err);
  });
复制代码

上述例子中的执行结果是因为 then 的执行会返回一个新的 Promise 对象,并且如果 then 执行后返回的仍然是 Promise 对象,那么下一个 then 的链式调用会等待该 Promise 对象的状态发生变化后才会调用(能得到这个 Promise 处理的结果)。接下来重点看下 Promise 的错误处理:

const promise = new Promise<string>((resolve, reject) => {
  // 下述是异常代码
  console.log(a.b.c);
  resolve('hello');
});
promise
  .then((result) => {
    console.log('result: ', result);
  })
  // 去掉 catch 仍然会抛出错误,但不会退出进程终止脚本执行
  .catch((err) => {
    // 执行
    // ReferenceError: a is not defined
    console.error(err);
  });
setTimeout(() => {
  // 继续执行
  console.log('hello world!');
}, 2000);
复制代码

从上述示例可以看出 Promise 的错误不会影响其他代码的执行,只会影响 Promise 内部的代码本身,因为Promise 会在内部对错误进行异常捕获,从而保证整体代码执行的稳定性。Promise 还提供了其他的一些 API 方便多任务的执行,包括

  • Promise.all:适合多个异步任务并发执行但不允许其中任何一个任务失败
  • Promise.race :适合多个异步任务抢占式执行
  • Promise.allSettled :适合多个异步任务并发执行但允许某些任务失败

Promise 相对于 Callback 对于异步的处理更加优雅,并且能力也更加强大, 但是也存在一些自身的缺点:

  • 无法取消 Promise 的执行
  • 无法在 Promise 外部通过 try...catch... 的形式进行错误捕获(Promise 内部捕获了错误)
  • 状态单一,每次决断只能产生一种状态结果,需要不停的进行链式调用

温馨提示:手写 Promise 是面试官非常喜欢的一道笔试题,本质是希望面试者能够通过底层的设计正确了解 Promise 的使用方式,如果你对 Promise 的设计原理不熟悉,可以深入了解一下或者手动设计一个。

Generator

Promise 解决了 Callback 的回调地狱问题,但也造成了代码冗余,如果一些异步任务不支持 Promise 语法,就需要进行一层 Promise 封装。Generator 将 JavaScript 的异步编程带入了一个全新的阶段,它使得异步代码的设计和执行看起来和同步代码一致。Generator 使用的简单示例如下:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
// 在 Generator 函数里执行的异步代码看起来和同步代码一致
function* gen(result: number): Generator<Promise<number>, Promise<number>, number> {
  // 异步代码
  const firstResult = yield firstPromise(result)
  console.log('firstResult: ', firstResult) // 2
 // 异步代码
  const nextResult = yield nextPromise(firstResult)
  console.log('nextResult: ', nextResult) // 6
  return nextPromise(firstResult)
}
const g = gen(1)
// 手动执行 Generator 函数
g.next().value.then((res: number) => {
  // 将 firstPromise 的返回值传递给第一个 yield 表单式对应的 firstResult
  return g.next(res).value
}).then((res: number) => {
  // 将 nextPromise 的返回值传递给第二个 yield 表单式对应的 nextResult
  return g.next(res).value
})
复制代码

通过上述代码,可以看出 Generator 相对于 Promise 具有以下优势:

  • 丰富了状态类型,Generator 通过 next 可以产生不同的状态信息,也可以通过 return 结束函数的执行状态,相对于 Promise 的 resolve 不可变状态更加丰富
  • Generator 函数内部的异步代码执行看起来和同步代码执行一致,非常利于代码的维护
  • Generator 函数内部的执行逻辑和相应的状态变化逻辑解耦,降低了代码的复杂度

next 可以不停的改变状态使得 yield 得以继续执行的代码可以变得非常有规律,例如从上述的手动执行 Generator 函数可以看出,完全可以将其封装成一个自动执行的执行器,具体如下所示:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
type Gen =  Generator<Promise<number>, Promise<number>, number>
function* gen(): Gen {
  const firstResult = yield firstPromise(1)
  console.log('firstResult: ', firstResult) // 2
  const nextResult = yield nextPromise(firstResult)
  console.log('nextResult: ', nextResult) // 6
  return nextPromise(firstResult)
}
// Generator 自动执行器
function co(gen: () => Gen) {
  const g = gen()
  function next(data: number) {
    const result = g.next(data)
    if(result.done) {
      return result.value
    }
    result.value.then(data => {
      // 通过递归的方式处理相同的逻辑
      next(data)
    })
  }
  // 第一次调用 next 主要用于启动 Generator 函数
  // 内部指针会从函数头部开始执行,直到遇到第一个 yield 表达式
  // 因此第一次 next 传递的参数没有任何含义(这里传递只是为了防止 TS 报错)
  next(0)
}
co(gen)
复制代码

温馨提示:TJ Holowaychuk [23] 设计了一个 Generator 自动执行器 Co [24] ,使用 Co 的前提是 yield 命令后必须是 Promise 对象或者 Thunk 函数。Co 还可以支持并发的异步处理,具体可查看官方的 API 文档 [25]

需要注意的是 Generator 函数的返回值是一个 Iterator 遍历器对象,具体如下所示:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
type Gen = Generator<Promise<number>>;
function* gen(): Gen {
  yield firstPromise(1);
  yield nextPromise(2);
}
// 注意使用 next 是继发执行,而这里是并发执行
Promise.all([...gen()]).then((res) => {
  console.log('res: ', res);
});
for (const promise of gen()) {
  promise.then((res) => {
    console.log('res: ', res);
  });
}
复制代码

Generator 函数的错误处理相对复杂一些,极端情况下需要对执行和 Generator 函数进行双重错误捕获,具体如下所示:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // 需要注意这里的reject 没有被捕获
    setTimeout(() => reject(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
type Gen = Generator<Promise<number>>;
function* gen(): Gen {
  try {
    yield firstPromise(1);
    yield nextPromise(2);
  } catch (err) {
    console.error('Generator 函数错误捕获: ', err);
  }
}
try {
  const g = gen();
  g.next();
  // 返回 Promise 后还需要通过 Promise.prototype.catch 进行错误捕获
  g.next();
  // Generator 函数错误捕获
  g.throw('err');
  // 执行器错误捕获
  g.throw('err');
} catch (err) {
  console.error('执行错误捕获: ', err);
}
复制代码

在使用 g.throw 的时候还需要注意以下一些事项:

  • 如果 Generator 函数本身没有捕获错误,那么 Generator 函数内部抛出的错误可以在执行处进行错误捕获
  • 如果 Generator 函数内部和执行处都没有进行错误捕获,则终止进程并抛出错误信息
  • 如果没有执行过 g.next,则 g.throw 不会在 Gererator 函数中被捕获(因为执行指针没有启动 Generator 函数的执行),此时可以在执行处进行执行错误捕获

Async

Async 是 Generator 函数的语法糖,相对于 Generator 而言 Async 的特性如下:

  • 内置执行器:Generator 函数需要设计手动执行器或者通用执行器(例如 Co 执行器)进行执行,Async 语法则内置了自动执行器,设计代码时无须关心执行步骤
  • yield 命令无约束:在 Generator 中使用 Co 执行器时 yield 后必须是 Promise 对象或者 Thunk 函数,而 Async 语法中的 await 后可以是 Promise 对象或者原始数据类型对象、数字、字符串、布尔值等(此时会对其进行 Promise.resolve() 包装处理)
  • 返回 Promise: async 函数的返回值是 Promise 对象(返回原始数据类型会被 Promise 进行封装), 因此还可以作为 await 的命令参数,相对于 Generator 返回 Iterator 遍历器更加简洁实用

举个简单的示例:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
async function co() {
  const firstResult = await firstPromise(1);
  // 1s 后打印 2
  console.log('firstResult: ', firstResult); 
  // 等待 firstPromise 的状态发生变化后执行
  const nextResult = await nextPromise(firstResult);
  // 2s 后打印 6
  console.log('nextResult: ', nextResult); 
  return nextResult;
}
co();
co().then((res) => {
  console.log('res: ', res); // 6
});
复制代码

通过上述示例可以看出,async 函数的特性如下:

  • 调用 async 函数后返回的是一个 Promise 对象,通过 then 回调可以拿到 async 函数内部 return 语句的返回值
  • 调用 async 函数后返回的 Promise 对象必须等待内部所有 await 对应的 Promise 执行完(这使得 async 函数可能是阻塞式执行)后才会发生状态变化,除非中途遇到了 return 语句
  • await 命令后如果是 Promise 对象,则返回 Promise 对象处理后的结果,如果是原始数据类型,则直接返回原始数据类型

上述代码是阻塞式执行,nextPromise 需要等待 firstPromise 执行完成后才能继续执行,如果希望两者能够并发执行,则可以进行下述设计:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
async function co() {
  return await Promise.all([firstPromise(1), nextPromise(1)]);
}
co().then((res) => {
  console.log('res: ', res); // [2,3]
});
复制代码

除了使用 Promise 自带的并发执行 API,也可以通过让所有的 Promise 提前并发执行来处理:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    console.log('firstPromise');
    setTimeout(() => resolve(result * 2), 10000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    console.log('nextPromise');
    setTimeout(() => resolve(result * 3), 1000);
  });
};
async function co() {
  // 执行 firstPromise
  const first = firstPromise(1);
  // 和 firstPromise 同时执行 nextPromise
  const next = nextPromise(1);
  // 等待 firstPromise 结果回来
  const firstResult = await first;
  console.log('firstResult: ', firstResult);
  // 等待 nextPromise 结果回来
  const nextResult = await next;
  console.log('nextResult: ', nextResult);
  return nextResult;
}
co().then((res) => {
  console.log('res: ', res); // 3
});
复制代码

Async 的错误处理相对于 Generator 会更加简单,具体示例如下所示:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Promise 决断错误
    setTimeout(() => reject(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
async function co() {
  const firstResult = await firstPromise(1);
  console.log('firstResult: ', firstResult);
  const nextResult = await nextPromise(1);
  console.log('nextResult: ', nextResult);
  return nextResult;
}
co()
  .then((res) => {
    console.log('res: ', res);
  })
  .catch((err) => {
    console.error('err: ', err); // err: 2
  });
复制代码

async 函数内部抛出的错误,会导致函数返回的 Promise 对象变为 rejected 状态,从而可以通过 catch 捕获, 上述代码只是一个粗粒度的容错处理,如果希望 firstPromise 错误后可以继续执行 nextPromise,则可以通过 try...catch...async 函数里进行局部错误捕获:

const firstPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    // Promise 决断错误
    setTimeout(() => reject(result * 2), 1000);
  });
};
const nextPromise = (result: number): Promise<number> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 3), 1000);
  });
};
async function co() {
  try {
    await firstPromise(1);
  } catch (err) {
    console.error('err: ', err); // err: 2
  }
  
  // nextPromise 继续执行
  const nextResult = await nextPromise(1);
  return nextResult;
}
co()
  .then((res) => {
    console.log('res: ', res); // res: 3
  })
  .catch((err) => {
    console.error('err: ', err);
  });
复制代码

温馨提示:Callback 是 Node.js 中经常使用的编程方式,Node.js 中很多原生的 API 都是采用 Callback 的形式进行异步设计,早期的 Node.js 经常会有 Callback 和 Promise 混用的情况,并且在很长一段时间里都没有很好的支持 Async 语法。如果你对 Node.js 和它的替代品 Deno 感兴趣,可以观看 Ryan Dahl 在 TS Conf 2019 中的经典演讲 Deno is a New Way to JavaScript [26]

3、promise延伸问题

如何中断一个promise

Promise 有个缺点就是一旦创建就无法取消,所以本质上 Promise 是无法被终止的,但我们在开发过程中可能会遇到下面两个需求:

1、 中断调用链

就是在某个 then/catch 执行之后,不想让后续的链式调用继续执行了,即:

somePromise
  .then(() => {})
  .then(() => {
    // 终止 Promise 链,让下面的 then、catch 和 finally 都不执行
  })
  .then(() => console.log('then'))
  .catch(() => console.log('catch'))
  .finally(() => console.log('finally'))

答案就是在 then/catch 的最后一行返回一个永远 pending 的 promise 即可:

return new Promise((resolve, reject) => {})

这样的话后面所有的 then、catch 和 finally 都不会执行了。

2、 中断Promise

注意这里是中断而不是终止,因为 Promise 无法终止,这个中断的意思是:在合适的时候,把 pending 状态的 promise 给 reject 掉。例如一个常见的应用场景就是希望给网络请求设置超时时间,一旦超时就就中断,我们这里用定时器模拟一个网络请求,随机 3 秒之内返回:

const request = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('收到服务端数据')
  }, Math.random() * 3000)
})

如果认为超过 2 秒就是网络超时,可以对该 promise 写一个包装函数 timeoutWrapper:

function timeoutWrapper(p, timeout = 2000) {
  const wait = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求超时')
    }, timeout)
  })
  return Promise.race([p, wait])
}

于是就可以像下面这样用了:

const req = timeoutWrapper(request)
req.then(res => console.log(res)).catch(e => console.log(e))

不过这种方式并不灵活,因为终止 promise 的原因可能有很多,例如当用户点击某个按钮或者出现其他事件时手动终止。所以应该写一个包装函数,提供 abort 方法,让使用者自己决定何时终止:

function abortWrapper(p1) {
  let abort
  let p2 = new Promise((resolve, reject) => (abort = reject))
  let p = Promise.race([p1, p2])
  p.abort = abort
  return p
}
const req = abortWrapper(request)
req.then(res => console.log(res)).catch(e => console.log(e))
setTimeout(() => req.abort('用户手动终止请求'), 2000) // 这里可以是用户主动点击

最后,再次强调一下,虽然 promise 被中断了,但是 promise 并没有终止,网络请求依然可能返回,只不过那时我们已经不关心请求结果了。

4、async

Promise解决了回调地狱的问题,但是如果遇到复杂的业务,代码里面会包含大量的 then 函数,使得代码依然不是太容易阅读。

基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰,而且还支持 try-catch 来捕获异常,非常符合人的线性思维。

async 是Generator函数的语法糖,并对Generator函数进行了改进。

Generator函数简介

Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态,但是只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

有这样一段代码:

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
复制代码

调用及运行结果:

hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }
复制代码

由结果可以看出,Generator函数被调用时并不会执行,只有当调用next方法、内部指针指向该语句时才会执行,即函数可以暂停,也可以恢复执行。每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

Generator函数暂停恢复执行原理

要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。

一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

协程是一种比线程更加轻量级的存在。普通线程是抢先式的,会争夺cpu资源,而协程是合作的 ,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。它的运行流程大致如下:

  1. 协程A开始执行
  2. 协程A执行到某个阶段,进入暂停,执行权转移到协程B
  3. 协程B执行完成或暂停,将执行权交还A
  4. 协程A恢复执行

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

执行器

通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器,co 模块就是一个著名的执行器。

Generator 是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:

  1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
  2. Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

一个基于 Promise 对象的简单自动执行器:

function run(gen){
  var g = gen();
  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }
  next();
}
复制代码

我们使用时,可以这样使用即可,

function* foo() {
    let response1 = yield fetch('https://xxx') //返回promise对象
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://xxx') //返回promise对象
    console.log('response2')
    console.log(response2)
}
run(foo);
复制代码

上面代码中,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。

async/await

ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。

前文中的代码,用async实现是这样:

const foo = async () => {
    let response1 = await fetch('https://xxx') 
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://xxx') 
    console.log('response2')
    console.log(response2)
}
复制代码

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点:

  1. 内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。
  2. 更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  4. 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。

这里的重点是自带了执行器,相当于把我们要额外做的(写执行器/依赖co模块)都封装了在内部。比如:

async function fn(args) {
  // ...
}
复制代码

等同于:

function fn(args) {
  return spawn(function* () {
    // ...
  });
}
function spawn(genF) { //spawn函数就是自动执行器,跟简单版的思路是一样的,多了Promise和容错处理
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}
复制代码

async/await执行顺序

通过上面的分析,我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:

console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

分析这段代码:

  • 执行代码,输出script start
  • 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。
  • 遇到setTimeout,产生一个宏任务
  • 执行Promise,输出Promise。遇到then,产生第一个微任务
  • 继续执行代码,输出script end
  • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
  • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1
  • 执行await,实际上会产生一个promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})

执行完成,执行await后面的语句,输出async1 end

  • 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

注意

新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
复制代码

但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。 知乎上也有相关讨论,可以看看 www.zhihu.com/question/26…

我们可以分2种情况来理解:

  1. 如果await 后面直接跟的为一个变量,比如:await 1;这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)。然后跳出async1函数,执行其他代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。所以这种情况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。
  2. 如果await后面跟的是一个异步函数的调用,比如上面的代码,将代码改成这样:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
  console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
复制代码

输出为:

// script start => async2 end => Promise => script end =>async2 end1 => promise1 => promise2 => async1 end => setTimeout
复制代码

此时执行完awit并不先把await后面的代码注册到微任务队列中去,而是执行完await之后,直接跳出async1函数,执行其他代码。然后遇到promise的时候,把promise.then注册为微任务。其他代码执行完毕后,需要回到async1函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的。所以这种情况会先执行async1函数之外的微任务(promise1,promise2),然后才执行async1内注册的微任务(async1 end).

Promise API

  1. Promise.all()
  2. Promise.allSettled()
  3. Promise.any()
  4. Promise.prototype.catch()
  5. Promise.prototype.finally()
  6. Promise.race()
  7. Promise.reject()
  8. Promise.resolve()
  9. Promise.prototype.then()

Promise.all

Promise.all() 方法接收一个promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型)的输入,并且只返回一个Promise实例, 那个输入的所有promise的resolve回调的结果是一个数组。这个Promise的resolve回调执行是在所有输入的promise的resolve回调都结束,或者输入的iterable里没有promise了的时候。它的reject回调执行是,只要任何一个输入的promise的reject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息

注意点

  • 如果传入的参数是一个空的可迭代对象,则返回一个已完成(already resolved) 状态的 Promise
  • 如果传入的参数不包含任何 promise,则返回一个异步完成(asynchronously resolved) Promise。注意:Google Chrome 58 在这种情况下返回一个已完成(already resolved) 状态的 Promise
  • 其它情况下返回一个处理中(pending)Promise。这个返回的 promise 之后会在所有的 promise 都完成或有一个 promise 失败时异步地变为完成或失败。 见下方关于“Promise.all 的异步或同步”示例。返回值将会按照参数内的 promise 顺序排列,而不是由调用 promise 的完成顺序决定。

主要场景

  • 更适合彼此相互依赖或者在其中任何一个reject时立即结束

Promise.allSettled ( ES2020 新特性)

澳-赛都

返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果

可用于并行执行独立的异步操作,并收集这些操作的结果

  • 常见广告位,埋点位置

Promise. any

Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise,如果都失败,那就 返回所有失败的,

本质上,这个方法和Promise.all()是相反的

www.jianshu.com/p/5ff00fb6f…

  • 从最快的服务器检索资源
  • 显示第一张已加载的图片(来自MDN

Promise.race

Promise.race(iterable) ****方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

只要有一个出结果(不管成功还是失败)则立即返回结果

  • 封装 请求,fetch

Promise.resolve()

Promise.resolve(value) 方法返回一个以给定值解析后的Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是thenable(即带有"then"方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。

警告: 不要在解析为自身的thenable 上调用Promise.resolve。这将导致无限递归,因为它试图展平无限嵌套的promise。一个例子是将它与Angular中的异步管道一起使用。在此处了解更多信息

let thenable = {
  then: (resolve, reject) => {
    resolve(thenable)
  }
}

Promise.resolve(thenable)  //这会造成一个死循环

Promise.reject()

Promise.reject() 方法返回一个带有拒绝原因的Promise对象。

Ajax

juejin.cn/post/684490…

  • Ajax如何实现
  • Ajax解决浏览器缓存问题
  • promise和ajax有什么不同

如何实现

Ajax是一种异步请求数据的web开发技术,对于改善用户的体验和页面性能很有帮助。简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。Ajax的目的是提高用户体验,较少网络数据的传输量。同时,由于AJAX请求获取的是数据而不是HTML文档,因此它也节省了网络带宽,让互联网用户的网络冲浪体验变得更加顺畅。

如何使用

1.创建Ajax核心对象XMLHttpRequest(记得考虑兼容性)

  1. var xhr=null; 2. if (window.XMLHttpRequest) 3. {// 兼容 IE7+, Firefox, Chrome, Opera, Safari 4. xhr=new XMLHttpRequest(); 5. } else{// 兼容 IE6, IE5 6. xhr=new ActiveXObject("Microsoft.XMLHTTP"); 7. } 复制代码

2.向服务器发送请求

  1. xhr.open(method,url,async); 2. send(string);//post请求时才使用字符串参数,否则不用带参数。 复制代码
  • method:请求的类型;GET 或 POST
  • url:文件在服务器上的位置
  • async:true(异步)或 false(同步) 注意:post请求一定要设置请求头的格式内容

xhr.open("POST","test.html",true); xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); xhr.send("fname=Henry&lname=Ford"); //post请求参数放在send里面,即请求体 复制代码

3.服务器响应处理(区分同步跟异步两种情况)

responseText 获得字符串形式的响应数据。

responseXML 获得XML 形式的响应数据。

①同步处理

  1. xhr.open("GET","info.txt",false); 2. xhr.send(); 3. document.getElementById("myDiv").innerHTML=xhr.responseText; //获取数据直接显示在页面上 复制代码

②异步处理

相对来说比较复杂,要在请求状态改变事件中处理。

  1. xhr.onreadystatechange=function() { 2. if (xhr.readyState==4 &&xhr.status==200) { 3. document.getElementById("myDiv").innerHTML=xhr.responseText; 4. } 5. } 复制代码

什么是readyState?

readyState是XMLHttpRequest对象的一个属性,用来标识当前XMLHttpRequest对象处于什么状态。 readyState总共有5个状态值,分别为0~4,每个值代表了不同的含义

  • 0:未初始化 -- 尚未调用.open()方法;
  • 1:启动 -- 已经调用.open()方法,但尚未调用.send()方法;
  • 2:发送 -- 已经调用.send()方法,但尚未接收到响应;
  • 3:接收 -- 已经接收到部分响应数据;
  • 4:完成 -- 已经接收到全部响应数据,而且已经可以在客户端使用了;

什么是status?

HTTP状态码(status)由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:

常见的状态码

仅记录在 RFC2616 上的 HTTP 状态码就达 40 种,若再加上 WebDAV(RFC4918、5842)和附加 HTTP 状态码 (RFC6585)等扩展,数量就达 60 余种。接下来,我们就介绍一下这些具有代表性的一些状态码。

  • 200 表示从客户端发来的请求在服务器端被正常处理了。
  • 204 表示请求处理成功,但没有资源返回。
  • 301 表示永久性重定向。该状态码表示请求的资源已被分配了新的URI,以后应使用资源现在所指的URI。
  • 302 表示临时性重定向。
  • 304 表示客户端发送附带条件的请求时(指采用GET方法的请求报文中包含if-matched,if-modified-since,if-none-match,if-range,if-unmodified-since任一个首部)服务器端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回304Modified(服务器端资源未改变,可直接使用客户端未过期的缓存)
  • 400 表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。
  • 401 表示未授权(Unauthorized),当前请求需要用户验证
  • 403 表示对请求资源的访问被服务器拒绝了
  • 404 表示服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。
  • 500 表示服务器端在执行请求时发生了错误。也有可能是Web应用存在的bug或某些临时的故障。
  • 503 表示服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

③GET和POST请求数据区别

  • 使用Get请求时,参数在URL中显示,而使用Post方式,则放在send里面
  • 使用Get请求发送数据量小,Post请求发送数据量大
  • 使用Get请求安全性低,会被缓存,而Post请求反之 关于第一点区别,详情见下面两张图:

ajax axiox fetch区别

juejin.cn/post/693415…

juejin.cn/post/699778…

Fetch 提供了一个获取资源的接口(包括跨域请求)。

Fetch 是一个现代的概念, 等同于 XMLHttpRequest。它提供了许多与 XMLHttpRequest 相同的功能,但被设计成更具可扩展性和高效性

Fetch 的核心在于对 HTTP 接口的抽象,包括 Request、Response、Headers 和 Body,以及用于初始化异步请求的 global fetch。得益于 JavaScript 实现的这些抽象好的 HTTP 模块,其他接口能够很方便的使用这些功能。

除此之外,Fetch 还利用到了请求的异步特性——它是基于 Promise 的。

fetch() 方法必须接受一个参数——资源的路径。无论请求成功与否,它都返回一个 Promise 对象,resolve 对应请求的 Response。

  • Ajax 是一种代表异步 JavaScript + XML 的模型(技术合集) ,所以 Fetch 也是 Ajax 的一个子集
  • 在之前,我们常说的 Ajax 默认是指以 XHR 为核心的技术合集,而在有了 Fetch 之后,Ajax 不再单单指 XHR 了,我们将以 XHR 为核心的 Ajax 技术称作传统 Ajax
  • Axios 属于传统 Ajax(XHR)的子集,因为它是基于 XHR 进行的封装。
  • Fetch的优势仅仅在于浏览器原生支持,而Axios需要引入Axios库
    • 因为Node环境下默认是不支持Fetch的,所以必须要使用node-fetch这个包
  • Axios是对XMLHttpRequest的封装,而Fetch是一种新的获取资源的接口方式,并不是对XMLHttpRequest的封装。
  • 使用中最大的不同之处在于传递数据的方式不同,Axios是放到data属性里,以对象的方式进行传递,而Fetch则是需要放在body属性中,以字符串的方式进行传递
  • 响应超时
    • Axios的相应超时设置是非常简单的,直接设置timeout属性就可以了
    • Fetch提供了AbortController属性,但是使用起来不像Axios那么简单
      axios
  • Axios还有非常好的一点就是会自动对数据进行转化,而Fetch则不同,它需要使用者进行手动转化。
  • Axios的一大卖点就是它提供了拦截器,可以统一对请求或响应进行一些处理
  • fetch应该和xhr比,毕竟都是浏览器支持的。axios是xhr封装后的产物,比的话也应该和fetch封装后的产物比。

一个是浏览器 API,一个是第三方库

axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

Vue2.0之后,尤雨溪推荐大家用axios替换JQuery ajax,想必让axios进入了很多人的目光中。
axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,它本身具有以下特征
1.从浏览器中创建 XMLHttpRequest
2.支持 Promise API
3.客户端支持防止CSRF
4.提供了一些并发请求的接口(重要,方便了很多的操作)
5.从 node.js 创建 http 请求
6.拦截请求和响应
7.转换请求和响应数据
8.取消请求
9.自动转换JSON数据
PS:防止CSRF:就是让你的每个请求都带一个从cookie中拿到的key, 根据浏览器同源策略,假冒的网站是拿不到你cookie中得key的,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。



面试题

juejin.cn/post/684490…

juejin.cn/post/694531…

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/684490…