摘要:一個沒有返回值的函數執行的效果其實是利用它的副作用一個沒有返回值和利用副作用的函數其實就是一個黑洞。
本篇博客尚未上傳 github
github 首頁(star+watch,一手動態直達): https://github.com/HCThink/h-blog
掘金 link , 掘金 專欄
segmentfault 主頁
原創禁止私自轉載
異步處理方案系列- 1.callback 引言異步/異步操作,已經是前端領域一個老生常談的話題.也是做前端開發中經常面臨的一個問題.
然而異步的問題往往比較復雜且難于處理, 特別是異步問題還經常不是多帶帶出現,往往存在比較多樣的組合關系.
在實際處理中就顯得更加復雜而難于處理. 特別是在 io 操作頻繁,或者 node server 中,經常遇到非常復雜的組合型異步。
舉個業務開發中常見的例子:
eg: 省市縣三級級聯問題
這個問題非常常見, 假設數據量較大, 我們大多數情況下不會一次加載所有的數據, 然后做前端級聯的方案.
而是采取三個數據接口,在下拉改變的時候去動態請求的方式.這就形成一種很常見的多個異步串行的模型.
怎么處理這樣的問題, 怎么較好的維護多個異步之間的關系, 怎么讓代碼正常執行的同時,在邏輯和結構上更可讀呢?
我將會梳理
callback
cps
thunk
defer / promise(非 es6)
promise(ES6)
generator -> co.
async / await
這幾種處理方式. 加上兩種模式
事件監聽
訂閱發布模型
列出一個系列的博客去討論這個問題.
看我們在不同階段, 使用不同技術,如何處理相同的問題. 在不同方案之間橫向對比, 去深入了解
技術變遷以及背后的處理思路和邏輯的變化.
什么是回調呢? 這么問似乎有點多余, 每個寫過 javascript 的開發者, 或多或少都會接觸到回調. 回調的使用成本很低,
實現回調函數就像傳遞一般的參數變量一樣簡單.由于函數式編程極好的支持,以至于這項技術使用基本沒有障礙.我們隨手就能寫出一個回調
Ajax.get("http://xxxx", {}, (resp) => { // ..... })
但是呢,要真給回調下一個定義, 也還真不好回答.
我們不妨從一些側面去看看回調
回調是一種處理特定問題的模式, 伴隨著函數式編程而生. 函數式編程中很重要的技術之一就是回調函數
當一個函數作為主調函數的參數時, 它往往會在特定的時間和場景(上下文)中執行.
執行過程中,回調函數選擇性接收函數內部的數據, 或者狀態(內存), 經過處理選擇性返回,或者改變狀態(hock).
callback 業務模型說這么多, 我們不如從代碼的角度去解決一個串行的異步模型.
為了說明問題, 我們將問題簡化成 A B C 三個異步(可能是 io, 網絡請求, 或者其他.為了方面描述, 我們采用 settimeout 來模擬), 這三個異步耗時不確定, 但是必須按照 A B C 的順序處理他們的返回結果.
處理這個問題, 我們基本上有兩種思路:
控制異步發出的順序, 在 a 返回之后再發 b 請求, 這樣將問題串行化(省市縣模型中經常需要省的返回值去請求省所對應的市).
同時發出異步請求,控制處理的順序.
方案一: 串行化請求// 模擬 ajax 函數 function ajax(url) { return function (cb) { setTimeout(function() { cb({ url }); }, Math.random() * 3000); } } // 初始化出三個請求 const A = ajax("/ofo/a"); const B = ajax("/ofo/b"); const C = ajax("/ofo/c"); // 控制請求順序 log("ajax A send..."); A(function (a) { log("ajax A receive..."); log("ajax B send..."); B(function (b) { log("ajax B receive..."); log("ajax C send..."); C(function (C) { log("ajax C receive..."); }); }) })
代碼很簡單, 大多是方案也是這么走的, 因為 A 的返回值可以作為 B 的參數.
但是相應的這個模式的總時間必定大于三個請求的時間之和.輸出如下:
ajax A send... ajax A receive... ajax B send... ajax B receive... ajax C send... ajax C receive...方案二: 自由請求,串行化處理
是相對不那么通用的方案, 但是處理沒有直接數據依賴的串行請求非常合適.
// 發送容器 const sender = []; // 稍作改造 function ajax(url, time) { return function(cb) { // 記錄發送順序, 必須有序 sender.push(url); setTimeout(function() { const data = { from: url, reso: "ok" }; // 將 data, 回調傳遞給一個處理函數 dealReceive({url, cb, data}); }, time); } } // 按照順序處理返回結果 // 返回結果容器 const receiver = {}; function dealReceive({url, cb, data}) { // 記錄返回結果.可以無序 receiver[url] = {cb, data}; for (var i = 0; i < sender.length; i++) { let operate = receiver[sender[i]]; if(typeof operate === "object") { operate.cb.call(null, operate.data); } else { return; } } } // 手動模擬出請求時間, A 最耗時.b 最快, 更好說明問題 const A = ajax("/ofo/a", 4000); const B = ajax("/ofo/b", 600); const C = ajax("/ofo/c", 2000); // 注意我們的調用方式 是沒有任何控制的 // A,B,C 依次發出. 還可以按照這個順序處理 A,B,C 的返回值 A(function (a) { log(a); }); B(function (b) { log(b); }); C(function (c) { log(c); });
輸出:
{"from":"/ofo/a","reso":"ok"} {"from":"/ofo/b","reso":"ok"} {"from":"/ofo/c","reso":"ok"}
這種方案總耗時基本上是耗時最長的 ajax 的耗時。
值得注意的是, A,B,C 的調用上沒有做任何控制. A 最耗時, 但是要最最先處理 A 的返回數據.
實現這一點的關鍵就在于我們 dealReceive 有個輪詢, 這個輪詢不是定時觸發的,而是每當請求回來時, 觸發輪詢. 整個過程輪詢 3 次.
基本上 callback 處理組合異步模型的思路說完了.串行是容易處理的一種模型, 如果出現 c 依賴 a,b 都正確返回的模型時, 基本上我們暴力一點就是轉化為串行關系. 盡管 a, b 沒有關系.
或者呢我們就在 a, b 的回調里做標志位. 和 dealReceive 類似.
單個異步不需要有太多處理, callback 的一些細節也不做討論. 主要討論是回調在實際場景中的處理問題方案
回調兩面性我們還是落入俗套的分析一下回調的優缺點.其實主要是缺點.
優點: 使用成本低, 處理簡單問題非常方便.能夠拿到主調函數內部的環境.等等.
大多數人認為的缺點:
回調很 low: 可能是因為, 實現回調函數就像傳遞一般的參數變量一樣簡單.由于函數式編程極好的支持,以至于這項技術使用基本沒有障礙.也沒有比較嚴格的模式要求.大家習以為常了.
回調地獄(代碼橫向發展): 其實這并不是回調的錯. 當我們遇到回調無底洞的時候,也無需驚慌,其實這根本不是什么問題, 因為同樣有協程和 monad 無底洞。因為如果你把任何一個抽象使用地足夠頻繁的話,都同樣會創造一個無底洞。
使用回調上的建議: 沒有使用障礙導致回調的濫用, 大部分問題都用了簡單的回調堆疊來解決. 實際上我們有很多基于回調的模式可以避免這些問題.比如: cps, cps 進一步轉化為 thunk.等等.
這樣看來, 回調沒有缺點, 是這樣么? 不是的. 回調有非常致命的機制上的缺點, 這個問題可能在 node 中爆發,除非自身改變,或者被吃掉。
所謂的機制就是:你可能在用回調處理復雜問題的時候,對自己能力產生懷疑,這些異步之間的關系是那么難以梳理清晰,而又難以寫出容易維護的代碼.
其實這都不是你的錯.
使用回調處理異步往往意味著,你舍棄了返回值,而使用回調接收異步操作結果. 而這正是用回調風格來編程會很困難的根本原因: 回調風格不返回任何值,所以難以組合[函數式編程中函數有良好的輸入和輸出是函數可以組合的根本]。
一個沒有返回值的函數執行的效果其實是利用它的副作用
一個沒有返回值和利用副作用的函數其實就是一個黑洞。
所以,使用回調風格來編程無法避免會是指令式的,它實際上是通過把一系列嚴重依賴于副作用的操作安排好執行順序,而不是通過函數的調用來把輸入輸出值對應好。如果你是通過回調組織程序執行流程, 而不是靠理順值的關系來解決問題的, 是很難編寫出正確的并行程序
這種問題也間接的導致了回調難于調試,定位問題和維護.
最終的結果就是: 你崩潰了
注:系列博客陸續推出,稍安勿躁。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100060.html
摘要:編寫異步代碼可能是一種不同的體驗,尤其是對異步控制流而言。回調函數的準則在編寫異步代碼時,要記住的第一個規則是在定義回調時不要濫用閉包。為回調創建命名函數,避免使用閉包,并將中間結果作為參數傳遞。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關注我的專欄,之后的博文將在專...
摘要:運行機制小程序啟動會有兩種情況,一種是冷啟動,一種是熱啟動。建議小程序在必要時使用監聽內存告警事件,進行必要的內存清理。 前言 以小程序為切入點,深入理解總結方方面面的知識點,做成系列文章,希望能得到大神的指點和幫助新人入門,承上啟下才是好程序猿由于是系列第一篇文章,緊跟著的是一大段廢話,只關心技術的可以跳過 轉眼半年又要過去了,意味著來新公司快半年了,離上次寫文章也半年了,渾渾噩噩...
摘要:異步問題回調地獄首先,我們來看下異步編程中最常見的一種問題,便是回調地獄。同時使用也是異步編程最基礎和核心的一種解決思路。基于,目前也被廣泛運用,其是異步編程的一種解決方案,比傳統的回調函數解決方案更合理和強大。 關于 微信公眾號:前端呼啦圈(Love-FED) 我的博客:勞卜的博客 知乎專欄:前端呼啦圈 前言 在實際編碼中,我們經常會遇到Javascript代碼異步執行的場景...
摘要:異步編程三座大山原型原型鏈作用域閉包同步異步。異步操作執行完畢后,再執行該回調函數,確保回調在異步操作之后執行。回調函數本身是我們約定俗成的一種叫法,我們定義它,但是并不會自己去執行它,它最終被其他人執行了。 JS異步編程 JS三座大山:原型原型鏈、作用域閉包、同步異步。之前有寫過自己對閉包的理解,今天來總結一下JS中的異步。 思考(案例來自stackoverflow): functi...
摘要:工作當中經常會用到,在此進行深入學習異步編程解決方案是異步編程的一種解決方案,比傳統的解決方案回調函數和事件更合理和更強大。所有源碼注釋見學習筆記 工作當中經常會用到Promise,在此進行深入學習 異步編程解決方案 Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了...
閱讀 1386·2019-08-30 12:54
閱讀 1870·2019-08-30 11:16
閱讀 1613·2019-08-30 10:50
閱讀 2448·2019-08-29 16:17
閱讀 1266·2019-08-26 12:17
閱讀 1378·2019-08-26 10:15
閱讀 2387·2019-08-23 18:38
閱讀 785·2019-08-23 17:50