學習筆記-JavaScript異步編程以及手撕Promise

JavaScript異步編程

眾所周知,目前主流的JavaScript環(huán)境都是以單線程模式去執(zhí)行的JavaScript代碼,JavaScript采用單線程模式工作的原因與它最早的設(shè)計初衷有關(guān),最早JavaScript就是運行在瀏覽器端的腳本語言,目的是為了實現(xiàn)頁面上的動態(tài)交互,而實現(xiàn)頁面交互的核心就是dom操作,這也決定了它必須使用單線程,否則會出現(xiàn)復雜的線程同步問題。試想一下,假定我們在JavaScript中同時又多個線程一起工作,其中一個線程修改了某個dom元素,而另外一個線程同時刪除這個元素,那此時瀏覽器就無法明確該以哪個線程工作結(jié)果為準,所以為了避免線程同步的問題,從一開始JavaScript就被設(shè)計為了單線程模式工作,這也成了這門語言最為核心的特性之一。這里的單線程指的是在js執(zhí)行環(huán)境中負責執(zhí)行代碼的線程只有一個。一次只能執(zhí)行一個任務,有多個任務就需要排隊,一個一個完成。這種模式最大的優(yōu)點就是更安全更簡單,缺點也同樣很明顯,如果遇到某個特別耗時的任務,后邊的任務都必須排隊等待這個任務的結(jié)束,這也就會導致整個程序的執(zhí)行會被拖延,出現(xiàn)假死的情況。為了解決耗時任務阻塞的問題,JavaScript將任務的執(zhí)行模式分成了兩種,同步模式和異步模式。

同步模式和異步模式

同步模式

同步模式指代碼當中的任務依次執(zhí)行,后一個任務必須等待前一個任務結(jié)束,按照代碼書寫的順序執(zhí)行。

異步模式

不會去等待這個任務的結(jié)束才開始下一個任務。對于耗時任務,開啟過后就立即往后執(zhí)行下一個任務,耗時任務的后續(xù)邏輯通過回調(diào)函數(shù)的方式定義。耗時任務完成會自動執(zhí)行這里的回調(diào)函數(shù)。如果沒有異步模式,單線程的JavaScript語言無法同時處理大量耗時任務。但是對于開發(fā)者而言,異步模式最大的問題就是代碼的執(zhí)行順序混亂。

回調(diào)函數(shù):由調(diào)用者定義,交給執(zhí)行者執(zhí)行的函數(shù)。

事件循環(huán)和消息隊列

image.png

JavaScript線程首先執(zhí)行同步任務,在遇到異步任務(eg:setTimeout)的時候,發(fā)起異步調(diào)用,然后繼續(xù)執(zhí)行同步任務。與此同時,異步調(diào)用線程執(zhí)行異步任務后,將異步任務的回調(diào)放入消息隊列。待同步任務執(zhí)行完畢后,Event Loop會去消息隊列中尋找任務,依次執(zhí)行消息隊列中的任務。

異步編程的幾種方式

Promise異步方案、宏任務/微任務隊列

image.png

Promise就是一個對象,用來表示一個異步任務最終結(jié)束過后,究竟是成功還是失敗。就像是一個承諾,一開始是待定的狀態(tài)- Pending,成功后叫Fulfilled,失敗后叫Rejected。承諾明確后會有對應的任務執(zhí)行,onFilfilled, onRejected.

基本用法

const promise = new Promise(function (resolve, reject) {
    // 兌現(xiàn)承諾

    // 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對象,所以可以使用鏈式調(diào)用
  • 后面的then方法就是在為上一個then返回的Promise注冊回調(diào)
  • 前面then方法中回調(diào)函數(shù)的返回值會作為后面then方法回調(diào)的參數(shù)
  • 如果回調(diào)中返回的是Promise,那后面的then方法的回調(diào)會等待這個Promise結(jié)束

異常處理

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方法的第二個回調(diào)函數(shù)進行異常捕獲
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的第二個回調(diào)捕獲異常,只能捕獲到前一個拋出的異常,而使用catch,因為每一個then都會返回一個promise對象,所以catch首先捕獲的是前一個then 的異常,然后會捕獲鏈上往前的異常,也就是catch會捕獲鏈上catch以前的異常。

Promise 靜態(tài)方法

Promise.resolve()
Promise.reject()

Promise 并行執(zhí)行

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) {
  // 接收的是數(shù)組,包含每個異步任務執(zhí)行的結(jié)果
  console.log(values)
}).catch(function (error) {
  console.log(error)
})

Promise.race()

Promise.race()也會將多個promise對象組合返回一個新的promise對象,但與 all 不同的是:

all 等待所有任務結(jié)束,它才會結(jié)束

race 只會等待第一個結(jié)束的任務,也就是只要有一個任務完成了,新的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毫秒內(nèi)請求成功,就返回成功,使用.then方法
// 如果500毫秒請求沒有返回結(jié)果,就會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 就返回失敗。 失敗后,其他異步任務仍會繼續(xù)執(zhí)行。
  • Promise.race(): p1, p2, p3任意一個返回成功,p 就返回成功, p1, p2, p3中任意一個返回失敗,p 就返回失敗。 失敗后,其他異步任務仍會繼續(xù)執(zhí)行。
  • Promise.allSettled():等到p1,p2,p3全部執(zhí)行完,不管成功失敗,p 的狀態(tài)為fulfilled。監(jiān)聽函數(shù)接收到的參數(shù)時數(shù)組[{status:'fulfilled', value: 42}, {status:'rejeceted}, reason:-1]
  • Promise.any(): p1, p2, p3只要有一個成功,p 就返回成功,p1,p2,p3全部失敗,p 才返回失敗

Promise 執(zhí)行時序

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

按照前面說的,回調(diào)進入回調(diào)隊列,依次執(zhí)行,可能我們會認為先打印setTimeout,再打印promise,但是結(jié)果不是這樣的。這是因為js將任務分為了宏任務和微任務。微任務會插隊,在本輪任務的末尾直接執(zhí)行。

大部分異步任務都會作為宏任務。

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

Generator異步方案、Async/Await語法糖

基本使用

// 比普通的函數(shù)多了一個 *
function * foo() {
  console.log('start')

  // 用 yield 返回一個值,next 方法返回的就是這個值
  // yield 不會結(jié)束生成器的執(zhí)行,只是 暫停
  // 如果next方法傳入一個參數(shù),會作為上一個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)
  }
}

// 調(diào)用生成器并不會立即執(zhí)行,而是得到一個生成器對象
const generator = foo()

// 調(diào)用next方法,函數(shù)體才會執(zhí)行
const result = generator.next()
// 返回結(jié)果中有一個done屬性,表示生成器是否一起執(zhí)行完了
console.log(result)  //{value: "foo", done: false}

// 再一次調(diào)用next方法時,會從 yield 位置開始執(zhí)行
// generator.next('bar')

// 如果調(diào)用生成器的throw方法,也會繼續(xù)往下執(zhí)行,但是它會拋出一個異常
// 在生成器內(nèi)部使用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)
  }
}
// 直接調(diào)用,不需要 co
// async 函數(shù)返回一個promise對象
const promise = main()
promise.then(() => {
  console.log('all completed')
})

手撕Promise

/**
 * 手撕Promise
 * 首先,promise是一個類,傳入一個函數(shù)作為參數(shù),直接調(diào)用
 * promise 有三個狀態(tài), pending, fulfilled, rejected
 * 在 resolve 和 reject調(diào)用后狀態(tài)修改,且狀態(tài)修改后不能再修改
 * 將 resolve 和 reject 中的參數(shù)記錄下來,作為 then 方法成功和失敗回調(diào)的參數(shù)
 * 如果 promise 中執(zhí)行出錯,要捕獲錯誤,可以使用try catch來捕獲
 * 需要捕獲錯誤的地方包括promise傳入的函數(shù)執(zhí)行器,和 then 方法的回調(diào)
 */
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECRED = 'rejected'
class MyPromise {
  constructor(fn) {
    try {
      // promise 傳入一個函數(shù),直接調(diào)用,函數(shù)的參數(shù)為 resolve 和 reject
      fn(this.resolve, this.reject)
    } catch (err) {
      this.reject(err)
    }
  }
  // 定義初始狀態(tài)
  status = PENDING
  // then 方法成功回調(diào)的參數(shù)
  value = undefined
  // then 方法失敗回調(diào)的參數(shù)
  error = undefined
  // 初始化存儲 then 回調(diào)的值
  sCallback = []
  fCallback = []
  resolve = (value) => {
    // 如果狀態(tài)不是 pending ,不做修改
    if (this.status !== PENDING) return
    // resolve 后將狀態(tài)修改為成功
    this.status = FULFILLED
    // 將結(jié)果記錄
    this.value = value
    // 如果有儲存的成功回調(diào),則調(diào)用,數(shù)組需要循環(huán)調(diào)用
    // this.sCallback && this.sCallback(value)
    while (this.sCallback.length) this.sCallback.shift()()
  }
  reject = (error) => {
    // 如果狀態(tài)不是 pending ,不做修改
    if (this.status !== PENDING) return
    // reject 后將狀態(tài)修改為失敗
    this.status = REJECRED
    // 將結(jié)果記錄
    this.error = error
    // 如果有儲存的失敗回調(diào),則調(diào)用,數(shù)組需要循環(huán)調(diào)用
    // this.fCallback && this.fCallback(error)
    while (this.fCallback.length) this.fCallback.shift()()
  }
  /**
   * then 方法參數(shù)為成功回調(diào)和失敗回調(diào)
   * 根據(jù)狀態(tài)判斷執(zhí)行哪個回調(diào)
   * 如果是異步調(diào)用,執(zhí)行 then 方法時狀態(tài)還是 pending,則要將兩個回調(diào)儲存起來
   * 儲存的方法在 resolve 和 reject 的方法里對應的調(diào)用
   * 同一個promise可能會有多個 then 調(diào)用,也就會有多組成功和失敗的回調(diào),將異步時回調(diào)儲存為數(shù)組
   * then 方法可以鏈式調(diào)用,所以它返回的是一個promise對象,將回調(diào)中返回的值作為下一個then方法的參數(shù)
   * then 方法返回的promise對象不能是自身,將 newPromise 與 返回值進行判斷
   * 在pending狀態(tài)也要判斷不能返回自身
   * then 方法可以不傳遞參數(shù),不傳遞參數(shù)時,下一個then可以拿到這個then應該拿到的結(jié)果
   * 所以 then 不傳遞參數(shù)時,相當于把結(jié)果傳遞到下一個then
   */
  then(sCallback = value => value, fCallback = error => { throw error }) {
    let newPromise = new MyPromise((resolve, reject) => {
      // 這里是同步執(zhí)行,所以可以將要執(zhí)行的操作放在這里
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            // 調(diào)用后獲取返回的值
            const x = sCallback(this.value)
            // 判斷返回的值如果是 promise 對象,根據(jù)promise的結(jié)果進行resolve和reject
            // 如果是普通值,直接resolve
            // 這個操作在失敗是也會調(diào)用,所以包裝成一個方法
            // then 方法不能返回自己,所以將 newPromise 傳進去判斷
            // 但是這里其實拿不到newPromise,可以將這段代碼放入 setTimeout 中
            // 放入setTimeout 中并不是為了延時,只是為了等 newPromise 創(chuàng)建好了可以引用,所以時間設(shè)為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 {
        // 調(diào)用 then 方法時,promise的異步還沒執(zhí)行完,狀態(tài)還是pending,把兩個回調(diào)儲存
        // 判斷不能返回自身
        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
  }
  /**
   * 實現(xiàn)finally方法, finally 方法不管promise成功失敗都會執(zhí)行回調(diào)
   * finally 會將promise的結(jié)果往下傳
   * 可以利用 then 方法來實現(xiàn)
   * finally 方法返回一個新的promise對象,由于then方法就是返回一個promise對象,所以直接返回
   * 如果finally返回一個promise對象,要等promise對象有了結(jié)果,才會執(zhí)行下方的 then
   */
  finally(callback) {
    return this.then(value => {
      return MyPromise.resolve(callback()).then(() => value)
    }, err => {
      return MyPromise.resolve(callback()).then(() => { throw err })
    })
  }
  /**
   * 實現(xiàn) catch,catch方法只有一個回調(diào),就是失敗回調(diào),返回一個promise
   */
  catch(callback) {
    return this.then(undefined, callback)
  }
  /**
   * 實現(xiàn)一個all方法, all 方法傳入一個數(shù)組,數(shù)組中會有異步調(diào)用,返回一個新的promise對象
   * 數(shù)組中所有異步都成功,將結(jié)果以數(shù)組形式返回,否則一個出錯就出錯
   */
  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數(shù)組
        if (args[i] instanceof MyPromise) {
          // promise 對象
          args[i].then(value => {
            addData(i, value)
          }, reject)
        } else {
          // 普通值
          addData(i, args[i])
        }
      }
    })
  }
  /**
   * 實現(xiàn)一個Promise.resolve方法
   * Promise.resolve方法后面要接 then 方法
   * 參數(shù)如果是個promise對象,就按照這個promise執(zhí)行,返回它
   * 參數(shù)如果是個普通值,創(chuàng)建一個新的promise對象
   */
  static resolve(value) {
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => { resolve(value) })
  }
  /**
   * Promise.reject 方法,返回一個新的Promise,狀態(tài)為reject
   * 參數(shù)原封不動的作為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)
  }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容