異步與回調/函數的作用域鏈

異步與回調/函數的作用域鏈

JavaScript 只在一個線程上運行,JavaScript 同時只能執行一個任務,其他任務都必須在後面排隊等待。 這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。 JavaScript 語言本身並不慢,慢的是讀寫外部數據,比如等待 Ajax 請求返回結果。這個時候,如果對方服務器遲遲沒有響應,或者網絡不通暢,就會導致腳本的長時間停滯。

異步與回調

同步任務與異步任務

程序裏面所有的任務,可以分成兩類:同步任務(synchronous)和異步任務(asynchronous)。

同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行後一個任務。

異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認爲某個異步任務可以執行了(比如 Ajax 操作從服務器得到了結果),該任務(採用回調函數的形式)纔會進入主線程執行。排在異步任務後面的代碼,不用等待異步任務結束會馬上運行,也就是說,異步任務不具有”堵塞“效應。

舉例來說,Ajax 操作可以當作同步任務處理,也可以當作異步任務處理,由開發者決定。如果是同步任務,主線程就等着 Ajax 操作返回結果,再往下執行;如果是異步任務,主線程在發出 Ajax 請求以後,就直接往下執行,等到 Ajax 操作有了結果,主線程再執行對應的回調函數。

任務隊列和事件循環

JavaScript 運行時,除了一個正在運行的主線程,引擎還提供一個任務隊列(task queue),裏面是各種需要當前程序處理的異步任務。(實際上,根據異步任務的類型,存在多個任務隊列。爲了方便理解,這裏假設只存在一個隊列。)

首先,主線程會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務隊列裏面的異步任務。如果滿足條件,那麼異步任務就重新進入主線程開始執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。

異步任務的寫法通常是回調函數。一旦異步任務重新進入主線程,就會執行對應的回調函數。如果一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會重新進入主線程,因爲沒有用回調函數指定下一步的操作。

JavaScript 引擎怎麼知道異步任務有沒有結果,能不能進入主線程呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務執行完了,引擎就會去檢查那些掛起來的異步任務,是不是可以進入主線程了。這種循環檢查的機制,就叫做事件循環(Event Loop)。維基百科的定義是:“事件循環是一個程序結構,用於等待和發送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。

異步操作

異步操作的模式--回調函數 有這樣一個問題:

我想先定個鬧鐘,三秒鐘後鬧鐘就會響.這時候我再起牀.

如果代碼這樣寫:

function setClock(){
  console.log('1定一個鬧鐘,三秒鐘之後響');
  setTimeout(()=>{
    console.log('2三秒到了,鬧鐘響了!');
  },3000)
}

function getUp(){
  console.log('3鬧鐘已經響了,該起牀了')
}
setClock();//定鬧鐘
getUp();//起牀

結果:

getUp();//起牀這個函數不會等到三秒後執行,而是會在setClock()執行後立即執行.

異步就是不等結果,直接進行下一步. setClock();//定鬧鐘執行完了之後直接進行下一步getUp();//起牀

setClock();//定鬧鐘就是異步代碼,不等待setClock()執行完就執行getUp(),setClock()就是異步任務

解決方法是使用回調函數: 回調是拿到異步結果的一種方式 (其實回調也可以拿同步結果) 舉一個例子:

  • 同步:我讓黃牛去買票,我站着等他買好票再給我,然後再去做別的.
  • 異步:我讓黃牛去買票(告訴黃牛買到票就call我一下),然後我繼續去做別的事

這裏:我讓黃牛去買票,然後我繼續去做別的事就是異步,括號裏的(告訴黃牛買到票就call我一下)就是回調

callBack英文有回電話的意思.就是打電話回去告訴異步結果已經得到了,可以繼續依照這個結果來做下面的事了.callBack就是這個意思

代碼執行完在執行下面的代碼就是同步,代碼沒有執行完就去執行下面的代碼就是異步

使用回調函數

function setClock(callBack){
  console.log('1定一個鬧鐘,三秒鐘之後響');
  setTimeout(()=>{
    console.log('2三秒到了,鬧鐘響了!');
    callBack();
  },3000)
}

function getUp(){
  console.log('3鬧鐘已經響了,該起牀了')
}

setClock(getUp);

getUp作爲參數傳入setClock函數,等三秒後在執行函數.getUp就是回調函數

區分同步和異步

就是因爲有了setTimeout纔算異步

所以我們來看看ajax.如果$.ajax()是同步的,即我們發送請求,然後等待服務器發回的響應來到之後在繼續執行下面的代碼,那麼有什麼後果:

假設我們想直接拿到請求的結果,那麼我們有下面的代碼:

意思就是不管請求相應多久,都等着,直到響應接收到,然後返回給這個創建的變量response.如果從發送請求到拿到相應用了2s,那麼代碼就停在這裏了2s.

所以$.ajax()是異步的,我們拿到的只是一個承諾(Promise),我承諾會執行,並承諾會在拿到結果後執行什麼什麼什麼 如下:

所以就可以使用promise.then(success,error)承諾成功之後執行success函數,承諾失敗後執行error函數. 這個success,error就是callBack(回調函數),這個Promise(承諾)就是異步任務 promise就是知道沒法得到結果,那我就要你一個承諾,要承諾好拿到結果後要做什麼事. 所以$.ajax()返回的結果是一個承諾,不是結果,因爲結果還沒有到來

使用回調函數

使用回調要用這樣的形式

fn(參數1,參數2,()=>{
    回調函數(xxx,xxx,()=>{})
})

不要用

fn(參數1,參數2,回調函數(xxx,xxx))

因爲這個參數裏傳入的回調函數(xxx,xxx)並不是函數本身,而是運行完畢之後的返回值.

下面帶我是我的一個小作品裏的一部分代碼,一直在嵌套回調函數.

會動的簡歷--完整代碼地址 會動的簡歷--預覽地址

函數的作用域鏈

先看面試題 題目1

var a = 1
function fn1(){
  function fn2(){
    console.log(a)
  }
  function fn3(){
    var a = 4
    fn2()
  }
  var a = 2
  return fn3
}
var fn = fn1()
fn() //2

題目2

var a = 1
function fn1(){
  function fn3(){
    var a = 4
    fn2()
  }
  var a = 2
  return fn3
}
function fn2(){
  console.log(a)
}
var fn = fn1()
fn() //1

題目3

var a = 1
function fn1(){

  function fn3(){
    function fn2(){
      console.log(a)
    }
    var a

    fn2()
    a = 4
  }
  var a = 2
  return fn3
}
var fn = fn1()
fn() //undefined

解密

  1. 函數在執行的過程中,先從自己內部找變量
  2. 如果找不到,再從創建當前函數所在的作用域去找, 以此往上
  3. 注意找的是變量的當前的狀態
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章