JS 是单线程的,一次只能执行一个任务,如果中间遇到耗时比较长的任务,剩余代码必须等待它执行完毕才能执行,这样会出现页面假死的情况。为了解决这个问题JS将任务分为同步任务和异步任务。

同步任务:就是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务

异步任务:是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。

这里说下 JS 为什么单线程的?JS 是浏览器脚本语言,主要用来实现和用户的交互,通过 JS 我们对 DOM 进行操作,如果 JS 是多线程的,一个线程对 DOM 节点内容进行添加,一个线程对同一 DOM 节点进行删除,这样就不好区分是如何操作这个 DOM 节点了,为了简化复杂性,所以 JS 是单线程的。

单线程的优点:实现起来相对简单;单线程的缺点:只要有一个任务耗时比较长,其余任务都需要等待。

# 回调函数

回调函数简单理解就是把函数当成一个参数传给另一个函数,这个当成参数的函数就是回调函数。

function fn1() {}
function fn2(f) {
  f();
}
fn1(fn2);

回调函数最大的优点就是兼容性好,缺点是多层嵌套会产生 ”回调地狱“,不利于代码的维护和扩展。在实际项目开发中,下一个请求可能依赖上一次的请求结果,代码可能如下:

// 为了简化代码,ajax 方法省略了 url,error 参数的的处理
function ajax(data, success) {
  $.ajax({
    url: "/api/getWeather",
    data: {
      zipcode: data
    },
    success
  });
}

ajax('xx', function(a) {
  ajax(a, function(b) {
    ajax(b, function(c) {
      ajax(c, function(d) {
        // ...
      })
    })
  })
})

如果再加上异步操作的失败处理,代码阅读起来将是恐怖的。

ajax('xx', function(a) {
  try {
    ajax(a, function(b) {
      try {
        ajax(b, function(c) {
          try {
            ajax(b, function(c) {
              try {
                ajax(c, function(d) {
                  // ...
                })
              }catch(err) {

              }
            })
          }catch(err) {

          }
        })
      }catch(err) {

      }
      
    })
  }catch(err) {

  }
})

# Promise

ES6 新增的 Promise 很好的解决了 “回调地狱” 的问题,使得我们可以以链式调用的方式来处理代码,避免了大量嵌套。Promise 一共有三种状态,分别是:pending(等待中)、resolve(完成)和reject(拒绝),且状态一旦发生改变就不会再变。Promise 的基本使用可以阅读阮一峰老师的文章 Promise (opens new window),这里不再无脑列举。

// 为了简化代码,ajax 方法省略了 url,error 参数的的处理
function ajax(data, success) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: "/api/getWeather",
      data: {
        zipcode: data
      },
      success: resolve
    });
  })
}

ajax('xx').then(a => {
  return ajax(a)
}).then(b => {
  return ajax(b)
}).then(c => {
  return ajax(c)
}).then(d => {
  return ajax(d)
})

Promise 中处理报错

ajax('xx').then(a => {
  return ajax(a)
}).then(b => {
  return ajax(b)
}).then(c => {
  return ajax(c)
}).then(d => {
  return ajax(d)
}).catch(err => {
  console.log(err);
})

可以看到,代码从阅读和维护上好了很多。这里我们问一个问题,Promise 是怎么解决 “回调地狱” 问题的呢?

Promise 通过:

  • 回调函数延迟绑定
  • 返回值穿透
  • 错误冒泡

来解决 "回调地狱" 问题。

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 2000, 'ok')
  })
}

ajax().then(res => {
  console.log(res)
})

从上面的例子可以看到 Promise 不是没有回调函数,它的回调函数是通过 then 传入的,这里注意下 then ,这方法添加的任务会放到微任务中,等当前的宏任务执行完毕后才会执行这个回调,这也就是 “回调函数延迟绑定”。

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 2000, 'ok')
  })
}

const p = ajax().then(res => {
  return ajax()
})
p.then(res => {
  console.log(res)
})

从上面的例子可以看出调用 then 方法后,又返回了一个 Promise 我们可以使用这个返回的 Promise 再次进行调用,这便是 “返回值穿透”。

function ajax() {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, 2000, 'ok')
    })
}

ajax().then(res => {
    return ajax()
}).then(res => {
    throw new Error('出错了1')
}).catch(err => {
    console.log(err)
})

从上面的例子可以出,前面的错误被后面的 catch 方法捕获了,这也就是 “错误冒泡”,catch 只能捕获它前面的报错,不能捕获后面的报错。

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 2000, 'ok')
  })
}

ajax().then(res => {
  return ajax()
}).then(res => {
  throw new Error('出错了1')
}).catch(err => {
  console.log(err) // Error: 出错了1
}).then(res => {
  throw new Error('出错了2') // Uncaught (in promise) Error: 出错了2
})

简单总结下:Promise 使用链式调用解决嵌套问题,使用错误冒泡解决错误处理问题。

在上面说道 “回调函数延迟绑定” 的时候,提到了把 then 方法中的回调函数放到了微任务中,这里需要考虑下这里为什么会用到微任务呢?

首先我们来思考下怎么处理回调问题呢?简单总结无非以下三种方法:

  • 使用同步回调,等异步任务执行完再执行后面的任务
  • 使用异步回调,将回调函数放到宏任务队列的队尾
  • 使用异步回调,将回调放到当前宏任务的后面

第一种方式明显不合适,会导致阻塞,也无法实现延迟绑定。第二种,如果宏任务队列很长,回调迟迟不能执行会造成页面卡顿。所以 Promise 使用第三种方式,将回调放到当前宏任务的后面执行,即使用微任务。

使用微任务解决了:

  • 使用异步回调代替同步解决了浪费 CPU 的问题
  • 放到当前宏任务后执行,解决了回调执行的实时性问题

# 简易版Promise

写之前我们通过一个简单的例子分析下 Promise 的功能:

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('ok');
  })
}).then(value => {
  console.log(value);
}, error => {
  console.log(error);
})

/* 
'ok'
*/
  • Promise 可以被 new ,所以是一个构造函数
  • 这个构造函数的参数是一个函数,这个函数接收两个参数,这两个参数也是函数,一个处理成功,一个处理失败
  • 可以调用 then 方法,then 方法中可以传递两个函数,一个处理成功,一个处理失败
  • 有三个状态:pending、resolved、rejected ,且状态一经改变不能再次被更改
const PENDING = 'pending',
  REJECTED = 'rejected',
  FULFILLED = 'fulfilled';

function MPromise(executor) {
  const self = this;
  self.value = null;
  self.error = null;
  self.status = PENDING; // 默认状态为 PENDING
  self.onFulfilled = null; // 成功的回调
  self.onRejected = null; // 失败的回调

  const resolve = function(value) {
    if (self.status !== PENDING) return;
    setTimeout(() => {
      self.status = FULFILLED;
      self.value = value;
      self.onFulfilled(value);
    })
  }

  const reject = function(error) {
    if (self.status !== PENDING) return;
    setTimeout(() => {
      self.status = REJECTED;
      self.error = error;
      self.onRejected(error)
    })
  }
  executor(resolve, reject);
}

MPromise.prototype.then = function(onFulfilled, onRejected) {
  if (this.status === PENDING) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }else if(this.status === FULFILLED) {
    onFulfilled(this.value);
  }else {
    onRejected(this.error);
  }
  return this;
}

测试:

new MPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('ok');
  })
}).then(value => {
  console.log(value);
}, error => {
  console.log(error);
})

/* 
'ok'
*/

可以看到用我们自己写的 Promise 也正确输出了理想的结果。

# 处理 then 方法不传参的情况

从 MDN 关于 Promise.prototype.then() (opens new window) 方法的介绍中可以看到,then方法接收的两个参数都是可选的。测试下我们自己的代码:

new MPromise((resolve, reject) => {
  resolve('ok');
}).then()

发现如果 then 方法中什么都不传会报错,下面我们来修复下这个问题:


 
 











MPromise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : v => v;
  if (this.status === PENDING) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }else if(this.status === FULFILLED) {
    onFulfilled(this.value);
  }else {
    onRejected(this.error);
  }
  return this;
}

# 数组回调

再来看一种 Promise 的用法:

const p1 = new Promise((resolve, rejected) => {
  resolve('ok')
})
const p2 = p1.then(res => {
    console.log(res)
})
const p3 = p2.then(res => {
    console.log(res)
})

/* 
'ok'
'ok'
*/

换成我们自己的 Promise

const p1 = new MPromise((resolve, rejected) => {
  resolve('ok')
})
const p2 = p1.then(res => {
    console.log(res)
})
const p3 = p2.then(res => {
    console.log(res)
})

/* 
'ok'
*/

可以看到使用我们自己写的 Promise 只输出了一个 'ok',下面我们来解决这个问题。我们对代码做如下修改:


















 
 
 








 
 
 









 
 








const PENDING = 'pending',
  REJECTED = 'rejected',
  FULFILLED = 'fulfilled';

function Promise(executor) {
  const self = this;
  self.value = null;
  self.error = null;
  self.status = PENDING; // 默认状态为 PENDING
  self.onFulfilled = []; // 成功的回调
  self.onRejected = []; // 失败的回调

  const resolve = function(value) {
    if (self.status !== PENDING) return;
    setTimeout(() => {
      self.status = FULFILLED;
      self.value = value;
      self.onFulfilled.forEach(onFulfilled => {
        onFulfilled(value)
      })
    })
  }

  const reject = function(error) {
    if (self.status !== PENDING) return;
    setTimeout(() => {
      self.status = REJECTED;
      self.error = error;
      self.onRejected.forEach(onRejected => {
        onRjected(error)
      })
    })
  }
  executor(resolve, reject);
}

Promise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : v => v;
  if (this.status === PENDING) {
    this.onFulfilled.push(onFulfilled);
    this.onRejected.push(onRejected);
  }else if(this.status === FULFILLED) {
    onFulfilled(this.value);
  }else {
    onRejected(this.error);
  }
  return this;
}

再来测试下我们刚才的代码,发现输出了两次 'ok' ,和我们预期的结果一样。

# 优化链式调用

我们再来看 Promise 的另一种使用情况:

const p = new Promise((resolve, reject) => {
  resolve('hello')
})

p.then(res => {
  return res + ' world'
}).then(res => {
  console.log(res)
})

/* 
'hello wrold'
*/

上面这种用法就是 Promise 的链式调用。

使用我们自己写的 Promise 测试上面代码,输出的结果却是 'hello'。这是为什么呢?我们把代码做下简单的改造:

const p = new Promise((resolve, reject) => {
  resolve('hello')
})

const p1 = p.then(res => {
  return res + ' world'
})
const p2 = p1.then(res => {
  console.log(res)
})

console.log(p1 === p2) // false

我们自己写的:

const p = new MPromise((resolve, reject) => {
  resolve('hello')
})

const p1 = p.then(res => {
  return res + ' world'
})
const p2 = p1.then(res => {
  console.log(res)
})

console.log(p1 === p2) // true

可以看到使用系统的 Promise, p1 和 p2是不相等的,而使用我们自己的 MPromise, p1 和 p2 却是相等的。回顾下我们自己写的 MPromise, 在then 方法中,我们返回的 this 指代的是第一次生成的实例。

下面我们来完善我们的代码:





 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 









MPromise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : v => v;
    if (this.status === PENDING) {
        return new MPromise((resolve, reject) => {
            this.onFulfilled.push(value => {
                try {
                    const x = onFulfilled(value)
                    resolve(x)
                }catch(err) {
                    reject(err)
                }
            });
            this.onRejected.push(error => {
                try {
                    const x = onRejected(value)
                    resolve(x)
                }catch(err) {
                    reject(err)
                }
            });
        })

    } else if (this.status === FULFILLED) {
        onFulfilled(this.value);
    } else {
        onRejected(this.error);
    }
    return this;
}

测试之前的代码,可以看到拿到了正确结果:

const p = new MPromise((resolve, reject) => {
  resolve('hello')
})

p.then(res => {
  return res + ' world'
}).then(res => {
  console.log(res)
})
/* 
'hello world'
*/

但是还有个问题,看下面这个代码:

new Promise((resolve, reject) => {
    resolve(Promise.resolve('ok'))
}).then(res => {
    console.log(`res->${res}`)
})

/* 
'ok'
*/

输出结果是 'ok',换成我们自己写的 Promise 在看下

new MPromise((resolve, reject) => {
    resolve(Promise.resolve('ok'))
}).then(res => {
    console.log(`res->${res}`)
})
/* 
'[object Promise]'
*/

这里需要注意下,在 then 方法的 resolve(x) 地方,这个 x 的值可能也是一个 promise ,直接 resolve(x) 明显不合适,我们需要处理下这种情况:

function resolvePromise(p, x, resolve, reject) {
  if (x instanceof MPromise) {
    if (x.status === PENDING) {
      x.then(res => {
        resolvePromise(p, res, resolve, reject)
      }, error => {
        reject(error);
      })
    }else {
      x.then(resolve, reject)
    }
  }else {
    // x 不是 Promise 的话直接 resolve 即可
    resolve(x)
  }
}
MPromise.prototype.then = function (onFulfilled, onRejected) {
    let p;
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : v => v;
    if (this.status === PENDING) {
        return p = new MPromise((resolve, reject) => {
            this.onFulfilled.push(value => {
                try {
                    const x = onFulfilled(value)
                    resolvePromise(p, x, resolve, reject)
                }catch(err) {
                    reject(err)
                }
            });
            this.onRejected.push(error => {
                try {
                    const x = onRejected(value)
                    resolve(x)
                }catch(err) {
                    reject(err)
                }
            });
        })

    } else if (this.status === FULFILLED) {
        return p = new MPromise((resolve, reject) => {
          try {
            const x = onFulfilled(this.value)
            resolvePromise(p, x, resolve, reject)
          }catch(err) {
            reject(err)
          }
        })
    } else {
        return p = new Promise((resolve, reject) => {
          try {
            const x = onRejected(this.error)
            resolvePromise(p, x, resolve, reject)
          }catch(err) {
            reject(err)
          }
        })
    }
    return this;
}

# 错误捕获

MPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

# Promise.resovle

MPromise.resolve = (param) => {
  if(param instanceof MPromise) return param;
  return new MPromise((resolve, reject) => {
    if(param && param.then && typeof param.then === 'function') {
      // param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
      param.then(resolve, reject);
    }else {
      resolve(param);
    }
  })
}

# Promise.reject

先分析下 Promise.reject:

  • 返回一个 Promise
  • 传入的参数也就是失败原因会往下传
MPromise.reject = function(reason) {
  return new MPromise((resolve, reject) => {
    reject(reason)
  })
}

# Promise.finally

MPromise.prototype.finally = function(callback) {
  this.then(value => {
    return MPromise.resolve(callback()).then(() => {
      return value;
    })
  }, error => {
    return MPromise.resolve(callback()).then(() => {
      throw error;
    })
  })
}

# Promise.all

首先通过 Promise.all 的基本使用来分析下 Promise.all:

  • 它的参数是一个数组,数组中的值不一定是 Promise 对象
  • 如果参数的数组是一个空数组,则直接resolve这个空数组
  • 有一个失败,就失败,但是其他的也会执行
  • 成功的结果是一个数组
  • 返回的是一个 Promise 对象
Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    const len = promises.length, resArr = [];
    if (len === 0) {
      resolve(promises)
      return;
    }
    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i]).then(res => {
        resArr[i] = res;
        if (i === len - 1) resolve(resArr)
      }).catch(err => {
        reject(err);
      })
    }
  })
}

# Promise.race

const p = Promise.race([1, 2, 3]).then(res => {
    console.log(res) // 1
})
console.log(p) // Promise {<pending>}

/* 
Promise {<pending>}
1
*/

从 Promise.race 的基本使用分析可以看出:

  • 参数是一个数组,数组的值不一定是 promise 对象
  • 有一个执行成功就结束,结果是这个成功的 promise 值
  • 有一个失败就结束,失败原因就是这个失败的 promise 的失败的原因
  • 返回值是一个 promise 对象
Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    const len = promises.length;
    if (len === 0) return

    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i]).then(res => {
        resolve(res)
        return;
      }).catch(err => {
        reject(err);
        return;
      })
    }
  })
}

# Generator

Generator 在我们实际开发中使用的非常少,详细使用请阅读阮一峰老师的文章:

这里只做简单介绍,先来看一个例子:

function * fn() {
  yield 1
  yield (function() {return 2})()
  yield 3
}

const f = fn()

console.log(Object.prototype.toString.call(f))

console.log(f.next())
console.log(f.next())
console.log(f.next())
console.log(f.next())

/* 
'[object Generator]'
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
{value: undefined, done: true}
*/

从上面的代码可以看出,使用 function* 来声明一个函数,调用这个函数返回的是一个 Generator 对象,碰到 yield 语句代码暂停执行,使用返回的这个对象调用 next() 方法会执行函数中的代码。

上面介绍了 Generator 最简单的用法。

Generator 返回的是一个 Iterator 对象,具有 [Symbol.iterator] 属性的对象可以生成一个 Iterator 对象。

const arr = [1, 2]
const iterator = arr[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

/* 
{value: 1, done: false}
{value: 2, done: false}
{value: 1, done: false}
{value: undefined, done: true}
*/

可以看到 Iterator 对象可以通过 next 访问访问每个元素的值。

我们还可以通过 for...of 访问

const arr = [1, 2]
const iterator = arr[Symbol.iterator]()

for (let item of iterator) {
    console.log(item)
}

/* 
1
2
*/

既然我们前面说了 Generator 返回的也是一个 Iterator 对象,我们来看下之前的 Generator 代码:

function * fn() {
  yield 1
  yield (function() {return 2})()
  yield 3
}

const f = fn()

// 可以看到返回的这个 f 对象具有 Symbol.iterator 属性
console.log(f[Symbol.iterator]) // [Symbol.iterator]() { [native code] }

// 可以使用 for...of 访问
for (let item of f) {
    console.log(item)
}

// 可以通过 next 访问
console.log(f.next())
console.log(f.next())
console.log(f.next())
console.log(f.next())

/* 
[Symbol.iterator]() { [native code] }

1
2
3

{value: undefined, done: true}
{value: undefined, done: true}
{value: undefined, done: true}
{value: undefined, done: true}
*/

# thunk 函数

上面我们花费了大量篇幅介绍 Generator ,但似乎都没到重点,这篇文章整体介绍的是 JS 的异步解决方案,怎么用 Generator 来处理异步呢?

在开始步入正题前,我们需要先说下 thunk 函数,这对我们实现异步很重要。

// 模拟 ajax 请求
function ajax(param, cb) {
  setTimeout(() => {
      cb('ajax请求结果:巴拉巴拉')
  }, 1000)
}

ajax(1, (res) => {
  console.log(res)
})

上面的 ajax 函数是一个异步函数,我们对它进行一些改造:

const thunk = function(param) {
  return function (cb) {
    ajax(param, cb)
  }
}

const ajaxThunk = thunk('1')
ajaxThunk((data) => {
  console.log(data)
})

通过上面的例子我们看到 thunk 函数返回的是一个函数,ajaxThunk 这个函数只接受一个参数且这个参数是一个函数。

总结下: 我们通过对传统的异步函数进行封装,得到的一个只有一个参数且这个参数是一个callback函数的函数就是 thunk 函数。

let res = 0
function ajax(param, cb) {
    res++
    setTimeout(() => {
        cb(`ajax请求结果${res}`)
    }, 1000)
}

const thunk = function (param) {
    return function (cb) {
        ajax(param, cb)
    }
}

function* gen() {
    const r1 = yield thunk(1)
    console.log(`r1 -> ${r1}`)
    const r2 = yield thunk(2)
    console.log(`r2 -> ${r2}`)
}

const g = gen()

// g.next().value 是一个 thunk 函数,res1 是第一次调用请求方法返回的结果
g.next().value((res1) => {
    // g.next(res1) 将第一次请求的结果传给 r1
    // res2 是第二次请求返回的结果
    g.next(res1).value((res2) => {
        // 将第二次请求结果传给 r2
        g.next(res2)
    })
})

/* 
r1 -> ajax请求结果1
r2 -> ajax请求结果2
*/

上面我们实现了一个模拟 ajax 请求的例子,但向上面那样调用太麻烦了,如果每次都向上面那样调用我宁愿使用回调函数。。。

下面我们来封装一个 run 方法来简化我们的代码:
























 
 
 
 
 
 
 
 
 
 








// 模拟返回结果
let res = 0
// 模拟 ajax 请求
function ajax(param, cb) {
    res++
    setTimeout(() => {
        cb(`ajax请求结果${res}`)
    }, 1000)
}

const thunk = function (param) {
    return function (cb) {
        ajax(param, cb)
    }
}

function* gen() {
    const r1 = yield thunk(1)
    console.log(`r1 -> ${r1}`)
    const r2 = yield thunk(2)
    console.log(`r2 -> ${r2}`)
}

function run(generator) {
    const g = generator()
    function next(data) {
        const res = g.next(data)
        if (res.done) return

        res.value(next)
    }
    next()
}

run(gen)

/* 
r1 -> ajax请求结果1
r2 -> ajax请求结果2
*/

# co 库

虽然通过上面的封装代码简单了很多,每次都要自己写个 run 方法也比较麻烦,我们可以使用 co 库。上面的例子中 yield 后面跟的是一个 thunk 函数,这里我们换成一个 promise 试试。

// 引入 co 库
// <script src="https://cdn.bootcdn.net/ajax/libs/co/4.1.0/index.js"></script>

// 模拟 ajax 请求
function ajax(param) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(param)
    }, 1000)
  })
}

co(function* fn() {
  const param = 1
  const r1 = yield ajax(param)
  const r2 = yield ajax(r1 + 2)
  const r3 = yield ajax(r2 + 3)
  console.log(r3)
})

/* 
3
*/

# async和await

使用 Generator 来处理异步还是比较麻烦,ES8规范新增了 async/await ,可以让我们以同步的方式写异步代码。

我们先使用 async 和 await 改写下上面的代码,后续再介绍 async 和 await。

function ajax(data, success) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: "/api/getWeather",
      data: {
        zipcode: data
      },
      success: resolve
    });
  })
}

async function fecth() {
  const a = await ajax('xx')
  const b = await ajax(a)
  const c = await ajax(b)
  const d = await ajax(c)
}

fecth().catch(err => {
  console.log(err)
})

可以看到,改写后的代码无论从阅读性还是从维护性都很好。

async、await可以看做是 Generator 的语法糖。

# async

async 关键字用于声明异步函数,使用 async 声明的函数具有异步特征,但代码总体是同步求值的。

console.log(1)
async function fn() {
  console.log(2)
}
fn()
console.log(3)
/* 
1
2
3
*/

异步函数的返回值会被 Promise.resolve() 包装成一个期约对象。

async function fn() {
  return 1
}
fn().then(res=>console.log(res)) // 1

异步函数抛出错误会返回拒绝的期约。

async function fn() {
  throw new Error('error')
}
fn().catch(err => console.log(err)) // Error: error

拒绝期约的错误不会被异步函数捕获。

async function fn() {
  Promise.reject('err')
}
fn().catch(err => console.log(err)) // Uncaught (in promise) err

async 是 Generator 函数的语法糖,相比 Generator 来说,具有以下特点:

  • 内置执行器。async 函数自带执行器,调用方式和普通函数一样。
  • 更好的语义。相比 * 和 yield 更语义化。
  • 更广的适用性。yield 函数后面只能是 Thunk 函数或者 Promise 对象,而 async 函数中的 await 后面可以是任何值。
  • 返回值是 Promise,可以直接调用 then 方法,比 Generator 返回的 Iterator 对象方便。
console.log(1)
async function fn() {
  return 2
}
console.log(3)

/* 

*/

# await

await 关键字必须在 async 函数中直接使用,await 关键字可用于暂停异步函数代码的执行。

async function fn() {
  let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
  console.log(await p);
}

fn();

// 3

await 关键字会暂停执行异步函数后面的代码,让出 JS 执行线程。JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。等到await右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。即使await后面跟着一个立即可用的值,函数的其余部分也会被异步求值

console.log(1)
async function fn() {
    const a = await 2
    console.log(a)
}
fn();
console.log(3)

/* 
1
3
2
*/

await 关键会尝试解包对象的值,将这个值赋值给表达式。

async function fn() {
  console.log(await Promise.resolve('hello'));
}

fn(); // 'hello'

单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误,对拒绝的期约使用await则会释放(unwrap)错误值(将拒绝期约返回)。

async function fn() {
  console.log(1);
  await Promise.reject(3);
  console.log(4); // 这行代码不会执行
}

// 给返回的期约添加一个拒绝处理程序
fn().catch(console.log);
console.log(2);

// 1
// 2
// 3

异步函数中如果不包含await,其执行和普通函数一样。

console.log(1)
async function fn() {
  console.log(2)
}
fn();
console.log(3)
/* 
1
2
3
*/

如果await后面是一个期约,老版浏览器会有两个任务被添加到消息队列并被异步求值。新版浏览器只生成一个异步任务。

async function foo() {
  console.log(await Promise.resolve('foo'));
}
async function bar() {
  console.log(await 'bar');
}
async function baz() {
  console.log('baz');
}

foo();
bar();
baz();
/* 
新版浏览器:
baz
foo
bar

旧版浏览器:
baz
bar
foo
*/

参考:

《JS 高级程序设计(第四版)》

深入理解 JavaScript 异步 (opens new window)

js中的同步和异步 (opens new window)

「硬核JS」深入了解异步解决方案 (opens new window)

一次性让你懂async/await,解决回调地狱 (opens new window)