主頁 > 前端設計 > 【深扒】深入理解 JavaScript 中的異步編程

【深扒】深入理解 JavaScript 中的異步編程

2021-08-17 07:57:25 前端設計

異步編程

大家好,我是小丞同學,本文將會帶你理解和感受 Generator 函式的異步應用

引言

我們先引出一個非常常見的場景:對服務器端回傳的資料進行操作

與服務器端互動的程序是一個異步操作

如果按照正常的代碼撰寫的話,你可能會寫出這樣的代碼

我也不知道打的什么,大概意思就是異步請求結果回傳賦值給 data 然后輸出,

let data = ajax("http://127.0.0.1",ab) //隨便寫的
console.log(data)

雖然整個思路看起來沒什么毛病,對吧,但是它就是不行的,獲取資料是異步的,也就是說請求資料的時候,輸出已經執行了,這時候必然是 undefined

那為什么它要這么做呢?

JavaScript 是一門單執行緒的語言,如果沒有了異步執行,你想想會怎么樣

就像逛街一樣,你非要跟著前面的人走,它走了你才走,它停下了去買點東西,后面的人全部都停下來等它回來,那這會怎么辦,很顯然,路堵了!換到 JS 運行機制上來也是一樣的,會阻塞代碼運行,因此出現了“異步”的概念,接下來我們先了解一下異步的概念,以及傳統方法是如何實作異步操作的

什么是同步、異步

同步:任務會按順序依次執行,當遇到大量耗時任務,后面的任務就會被延遲,這種延遲稱為阻塞,阻塞會造成頁面卡頓

異步:不會等待耗時任務,遇到異步任務就開啟后立即執行下一個任務,耗時任務的后續邏輯通常通過回呼函式來定義執行,代碼執行順序混亂

實作異步編程

在 ES6 誕生之前,實作異步編程的方法有以下幾種,

  1. 回呼函式
  2. 事件監聽
  3. 發布/訂閱
  4. Promise 物件

下面來先來回顧以下傳統方法是如何實作異步編程的

Callback

回呼函式可以理解為一件想要去做的事情,由呼叫者定義好函式,交給執行者在某個時機去執行,把需要執行的操作放在函式里,將函式傳入給執行者執行

主要體現在,把任務的第二段寫在一個函式里面,等到重新執行這個任務的時候,直接呼叫

那有人就會問了,第二段是指什么,我們再舉一個例子,讀取檔案進行列印,這個操作肯定是異步的吧,那它怎么分兩段呢?

按照邏輯來分,第一段是讀取檔案,第二段是列印檔案,可以理解為第一段是請求資料,第二段是列印資料

阮老師的代碼實體

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});

在第一階段執行結束后,會將結果回傳給后面的函式作為引數,傳入第二段

回呼函式的使用場景:

  1. 事件回呼

  2. 定時器的回呼

  3. Ajax 請求

Promise

采用回呼函式的方法,本身是沒有問題的,但是問題出現在多個回呼函式的嵌套

想一想,我執行完執行你,你執行完執行他,他執行完又執行她…

是不是需要層層嵌套,那這樣套娃式的操作顯然不利于閱讀

fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

同時你也可以這樣去思考一下,如果有其中一個代碼需要修改,那它的上層回呼和下層回呼都要修改,這也叫做強耦合

耦合,藕斷絲連,關聯性很強的意思

這種場景也叫做“回呼地獄”

而 Promise 物件的誕生就是為了解決這個問題,它采用了以一種全新的寫法,鏈式呼叫

Promise 可以用來表示一個異步任務執行的狀態,有三種狀態

  • Pending:開始是等待狀態
  • Fulfilled:成功的狀態,會觸發 onFulfilled
  • Rejected:失敗的狀態,會觸發 onRejected

它的寫法如下

const promise = new Promise(function(resolve, reject) {
    // 同步代碼
    // resolve執行表示異步任務成功
    // reject執行表示異步任務失敗
    resolve(100)
    // reject(new Error('reject')) // 失敗
})
promise.then(function() {
    // 成功的回呼
}, function () {
    // 失敗的回呼
})

Promise 物件呼叫 then 方法后會回傳一個新的 Promise 物件,這個新的 Promise 物件可以繼續呼叫 then 實作鏈式呼叫

后面的 then 方法是為上一個 then 回傳的 Promise 物件注冊回呼

前一個 then 方法中回呼函式的回傳值會作為后面 then 方法回呼的引數

鏈式呼叫的目的是為了解決回呼函式嵌套的問題

關于 Promise 的更多細節這里就不多說了,下一篇寫吧~

壞了,壞了,環環嵌套,我陷入回呼地獄了,努力更文

Promise 成功的解決了回呼地獄的問題,它又不是異步編程的終極方案,那它又帶來了什么問題呢?

  1. 無法取消 Promise
  2. 當處于 pending 狀態時是,無法得知進展
  3. 錯誤不能被 catch

但是這些都不是 Promise 的最大問題,它最大的問題是代碼冗余,當執行邏輯變得復雜時,代碼的語意會變得很不清楚,全是 then

其實看過上一篇文章的讀者們,看到這里應該對 Generator 實作異步編程有了一定的眉目,這里的 then 方法的作用,似乎 next 方法也能實作,啟動,運行,傳參,接下來我們來細說一下

Generator

Generator 函式可以暫停執行恢復執行, 這是它能封裝異步任務的根本原因,
除此之外,它還有兩個特征,使它可以作為異步編程的完美解決方案,

  • 函式體內外的資料傳遞
  • 錯誤處理機制

資料傳遞

在學習它是如何實作異步編程的之前,我們先回顧一下 Generator 函式的執行方法

// 宣告Generator函式
function* gen(x){
	let y = yield x + 2
  return y
}
// 遍歷器物件
let g = gen()
// 第一次呼叫next方法
g.next()  // { value: 3, done: false }
// 第二次呼叫 傳遞引數
g.next(2) // { value: 2, done: true }

首先執行 gen 函式,獲得遍歷器物件,此時函式并不會執行,當呼叫遍歷器物件的 next 方法時,執行到第一個 yield 陳述句,以此類推

也就是說只有呼叫 next 方法,才會往下執行

同時在上面的代碼中,我們可以通過 value 來獲取回傳的值,通過給 next 方法傳遞引數來實作資料交換

錯誤處理機制

Generator 函式內部可以部署錯誤處理代碼,捕獲函式體外拋出的錯誤

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出錯了');

或許會有人不理解為什么內部的 catch 可以捕獲外部的錯誤?

原因是我們通過 g.throw 來拋錯誤,其實是將錯誤拋入了生成器,畢竟我們是在 p 上來呼叫 throw 方法

實作異步編程

在我的上一篇文章詳細的介紹了生成器的執行機制,以及 yield 執行特點,可以先閱讀一下

我們主意利用 yield 暫停生成器函式執行的特點,來使用生成器函式去實作異步編程,我們來看一個例子

Generator + Promise
function * main () {
    const user = yield ajax('/api/usrs.json')
    console.log(user)
}
const g = main()
const result = g.next()
result.value.then(data => {
    g.next(data)
})

首先我們定義一個生成器函式 main ,然后在這個函式內部使用 yield 去回傳一個 ajax 的呼叫,也就是回傳了一個 Promise 物件,

然后去接收 yield 陳述句的回傳值,也就是第二個 next 方法的引數,

我們可以在外界去呼叫生成器函式得到它的迭代器物件,然后呼叫這個物件的 next 方法,這樣 main 函式就會執行到第一個 yield 的位置,也就是會執行到 ajax 的呼叫,這里 next 方法回傳物件的 value 值就是 ajax 回傳的 Promise 物件

因此我們可以通過 then 方法去指定這個 Promise 的回呼,在這個 Promise 回呼中我們就可以拿到這個 Promise 的執行結果 data,這時候我們就可以通過再呼叫一次 next 方法,把我們得到的 data 資料傳遞出去,這樣 main 函式就可以繼續執行了,而 data 就會被當作 yield 運算式的回傳值賦值給 user 使用了

異步迭代生成器

如果上面的 generator + promise 能夠理解的話,這個就更簡單了,就是單純的使用 generator 實作的異步編程

function foo(x, y) {
    ajax("1.2.34.2", function(err,data) {
        if(err) {
            it.throw(err)
        }else {
            it.next(data)
        }
    })
}
function *main() {
    let text = yield foo(11, 31)
	console.log( text )
}
const it = main()
it.next()

在上面的代碼中就是一個簡單的例子,雖然看起來要比回呼函式實作的方法要多很多,但是你會發現代碼邏輯要好非常多

這里面最關鍵的代碼

let text = yield foo(11,31)
console.log( text )

這個在上一 part 我們已經解釋過了

yield foo(11, 31) 中,首先呼叫 foo(11, 31) 沒有回傳值,發送請求獲取資料,請求成功,呼叫 it.next(data) ,這樣就將 data 作為上一個 yield 的回傳值,這樣就將異步代碼同步化了

async await

在 Generator 中還有很多的內容,工具,并發,委托等等讓生成器變得十分強大,但是這樣也讓手寫一個執行器函式越來越麻煩,所以在 ES7 中又新增了 async await 這對關鍵字,它使用起來會更加的方便,

async 函式就是生成器函式的一個語法糖,

在語法上跟 Generator 函式非常類似,只要把生成器函式修改為 async 關鍵字修飾的函式,把 yield 修改為 await 就可以了,并且可以直接在外面呼叫這個函式,執行這個函式的話,內部這個執行程序會跟 Generator 函式會是完全一樣的

相比于 Generator 函式 async 函式最大的好處就是不需要去配合一些工具去使用,類似于 Corunner 之類的

原因在于它是語言層面的標準異步編程,同時 async 函式可以回傳一個 Promise 物件,這樣也有利于控制代碼,

需要注意的是,await 只能出現在 async 函式體中

//將生成器函式改為 async 修飾的函式
async function main() {
    try {
        // 將 yield 換成 await
        const a = await ajax('xxxx')
        console.log(a)
        const b = await ajax('xxx')
        console.log(b)
        const c = await ajax('xx')
        console.log(c)
    } catch (e) {
        console.log(e)
    }
}
// 回傳一個Promise物件
const promise = main()

從上面的代碼我們也可以知道,我們并不需要像 Generator 一樣通過 next 來控制執行

async await 是 Generator 和 Promise 的組合,解決了先前方法留下的問題,這應該是目前處理異步的最優方案了

總結

本文寫了異步編程的4個階段,這是一個不斷進步的程序,一步步的解決前面方法所帶來的問題,

  1. 回呼函式:導致了兩個問題
    • 缺乏順序性:回呼地獄,造成代碼難以維護,閱讀性差等問題
    • 缺乏可信任性:控制反轉,導致代碼可能會執行錯誤
  2. promise:解決了可信任性的問題,但是代碼過于冗余
  3. Generator:解決了順序性的問題但是需要手動控制 next,同時搭配工具使用代碼會十分的復雜
  4. async await:結合了 generator + promise,無需手動呼叫,完美解決

參考文獻

  1. 《JavaScript》異步編程
  2. 《Generator》函式的異步應用
  3. 《JavaScript高級程式設計(第四版)》

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/294235.html

標籤:其他

上一篇:講講ref參考

下一篇:JavaScript易錯陷阱:區域變數污染全域變數

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more