学习笔记-JavaScript异步编程以及手撕Promise

JavaScript异步编程

众所周知,目前主流的JavaScript环境都是以单线程模式去执行的JavaScript代码,JavaScript采用单线程模式工作的原因与它最早的设计初衷有关,最早JavaScript就是运行在浏览器端的脚本语言,目的是为了实现页面上的动态交互,而实现页面交互的核心就是dom操作,这也决定了它必须使用单线程,否则会出现复杂的线程同步问题。试想一下,假定我们在JavaScript中同时又多个线程一起工作,其中一个线程修改了某个dom元素,而另外一个线程同时删除这个元素,那此时浏览器就无法明确该以哪个线程工作结果为准,所以为了避免线程同步的问题,从一开始JavaScript就被设计为了单线程模式工作,这也成了这门语言最为核心的特性之一。这里的单线程指的是在js执行环境中负责执行代码的线程只有一个。一次只能执行一个任务,有多个任务就需要排队,一个一个完成。这种模式最大的优点就是更安全更简单,缺点也同样很明显,如果遇到某个特别耗时的任务,后边的任务都必须排队等待这个任务的结束,这也就会导致整个程序的执行会被拖延,出现假死的情况。为了解决耗时任务阻塞的问题,JavaScript将任务的执行模式分成了两种,同步模式和异步模式。

同步模式和异步模式

同步模式

同步模式指代码当中的任务依次执行,后一个任务必须等待前一个任务结束,按照代码书写的顺序执行。

异步模式

不会去等待这个任务的结束才开始下一个任务。对于耗时任务,开启过后就立即往后执行下一个任务,耗时任务的后续逻辑通过回调函数的方式定义。耗时任务完成会自动执行这里的回调函数。如果没有异步模式,单线程的JavaScript语言无法同时处理大量耗时任务。但是对于开发者而言,异步模式最大的问题就是代码的执行顺序混乱。

回调函数:由调用者定义,交给执行者执行的函数。

事件循环和消息队列

image.png

JavaScript线程首先执行同步任务,在遇到异步任务(eg:setTimeout)的时候,发起异步调用,然后继续执行同步任务。与此同时,异步调用线程执行异步任务后,将异步任务的回调放入消息队列。待同步任务执行完毕后,Event Loop会去消息队列中寻找任务,依次执行消息队列中的任务。

异步编程的几种方式

Promise异步方案、宏任务/微任务队列

image.png

Promise就是一个对象,用来表示一个异步任务最终结束过后,究竟是成功还是失败。就像是一个承诺,一开始是待定的状态- Pending,成功后叫Fulfilled,失败后叫Rejected。承诺明确后会有对应的任务执行,onFilfilled, onRejected.

基本用法

const promise = new Promise(function (resolve, reject) {
    // 兑现承诺

    // resolve(100)  // 承诺达成

    reject(new Error('promise rejected')) // 承诺失败
})

promise.then(function (value) {
    console.log('resolved', value)
    return 1
}, function (error) {
    console.log('rejected', error)
}).then(function(value){
    console.log(value) // 1
})
  • Promise对象的then方法会返回一个全新的Promise对象,所以可以使用链式调用
  • 后面的then方法就是在为上一个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面的then方法的回调会等待这个Promise结束

异常处理

function ajax(url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest()
        xhr.open('GET', url)
        xhr.responseType = 'json'
        xhr.onload = function () {
            if (this.status === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
        xhr.send()
    })
}

// then方法的第二个回调函数进行异常捕获
ajax('/api/users.json').then(
    function onFulfilled(value) {
        console.log('onFulfilled', value)
    },
    function onRejected(error) {
        console.log('onRejected', error)
    }
)

// 使用catch进行异常捕获
ajax('/api/users.json')
    .then(function onFulfilled(value) {
        console.log('onFulfilled', value)
    })
    .catch(function onRejected(error) {
        console.log('onRejected', error)
    })

使用then的第二个回调捕获异常,只能捕获到前一个抛出的异常,而使用catch,因为每一个then都会返回一个promise对象,所以catch首先捕获的是前一个then 的异常,然后会捕获链上往前的异常,也就是catch会捕获链上catch以前的异常。

Promise 静态方法

Promise.resolve()
Promise.reject()

Promise 并行执行

Promise.all()

// Promise.all 返回一个全新的Promise
var promise = Promise.all([
  ajax('/api/user.json'),
  ajax('api/posts.json')
])
// 所有的Promise完成,全新的promise才会完成
// 所以的异步任务都成功,promise才成功
// 只要有一个异步任务失败,promise就失败
promise.then(function (values) {
  // 接收的是数组,包含每个异步任务执行的结果
  console.log(values)
}).catch(function (error) {
  console.log(error)
})

Promise.race()

Promise.race()也会将多个promise对象组合返回一个新的promise对象,但与 all 不同的是:

all 等待所有任务结束,它才会结束

race 只会等待第一个结束的任务,也就是只要有一个任务完成了,新的promise对象也就完成了。

const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject => {
  setTimeout(() => reject(new Error('timeout')), 500)
}))

// Promise.race()将多个异步任务组合后返回一个新的promise对象
// 多个异步任务中只要有一个完成(成功或失败),新的promise对象就完成了
// 这里如果request请求在500毫秒内请求成功,就返回成功,使用.then方法
// 如果500毫秒请求没有返回结果,就会reject一个错误,走到catch
Promise.race([require, timeout])
  .then(value => {
    console.log(value)
  })
  .catch(error => {
    console.log(error)
  })
const p = Promise.all([p1,p2,p3])
p.then(() => {})
.catch(err => {})
  • Promise.all(): p1, p2, p3全部返回成功,p 才会返回成功, p1, p2, p3中任意一个返回失败,p 就返回失败。 失败后,其他异步任务仍会继续执行。
  • Promise.race(): p1, p2, p3任意一个返回成功,p 就返回成功, p1, p2, p3中任意一个返回失败,p 就返回失败。 失败后,其他异步任务仍会继续执行。
  • Promise.allSettled():等到p1,p2,p3全部执行完,不管成功失败,p 的状态为fulfilled。监听函数接收到的参数时数组[{status:'fulfilled', value: 42}, {status:'rejeceted}, reason:-1]
  • Promise.any(): p1, p2, p3只要有一个成功,p 就返回成功,p1,p2,p3全部失败,p 才返回失败

Promise 执行时序

console.log('global start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

Promise.resolve()
  .then(() => {
    console.log('promise')
  })
  .then(() => {
    console.log('promise 2')
  })
  .then(() => {
    console.log('promise 3')
  })
console.log('global end')

// global start
// global end
// promise
// promise 2
// promise 3
// setTimeout

按照前面说的,回调进入回调队列,依次执行,可能我们会认为先打印setTimeout,再打印promise,但是结果不是这样的。这是因为js将任务分为了宏任务和微任务。微任务会插队,在本轮任务的末尾直接执行。

大部分异步任务都会作为宏任务。

微任务包括Promise,MutationObserver, process.nextTick/

Generator异步方案、Async/Await语法糖

基本使用

// 比普通的函数多了一个 *
function * foo() {
  console.log('start')

  // 用 yield 返回一个值,next 方法返回的就是这个值
  // yield 不会结束生成器的执行,只是 暂停
  // 如果next方法传入一个参数,会作为上一个yield 的返回值
  // yield 'foo'
  // const res = yield 'foo'
  // console.log(res) // bar

  try {
    const res = yield 'foo'
    console.log(res) // bar
  } catch (e) {
    console.log(e)
  }
}

// 调用生成器并不会立即执行,而是得到一个生成器对象
const generator = foo()

// 调用next方法,函数体才会执行
const result = generator.next()
// 返回结果中有一个done属性,表示生成器是否一起执行完了
console.log(result)  //{value: "foo", done: false}

// 再一次调用next方法时,会从 yield 位置开始执行
// generator.next('bar')

// 如果调用生成器的throw方法,也会继续往下执行,但是它会抛出一个异常
// 在生成器内部使用try{}catch(){}语句来接收异常
generator.throw(new Error('Generator error'))

function* main() {
  try {
    const users = yield ajax(url1)
    console.log(users)

    const posts = yield ajax(url2)
    console.log(posts)
  } catch (e) {
    console.log(e)
  }
}
function co(generator) {
  const g = generator()

  function handleResult(result) {
    if (result.done) return
    result.value.then(data => {
      handleResult(g.next(data))
    }, error => {
      g.throw(error)
    })
  }

  handleResult(g.next())
}

co(main)

Async / Await 语法糖

// 将生成器的 * 改为 async ,yield 改为 await 
async function main() {
  try {
    const users = await ajax(url1)
    console.log(users)

    const posts = await ajax(url2)
    console.log(posts)
  } catch (e) {
    console.log(e)
  }
}
// 直接调用,不需要 co
// async 函数返回一个promise对象
const promise = main()
promise.then(() => {
  console.log('all completed')
})

手撕Promise

/**
 * 手撕Promise
 * 首先,promise是一个类,传入一个函数作为参数,直接调用
 * promise 有三个状态, pending, fulfilled, rejected
 * 在 resolve 和 reject调用后状态修改,且状态修改后不能再修改
 * 将 resolve 和 reject 中的参数记录下来,作为 then 方法成功和失败回调的参数
 * 如果 promise 中执行出错,要捕获错误,可以使用try catch来捕获
 * 需要捕获错误的地方包括promise传入的函数执行器,和 then 方法的回调
 */
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECRED = 'rejected'
class MyPromise {
  constructor(fn) {
    try {
      // promise 传入一个函数,直接调用,函数的参数为 resolve 和 reject
      fn(this.resolve, this.reject)
    } catch (err) {
      this.reject(err)
    }
  }
  // 定义初始状态
  status = PENDING
  // then 方法成功回调的参数
  value = undefined
  // then 方法失败回调的参数
  error = undefined
  // 初始化存储 then 回调的值
  sCallback = []
  fCallback = []
  resolve = (value) => {
    // 如果状态不是 pending ,不做修改
    if (this.status !== PENDING) return
    // resolve 后将状态修改为成功
    this.status = FULFILLED
    // 将结果记录
    this.value = value
    // 如果有储存的成功回调,则调用,数组需要循环调用
    // this.sCallback && this.sCallback(value)
    while (this.sCallback.length) this.sCallback.shift()()
  }
  reject = (error) => {
    // 如果状态不是 pending ,不做修改
    if (this.status !== PENDING) return
    // reject 后将状态修改为失败
    this.status = REJECRED
    // 将结果记录
    this.error = error
    // 如果有储存的失败回调,则调用,数组需要循环调用
    // this.fCallback && this.fCallback(error)
    while (this.fCallback.length) this.fCallback.shift()()
  }
  /**
   * then 方法参数为成功回调和失败回调
   * 根据状态判断执行哪个回调
   * 如果是异步调用,执行 then 方法时状态还是 pending,则要将两个回调储存起来
   * 储存的方法在 resolve 和 reject 的方法里对应的调用
   * 同一个promise可能会有多个 then 调用,也就会有多组成功和失败的回调,将异步时回调储存为数组
   * then 方法可以链式调用,所以它返回的是一个promise对象,将回调中返回的值作为下一个then方法的参数
   * then 方法返回的promise对象不能是自身,将 newPromise 与 返回值进行判断
   * 在pending状态也要判断不能返回自身
   * then 方法可以不传递参数,不传递参数时,下一个then可以拿到这个then应该拿到的结果
   * 所以 then 不传递参数时,相当于把结果传递到下一个then
   */
  then(sCallback = value => value, fCallback = error => { throw error }) {
    let newPromise = new MyPromise((resolve, reject) => {
      // 这里是同步执行,所以可以将要执行的操作放在这里
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            // 调用后获取返回的值
            const x = sCallback(this.value)
            // 判断返回的值如果是 promise 对象,根据promise的结果进行resolve和reject
            // 如果是普通值,直接resolve
            // 这个操作在失败是也会调用,所以包装成一个方法
            // then 方法不能返回自己,所以将 newPromise 传进去判断
            // 但是这里其实拿不到newPromise,可以将这段代码放入 setTimeout 中
            // 放入setTimeout 中并不是为了延时,只是为了等 newPromise 创建好了可以引用,所以时间设为0
            thenValue(newPromise, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)
      } else if (this.status === REJECRED) {
        setTimeout(() => {
          try {
            const x = fCallback(this.error)
            thenValue(newPromise, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)
      } else {
        // 调用 then 方法时,promise的异步还没执行完,状态还是pending,把两个回调储存
        // 判断不能返回自身
        this.sCallback.push(() => {
          setTimeout(() => {
            try {
              const x = sCallback(this.value)
              thenValue(newPromise, x, resolve, reject)
            } catch (err) {
              reject(err)
            }
          }, 0)
        })
        this.fCallback.push(() => {
          setTimeout(() => {
            try {
              const x = fCallback(this.error)
              thenValue(newPromise, x, resolve, reject)
            } catch (err) {
              reject(err)
            }
          }, 0)
        })
      }
    })
    return newPromise
  }
  /**
   * 实现finally方法, finally 方法不管promise成功失败都会执行回调
   * finally 会将promise的结果往下传
   * 可以利用 then 方法来实现
   * finally 方法返回一个新的promise对象,由于then方法就是返回一个promise对象,所以直接返回
   * 如果finally返回一个promise对象,要等promise对象有了结果,才会执行下方的 then
   */
  finally(callback) {
    return this.then(value => {
      return MyPromise.resolve(callback()).then(() => value)
    }, err => {
      return MyPromise.resolve(callback()).then(() => { throw err })
    })
  }
  /**
   * 实现 catch,catch方法只有一个回调,就是失败回调,返回一个promise
   */
  catch(callback) {
    return this.then(undefined, callback)
  }
  /**
   * 实现一个all方法, all 方法传入一个数组,数组中会有异步调用,返回一个新的promise对象
   * 数组中所有异步都成功,将结果以数组形式返回,否则一个出错就出错
   */
  static all(args) {
    let results = []
    let index = 0
    return new MyPromise((resolve, reject) => {
      function addData(key, value) {
        results[key] = value
        // index代表给results中添加了几个值,如果index和args长度相等,说明全部成功
        // 不能用results长度来判断,因为results赋值不是通过 push 方法,而是针对 key 来赋值的
        index++
        if (index == args.length) {
          resolve(results)
        }
      }
      for (let i = 0; i < args.length; i++) {
        // 判断是promise对象还是普通值,普通值直接加入results数组
        if (args[i] instanceof MyPromise) {
          // promise 对象
          args[i].then(value => {
            addData(i, value)
          }, reject)
        } else {
          // 普通值
          addData(i, args[i])
        }
      }
    })
  }
  /**
   * 实现一个Promise.resolve方法
   * Promise.resolve方法后面要接 then 方法
   * 参数如果是个promise对象,就按照这个promise执行,返回它
   * 参数如果是个普通值,创建一个新的promise对象
   */
  static resolve(value) {
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => { resolve(value) })
  }
  /**
   * Promise.reject 方法,返回一个新的Promise,状态为reject
   * 参数原封不动的作为reject的理由
   */
  static reject(reason) {
    return new Promise((resolve, reject) => { reject(reason) })
  }
}
function thenValue(newPromise, x, resolve, reject) {
  if (newPromise === x) return reject(new TypeError('then方法不能返回自己'))
  if (x instanceof MyPromise) {
    // 如果是promise对象
    x.then(resolve, reject)
  } else {
    // 如果是普通值
    resolve(x)
  }
}

你可能感兴趣的:(学习笔记-JavaScript异步编程以及手撕Promise)