首頁技術文章正文

前端與移動開發(fā)JavaScript異步演變史

更新時間:2018-12-26 來源:黑馬程序員技術社區(qū) 瀏覽量:

當?shù)谝淮螌W習編程的時候,并不知道什么是回調(Callbacks),那現(xiàn)在同樣假設你不知道什么是回調來進行講解,如果我假設錯了,只需要向下滾動即可,節(jié)省一下時間


當我第一次學習編程時,它幫助我將函數(shù)理解為機器。這些機器可以做任何你想要的東西。他們甚至可以接受輸入并返回一個值。每臺機器上都有一個按鈕,你可以在需要機器運行時按下該按鈕,即()。
function add (x, y) {
  return x + y
}

add(2,3) // 5 - 按下按鈕,執(zhí)行機器
復制代碼無論我按下按鈕,你按下按鈕,或者別人按下按鈕無所謂。無論何時按下按鈕,機器都將運行。
function add (x, y) {
  return x + y
}

const me = add
const you = add
const someoneElse = add

me(2,3) // 5 - Press the button, run the machine.
you(2,3) // 5 - Press the button, run the machine.
someoneElse(2,3) // 5 - Press the button, run the machine.
復制代碼在上面的代碼,我們分配add函數(shù),三個不同的變量,me,you,和someoneElse。重要的是要注意add我們創(chuàng)建的原始變量和每個變量都指向內(nèi)存中的相同位置。它們在不同的名稱下完全相同。所以,當我們調用me時you,或者someoneElse,就好像我們正在調用一樣add函數(shù)。
現(xiàn)在如果我們把add機器送到另一臺機器怎么辦?請記住,按下()按鈕并不重要,如果按下它,它就會運行。
function add (x, y) {
  return x + y
}

function addFive (x, addReference) {
  return addReference(x, 5) // 15 - Press the button, run the machine.
}

addFive(10, add) // 15
復制代碼你的大腦可能在這一點上有點奇怪,但這里沒有新的東西。我們不是“按下按鈕” add,而是add作為參數(shù)傳遞addFive,重命名它addReference,然后我們“按下按鈕”或調用它。
這突出了JavaScript語言的一些重要概念。首先,正如你可以將字符串或數(shù)字作為參數(shù)傳遞給函數(shù)一樣,你也可以將函數(shù)的引用作為參數(shù)傳遞。當執(zhí)行此操作時,作為參數(shù)傳遞的函數(shù)稱為回調函數(shù),并且將回調函數(shù)傳遞給的函數(shù)稱為高階函數(shù)。
因為詞匯很重要,所以這里的代碼與重新命名的變量相同,以匹配他們演示的概念。
function add (x,y) {
  return x + y
}

function higherOrderFunction (x, callback) {
  return callback(x, 5)
}

higherOrderFunction(10, add)
復制代碼這種模式應該看起來很熟悉,無處不在。如果你曾經(jīng)使用過任何JavaScript Array方法,那么你已經(jīng)使用了回調。如果你曾經(jīng)使用過lodash,那么你已經(jīng)使用過回調。如果你曾經(jīng)使用過jQuery,那么你已經(jīng)使用了回調。
[1,2,3].map((i) => i + 5)

_.filter([1,2,3,4], (n) => n % 2 === 0 );

$('#btn').on('click', () =>
  console.log('Callbacks are everywhere')
)
復制代碼通常,回調有兩種常見的用例。第一,我們看下.map和_.filter例子,是翻轉一個值到另一個很好的抽象。我們說“嘿,這是一個數(shù)組和一個函數(shù)。來吧,根據(jù)我給你的函數(shù)給我一個新的值“。第二個,也就是我們在jQuery示例中看到的,是將函數(shù)的執(zhí)行延遲到特定時間?!昂?,這是這個函數(shù)。每當btn點擊具有id的元素時,請繼續(xù)調用它。“這是我們將關注的第二個用例,”延遲執(zhí)行函數(shù)直到特定時間“。
現(xiàn)在我們只看了同步的例子。正如我們在本文開頭所討論的那樣,我們構建的大多數(shù)應用程序都沒有預先獲得所需的所有數(shù)據(jù)。相反,他們需要在用戶與應用程序交互時獲取外部數(shù)據(jù)。我們剛剛看到回調如何成為一個很好的用例,因為它們再次允許你“延遲執(zhí)行函數(shù)直到特定時間”??纯次覀?nèi)绾问乖摼渥舆m應數(shù)據(jù)提取并不需要太多想象力。我們可以延遲函數(shù)的執(zhí)行,直到我們獲得所需的數(shù)據(jù),而不是將函數(shù)的執(zhí)行延遲到特定時間。這可能是最流行的例子,jQuery的方法:getJSON。
// updateUI and showError are irrelevant.
// Pretend they do what they sound like.

const id = 'tylermcginnis'

$.getJSON({
  url: `https://api.github.com/users/${id}`,
  success: updateUI,
  error: showError,
})
復制代碼在獲得用戶數(shù)據(jù)之前,我們無法更新應用的UI。那么我們該怎么辦?我們說,“嘿,這是一個對象。如果請求成功,請繼續(xù)調用success并傳遞用戶的數(shù)據(jù)。如果沒有,請繼續(xù)調用error并傳遞錯誤對象。你不需要擔心每種方法的作用,只要確保在你應該的時候調用它們。這是使用異步請求回調的完美演示。
在這一點上,我們已經(jīng)了解了回調是什么以及它們?nèi)绾卧谕胶彤惒酱a中都有用處的。我們還沒有談到的是回調的黑暗面。請看下面的代碼。你能說出發(fā)生了什么嗎?
// updateUI, showError, and getLocationURL are irrelevant.
// Pretend they do what they sound like.

const id = 'tylermcginnis'

$("#btn").on("click", () => {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: (user) => {
      $.getJSON({
        url: getLocationURL(user.location.split(',')),
        success (weather) {
          updateUI({
            user,
            weather: weather.query.results
          })
        },
        error: showError,
      })
    },
    error: showError
  })
})
復制代碼如果覺得有幫助,你可以在這里玩實時版本。
請注意,我們添加了一些回調層。首先,我們說在btn點擊具有id的元素之前不要運行初始的AJAX請求。單擊按鈕后,我們會發(fā)出第一個請求。如果該請求成功,我們會發(fā)出第二個請求。如果該請求成功,我們將調用updateUI從兩個請求獲得的數(shù)據(jù)的方法。無論你是否乍一看是否理解了代碼,客觀地說它比以前的代碼更難閱讀。這將我們帶到“回調地獄”的主題。
作為人類,我們很自然地會順序思考。當你在嵌套回調中嵌套回調時,它會強迫你超出你自然的思維方式。當你的軟件閱讀方式與自然思考方式之間存在脫節(jié)時,就會發(fā)生錯誤。
像大多數(shù)軟件問題的解決方案一樣,一種使“回調地獄”更容易消費的常用方法是模塊化你的代碼。
function getUser(id, onSuccess, onFailure) {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: onSuccess,
    error: onFailure
  })
}

function getWeather(user, onSuccess, onFailure) {
  $.getJSON({
    url: getLocationURL(user.location.split(',')),
    success: onSuccess,
    error: onFailure,
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis", (user) => {
    getWeather(user, (weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    }, showError)
  }, showError)
})
復制代碼如果覺得有幫助,你可以在這里玩實時版本。
好的,函數(shù)名稱可以幫助我們更加了解正在發(fā)生的事情,但客觀上是“更好”嗎?并不是很多。我們只是在回調地獄的可讀性問題上加了一個創(chuàng)可貼。問題仍然存在,我們自然地按順序思考,即使有額外的功能,嵌套的回調也會使我們擺脫順序的思維方式。
下一期回調與控制反轉有關。當你編寫一個回調時,假設你給回調的程序是負責的,并且會在它應該的時候(并且只有當它)時調用它。實際上是將程序控制權轉換為另一個程序。當您處理jQuery,lodash甚至vanilla JavaScript等庫時,可以安全地假設使用正確的參數(shù)在正確的時間調用回調函數(shù)。但是,對于許多第三方庫,回調函數(shù)是您與它們交互方式的接口。第三方庫無論是故意的還是偶然的,都可以打破他們與你的回調互動的方式,這是完全合情合理的。
function criticalFunction () {
  // It's critical that this function
  // gets called and with the correct
  // arguments.
}

thirdPartyLib(criticalFunction)
復制代碼既然你不是那個調用者criticalFunction,你就可以控制調用它的時間和參數(shù)。大多數(shù)時候這不是問題,但是當它出現(xiàn)問題時,這是一個很大的問題。
Promises
你有沒有預訂去過一個繁忙的餐館?當這種情況發(fā)生時,餐廳需要一種方法在桌子打開時與你聯(lián)系。從歷史上看,當你的桌子準備就緒時,他們只會取你的名字并大喊大叫。然后,自然而然地,他們決定開始變幻想。一個解決方案是,一旦桌子打開,他們就會取你的號碼并給你發(fā)短信,而不是取你的名字。這使您可以超出大喊大叫的范圍,但更重要的是,它允許他們隨時根據(jù)需要定位你的手機廣告。聽起來有點熟?這應該!好吧,也許不應該。這是回調的隱喻!將你的號碼提供給餐館就像給第三方服務提供回撥功能一樣。你希望餐廳在桌子打開時給您發(fā)短信,就像你一樣期望第三方服務在何時以及如何表達時調用你的功能。一旦你的號碼或回叫功能掌握在他們手中,您就失去了所有控制權。
值得慶幸的是,存在另一種解決方案。一個設計,允許您保持所有控制。你甚至可能以前都經(jīng)歷過 - 這是他們給你的小嗡嗡聲。你知道,這個。

如果你之前從未使用過,那么這個想法很簡單。他們沒有取你的名字或號碼,而是給你這個設備。當設備開始嗡嗡作響并發(fā)光時,你的桌子就準備好了。當你等待桌子打開時,你仍然可以做任何你想做的事,但現(xiàn)在你不必放棄任何東西。事實上,恰恰相反。他們必須給你一些東西。沒有控制倒置。
蜂鳴器始終處于三種不同狀態(tài)中的一種- pending,fulfilled或rejected。
pending是默認的初始狀態(tài)。當他們給你蜂鳴器時,它處于這種狀態(tài)。
fulfilled 當蜂鳴器閃爍并且你的桌子準備就緒時蜂鳴器所在的狀態(tài)。
rejected當出現(xiàn)問題時,蜂鳴器處于狀態(tài)。也許餐廳即將關閉,或者他們忘了有人在晚上出租餐廳。
同樣,要記住的重要一點是,你,蜂鳴器的接收器,擁有所有的控制權。如果蜂鳴器進入fulfilled,你可以去你的桌子。如果它被放入fulfilled并且你想忽略它,那么很酷,你也可以這樣做。如果它被放入rejected,那很糟糕,但你可以去別的地方吃。如果沒有任何事情發(fā)生并且它留在pending,你永遠不會吃,但你實際上并沒有任何東西。
現(xiàn)在你已成為餐廳蜂鳴器的主人,讓我們將這些知識應用到重要的事情上。
如果給餐廳你的號碼就像給他們一個回調功能,接收這個小小的東西就像收到所謂的“Promise”。
一如既往,讓我們從為什么開始吧。為什么Promises存在?它們的存在使得使異步請求更易于管理的復雜性。完全像蜂鳴器,一個 Promise可以處于三種狀態(tài)之一pending,fulfilled或者rejected。與蜂鳴器不同,它們代表表示餐館桌子狀態(tài)的這些狀態(tài),它們代表異步請求的狀態(tài)。
如果異步請求仍在進行中,則Promise狀態(tài)為pending。如果異步請求成功完成,則Promise狀態(tài)將更改為fulfilled。如果異步請求失敗,Promise則將更改為狀態(tài)rejected。蜂鳴器比喻很有意義,對嗎?
既然你已經(jīng)理解了Promise存在的原因以及它們可以存在的不同狀態(tài),那么我們還需要回答三個問題。
1、如何創(chuàng)造一個Promise?
2、如何改變Prommise的狀態(tài)?
3、當Promise的狀態(tài)發(fā)生變化時,如何監(jiān)聽?
如何創(chuàng)造一個Promise?
這個很直接。創(chuàng)建一個new實例Promise。
const promise = new Promise()
復制代碼如何改變Prommise的狀態(tài)?
該Promise構造函數(shù)接受一個參數(shù),一個(回調)函數(shù)。這個函數(shù)將傳遞兩個參數(shù),resolve和reject。
resolve - 一個允許你更改Promise狀態(tài)的功能 fulfilled
reject- 一個允許你更改Promise狀態(tài)的功能rejected。
在下面的代碼中,我們使用setTimeout等待2秒然后調用resolve。這將改變Promise的狀態(tài)fulfilled。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve() // Change status to 'fulfilled'
  }, 2000)
})

復制代碼我們可以通過在創(chuàng)建它之后立即記錄promise來看到這種變化,然后resolve在調用之后大約2秒后再次記錄。

注意Promise從pending到resolved。
當Promise的狀態(tài)發(fā)生變化時,如何監(jiān)聽?
在我看來,這是最重要的問題。很酷我們知道如何創(chuàng)建Promise并改變其狀態(tài),但如果我們在狀態(tài)發(fā)生變化后不知道如何做任何事情,那就毫無價值。
我們還沒有談到的一件事是Promise實際上是什么。當你創(chuàng)建一個時new Promise,你真的只是創(chuàng)建一個普通的舊JavaScript對象。該對象可以調用兩個方法then,和catch。這是關鍵。當promise的狀態(tài)更改fulfilled為時,.then將調用傳遞給的函數(shù)。當promise的狀態(tài)更改rejected為時,.catch將調用傳遞給的函數(shù)。這意味著一旦你創(chuàng)建了一個promise,如果異步請求成功,你將傳遞你想要運行的函數(shù).then。如果異步請求失敗,你將傳遞要運行的功能.catch。
我們來看一個例子吧。我們將setTimeout再次使用fulfilled在兩秒鐘(2000毫秒)之后將Promise的狀態(tài)更改為。
function onSuccess () {
  console.log('Success!')
}

function onError () {
  console.log('    


文章來源 :
作者:黑馬程序員前端與移動開發(fā)培訓學院    
首發(fā):http://web.itheima.com/?v2
分享到:
在線咨詢 我要報名
和我們在線交談!