摘要:前言在上一篇前端魔法堂異常不僅僅是中我們描述出一副異常及如何捕獲異常的畫像,但僅僅如此而已。調(diào)用方從右到左的順序?qū)?shù)壓入棧中,在被調(diào)用方執(zhí)行完成后,由被調(diào)用方負(fù)責(zé)清理棧中的參數(shù)也稱為棧平衡。
前言
?在上一篇《前端魔法堂——異常不僅僅是try/catch》中我們描述出一副異常及如何捕獲異常的畫像,但僅僅如此而已。試想一下,我們窮盡一切捕獲異常實例,然后僅僅為告訴用戶,運維和開發(fā)人員頁面報了一個哪個哪個類型的錯誤嗎?答案是否定的。我們的目的是收集剛剛足夠的現(xiàn)場證據(jù),好讓我們能馬上重現(xiàn)問題,快速修復(fù),提供更優(yōu)質(zhì)的用戶體驗。那么問題就落在“收集足夠的現(xiàn)場證據(jù)”,那么我們又需要哪些現(xiàn)場證據(jù)呢?那就是異常信息,調(diào)用棧和棧幀局部狀態(tài)。(異常信息我們已經(jīng)獲取了)
?本文將圍繞上調(diào)用棧和棧幀局部狀態(tài)敘述,準(zhǔn)開開車^_^
?本篇將敘述如下內(nèi)容:
什么是調(diào)用棧?
如何獲取調(diào)用棧?
什么是棧幀局部狀態(tài)?又如何獲取呢?
一.什么是調(diào)用棧??既然我們要獲取調(diào)用棧信息,那么起碼要弄清楚什么是調(diào)用棧吧!下面我們分別從兩個層次來理解~
印象派?倘若主要工作內(nèi)容為應(yīng)用開發(fā),那么我們對調(diào)用棧的印象如下就差不多了:
function funcA (a, b){ return a + b } function funcB (a){ let b = 3 return funcA(a, b) } function main(){ let a = 5 funcB(a) } main()
?那么每次調(diào)用函數(shù)時就會生成一個棧幀,并壓入調(diào)用棧,棧幀中存儲對應(yīng)函數(shù)的局部變量;當(dāng)該函數(shù)執(zhí)行完成后,其對應(yīng)的棧幀就會彈出調(diào)用棧。
?因此調(diào)用main()時,調(diào)用棧如下
----------------<--棧頂 |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
?調(diào)用funcB()時,調(diào)用棧如下
----------------<--棧頂 |function:funcB| |let b = 3 | |return funcA()| ---------------- |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
?調(diào)用funcA()時,調(diào)用棧如下
----------------<--棧頂 |function:funcA| |return a + b | ---------------- |function:funcB| |let b = 3 | |return funcA()| ---------------- |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
?funcA()執(zhí)行完成后,調(diào)用棧如下
----------------<--棧頂 |function:funcB| |let b = 3 | |return funcA()| ---------------- |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
?funcB()執(zhí)行完成后,調(diào)用棧如下
----------------<--棧頂 |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
?main()執(zhí)行完成后,調(diào)用棧如下
----------------<--棧頂 ----------------<--棧底
?現(xiàn)在我們對調(diào)用棧有了大概的印象了,但大家有沒有留意上面記錄"棧幀中存儲對應(yīng)函數(shù)的局部變量",棧幀中僅僅存儲對應(yīng)函數(shù)的局部變量,那么入?yún)⒛??難道會作為局部變量嗎?這個我們要從理論的層面才能得到解答呢。
理論派?這里我們要引入一個簡單的C程序,透過其對應(yīng)的匯編指令來講解了。我會盡我所能用通俗易懂的語言描述這一切的,若有錯誤請各位指正??!
前提知識Intel X86架構(gòu)中調(diào)用棧的棧底位于高位地址,而棧頂位于低位地址。(和印象派中示意圖的方向剛好相反)
調(diào)用棧涉及的寄存器有
ESP/RSP, 暫存棧頂?shù)刂?EBP/RBP, 暫存棧幀起始地址 EIP, 暫存下一個CPU指令的內(nèi)存地址,當(dāng)CPU執(zhí)行完當(dāng)前指令后,從EIP讀取下一條指令的內(nèi)存地址,然后繼續(xù)執(zhí)行
操作指令
PUSH,將ESP向低位地址移動操作數(shù)所需的空間,然后將操作數(shù)壓入調(diào)用棧中 POP ,從調(diào)用棧中讀取數(shù)據(jù)暫存到操作數(shù)指定的寄存器或內(nèi)存空間中,然后向高位地址移動操作數(shù)對應(yīng)的空間字節(jié)數(shù) MOV , ,數(shù)據(jù)傳送指令。用于將一個數(shù)據(jù)從源地址傳送到目標(biāo)地址,且不破壞源地址的內(nèi)容 ADD , ,兩數(shù)相加不帶進位,然后將結(jié)果保存到目標(biāo)地址上 RET,相當(dāng)于POP EIP。就是從堆棧中出棧,然后將值保存到EIP寄存器中 LEAVE,相當(dāng)于MOV EBP ESP,然后再POP EBP。就是將棧頂指向當(dāng)前棧幀地址,然后將調(diào)用者的棧幀地址暫存到EBP中
每個函數(shù)調(diào)用前匯編器都會加入以下前言(Prolog),用于保存棧幀和返回地址
push %rbp ;將調(diào)用者的棧幀指針壓入調(diào)用棧 mov %rsp,%rbp ;現(xiàn)在棧頂指向剛?cè)霔5腞BP內(nèi)容,要將其設(shè)置為棧幀的起始位置
?現(xiàn)在們結(jié)合實例來理解吧!
C語言
#includeint add(int a, int b){ return a + b; } int add2(int a){ int sum = add(0, a); return sum + 2; } void main(){ add2(2); }
然后執(zhí)行以下命令編譯帶調(diào)試信息的可執(zhí)行文件,和dump文件
$ gcc -g -o main main.c $ objdump -d main > main.dump
下面我們截取main、add2和add對應(yīng)的匯編指令來講解
main函數(shù)對應(yīng)的匯編指令
0x40050fpush %rbp 0x400510 mov %rsp,%rbp ;將2暫存到寄存器EDI中 0x400513 mov $0x2,%edi ;執(zhí)行call指令前,EIP寄存器已經(jīng)存儲下一條指令的地址0x40051d了 ;首先將EIP寄存器的值入棧,當(dāng)函數(shù)返回時用于恢復(fù)之前的執(zhí)行序列 ;然后才是執(zhí)行JUMP指令跳轉(zhuǎn)到add2函數(shù)中開始執(zhí)行其第一條指令 0x400518 callq 0x4004ea ;什么都不做 0x40051d nop ;設(shè)置RBP為指向main函數(shù)調(diào)用方的棧幀地址 0x40051e pop %rbp ;設(shè)置EIP指向main函數(shù)返回后將要執(zhí)行的指令的地址 0x40051f retq
下面是執(zhí)行add2函數(shù)第一條指令前的調(diào)用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數(shù)調(diào)用方的棧幀地址 <-- EBP +++++++++++++++++ 98 | 0x40051d | -- EIP的值,存放add2返回后將執(zhí)行的指令的地址 <-- ESP +++++++++++++++++ 低位地址
add2函數(shù)對應(yīng)的匯編指令
0x4004eapush %rbp 0x4004eb mov %rsp,%rbp 0x4004ee sub $0x18,%rsp ;棧頂向低位移動24個字節(jié),為后續(xù)操作預(yù)留堆棧空間 0x4004f2 mov %edi,-0x14(%rbp);從EDI寄存器中讀取參數(shù),并存放到堆??臻g中 0x4004f5 mov -0x14(%rbp),%eax;從堆??臻g中讀取參數(shù),放進EAX寄存器中 0x4004f8 mov %eax,%esi ;從EAX寄存器中讀取參數(shù),存放到ESI寄存器中 0x4004fa mov $0x0,%edi ;將0存放到EDI寄存器中 ;執(zhí)行call指令前,EIP寄存器已經(jīng)存儲下一條指令的地址0x400504了 ;首先將EIP寄存器的值入棧,當(dāng)函數(shù)返回時用于恢復(fù)之前的執(zhí)行序列 ;然后才是執(zhí)行JUMP指令跳轉(zhuǎn)到add函數(shù)中開始執(zhí)行其第一條指令 0x4004ff callq 0x4004d6 0x400504 mov %eax,-0x4(%rbp) ;讀取add的返回值(存儲在EAX寄存器中),存放到堆??臻g中 0x400507 mov -0x4(%rbp),%eax ;又將add的返回值存放到EAX寄存器中(這是有多無聊啊~~) 0x40050a add $0x2,%eax ;讀取EAX寄存器的值與2相加,結(jié)果存放到EAX寄存器中 0x40050d leaveq ;讓棧頂指針指向main函數(shù)的棧幀地址,然后讓EBP指向main函數(shù)的棧幀地址 0x40050e retq ;讓EIP指向add2返回后將執(zhí)行的指令的地址
下面是執(zhí)行完add2函數(shù)中mov %rsp,%rbp的調(diào)用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數(shù)調(diào)用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回后將執(zhí)行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數(shù)調(diào)用方(即main函數(shù))的棧幀地址<-- ESP,EBP +++++++++++++++++ 低位地址
下面是執(zhí)行add函數(shù)第一條指令前的調(diào)用??煺?/p>
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數(shù)調(diào)用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回后將執(zhí)行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數(shù)調(diào)用方(即main函數(shù))的棧幀地址<-- EBP +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執(zhí)行結(jié)果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | +++++++++++++++++ 72 | 0x400504 | -- EIP的值,存放add返回后將執(zhí)行的指令的地址 <-- ESP +++++++++++++++++ 低位地址
add函數(shù)對應(yīng)的匯編指令
0x4004d6push %rbp 0x4004d7 mov %rsp,%rbp 0x4004da mov %edi,-0x4(%rbp) 0x4004dd mov %esi,-0x8(%rbp) 0x4004e0 mov -0x4(%rbp),%edx 0x4004e3 mov -0x8(%rbp),%eax 0x4004e6 add %edx,%eax 0x4004e8 pop %rbp 0x4004e9 retq
下面是add函數(shù)執(zhí)行完mov %rsp,%rbp的調(diào)用??煺?/p>
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數(shù)調(diào)用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回后將執(zhí)行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數(shù)調(diào)用方(即main函數(shù))的棧幀地址 +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執(zhí)行結(jié)果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | +++++++++++++++++ 72 | 0x400504 | -- EIP的值,存放add返回后將執(zhí)行的指令的地址 +++++++++++++++++ 71 | 97 | -- 存放add函數(shù)調(diào)用方(即add函數(shù))的棧幀地址<-- EBP,ESP +++++++++++++++++ 低位地址
下面就是一系列彈出棧幀的過程了
當(dāng)add函數(shù)執(zhí)行完retq的調(diào)用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數(shù)調(diào)用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回后將執(zhí)行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數(shù)調(diào)用方(即main函數(shù))的棧幀地址 <-- EBP +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執(zhí)行結(jié)果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | <-- ESP +++++++++++++++++ 低位地址
然后就不斷彈出棧幀了~~~
?從上面看到函數(shù)入?yún)⑹窍却鎯Φ郊拇嫫髦?,然后在函?shù)體內(nèi)讀取到棧幀所在空間中(局部變量、臨時變量)。那么從調(diào)用棧中我們能獲取函數(shù)的調(diào)用流和入?yún)⑿畔?,從而恢?fù)案發(fā)現(xiàn)場^_^
?其實函數(shù)入?yún)⒌膫鬟f方式不止上述這種,還有以下3種
cdecl調(diào)用約定
?調(diào)用方從右到左的順序?qū)?shù)壓入棧中,在被調(diào)用方執(zhí)行完成后,由調(diào)用方負(fù)責(zé)清理棧中的參數(shù)(也稱為棧平衡)。
stdcall調(diào)用約定
?巨硬自稱的一種調(diào)用約定,并不是實際上的標(biāo)準(zhǔn)調(diào)用約定。調(diào)用方從右到左的順序?qū)?shù)壓入棧中,在被調(diào)用方執(zhí)行完成后,由被調(diào)用方負(fù)責(zé)清理棧中的參數(shù)(也稱為棧平衡)。
fastcall調(diào)用約定
?是stdcall的變體,調(diào)用方從右到左的順序?qū)?shù)壓入棧中,最右邊的兩個參數(shù)則不壓入棧中,而是分別存儲在ECX和EDX寄存器中,在被調(diào)用方執(zhí)行完成后,由被調(diào)用方負(fù)責(zé)清理棧中的參數(shù)(也稱為棧平衡)。
?但不管哪種,最終還是會在函數(shù)體內(nèi)讀取到當(dāng)前棧幀空間中。
二. 如何獲取調(diào)用棧??上面寫的這么多,可是我們現(xiàn)在寫的是JavaScript哦,那到底怎么才能讀取調(diào)用棧的信息呢?
拋個異??纯?/b>?IE10+的Error實例中包含一個stack屬性
示例
function add(a, b){ let sum = a + b throw Error("Capture Call Stack!") return sum } function add2(a){ return 2 + add(0, a) } function main(){ add2(2) } try{ main() } catch (e){ console.log(e.stack) }
Chrome回顯
Error: Capture Call Stack! at add (index.html:16) at add2 (index.html:21) at main (index.html:25) at index.html:29
FireFox回顯
add@file:///home/john/index.html:16:9 add2@file:///home/john/index.html:21:14 main@file:///home/john/index.html:25:3 @file:///home/john/index.html:29:3V8的Error.captureStackTrace函數(shù)
?V8引擎向JavaScript提供了其Stack Trace API中的captureStackTrace函數(shù),用于獲取調(diào)用Error.captureStackTrace時的調(diào)用??煺铡:瘮?shù)簽名如下
@static @method captureStackTrace(targetObject, constructorOpt) @param {Object} targetObject - 為targetObject添加.stack屬性,該屬性保存調(diào)用Error.captureStackTrace時的調(diào)用??煺?@param {Function} constructorOpt= - 調(diào)用??煺詹粩嘧鞒鰲2僮?,直到constructorOpt所指向的函數(shù)剛好出棧為止,然后保存到targetObject的stack屬性中 @return {undefined}
示例
function add(a, b){ let sum = a + b let targetObj = {} Error.captureStackTrace(targetObj) console.log(targetObj.stack) Error.captureStackTrace(targetObj, add) console.log(targetObj.stack) return sum } function add2(a){ return 2 + add(0, a) } function main(){ add2(2) } main()
Chrome回顯
Error at add (index.html:18) at add2 (index.html:28) at main (index.html:32) at index.html:35 Error at add2 (index.html:28) at main (index.html:32) at index.html:35控制臺的console.trace函數(shù)
?還有最后一招console.trace,不過實際用處不大
示例
function add(a, b){ let sum = a + b console.trace() return sum } function add2(a){ return 2 + add(0, a) } function main(){ add2(2) } main()
Chrome回顯
add @ index.html:16 add2 @ index.html:22 main @ index.html:26 (anonymous) @ index.html:29
?上述三種方式(實際就兩種可用啦)都只能獲取函數(shù)調(diào)用流,函數(shù)入?yún)?、局部變量等信息全都灰飛煙滅了?上面不是說好這些信息調(diào)用棧都有嘛,干嘛不給我呢?其實想想都知道調(diào)用棧中有這么多信息,其實我們只需一小部分,全盤托出并不是什么好設(shè)計。其實我們只要再獲取棧幀局部狀態(tài)就好了。
三. 什么是棧幀局部狀態(tài)?又如何獲取呢??所謂棧幀局部狀態(tài)其實就是函數(shù)入?yún)⒑途植孔兞浚囅肴绻覀兊玫?b>add函數(shù)調(diào)用時的入?yún)⑹?b>a=0、b=2和sum=2,那么不就得到完整案發(fā)現(xiàn)場了嗎?那問題就是如何獲得了。要不我們做個Monkey Patch
自定義一個異常類來承載棧幀局部狀態(tài)
function StackTraceError(e, env){ if (this instanceof StackTraceError);else return new StackTraceError(e, env) this.e = e this.env = env } let proto = StackTraceError.prototype = Object.create(Error.prototype) proto.name = "StackTraceError" proto.message = "Internal error." proto.constructor = StackTraceError proto.valueOf = proto.toString = function(){ let curr = this, q = [], files = [] do { if (curr.stack){ let stack = String(curr.stack) let segs = stack.split(" ").map(seg => seg.trim()) files = segs.filter(seg => seg != "Error") } else{ q.unshift({name: curr.name, msg: curr.message, env: curr.env}) } } while (curr = curr.e) let frames = [] let c = files.length, i = 0 while (i < c){ let file = files[i] let e = q[i] let frame = { name: e && e.name, msg: e && e.msg, env: e && e.env, file: file } frames.push(JSON.stringify(frame)) i += 1 } return frames.join(" ") }
每個函數(shù)定義都通過try/catch捕獲棧幀局部狀態(tài)
function add(a, b){ try{ var sum = a + b throw Error() } catch(e){ throw StackTraceError(e, ["a:", a, "b", b, "sum", sum].join("::")) } return sum } function add2(a){ try{ return 2 + add(0, a) } catch(e){ throw StackTraceError(e, ["a", a].join("::")) } } function main(){ try{ add2(2) } catch(e){ throw StackTraceError(e, "") } } try{ main() } catch(e){ console.log(e+"") }
chrome下
{"name":"StackTraceError","msg":"Internal error.","env":"a::0::b::2::sum::2","file":"at add (file:///home/john/index.html:57:11)"} {"name":"StackTraceError","msg":"Internal error.","env":"a:;2","file":"at add2 (file:///home/john/index.html:67:16)"} {"name":"StackTraceError","msg":"Internal error.","env":"","file":"at main (file:///home/john/index.html:76:5)"} {"file":"at file:///home/john/index.html:84:3"}
?上面這種做法有三個問題
V8引擎不會對包含try/catch的函數(shù)進行優(yōu)化,如果每個函數(shù)都包含try/catch那會嚴(yán)重影響執(zhí)行效率。
這種方式顯然不能讓每個開發(fā)人員手寫,必須通過預(yù)編譯器來靜態(tài)織入,開發(fā)難度有點大哦。
像sum這種臨時變量其實并不用記錄,因為它可以被運算出來,只要記錄a和b即可。
?假如我們寫的全是純函數(shù)(就是相同入?yún)⒈囟ǖ玫较嗤姆祷刂?,函?shù)內(nèi)部不依賴外部狀態(tài),如加法一樣,1+1永遠等于2),那么我們只需捕獲入口/公用函數(shù)的入?yún)⒓纯苫謴?fù)整個案發(fā)現(xiàn)場了。
function add(a, b){ var sum = a + b throw Error() return sum } function add2(a){ try{ return 2 + add(0, a) } catch(e){ throw {error:e, env:["a:", a].join("::")}) } } function main(){ add2(2) } try{ main() } catch(e){ console.log(e+"") }
?然后我們就可以拿著報錯信息從add2逐步調(diào)試到add中了。假如用ClojureScript我們還可以定義個macro簡化一下
;; 私有函數(shù) (defn- add [a b] (let [sum (+ a b)] (throw (Error.)) sum)) ;; 入口/公用函數(shù) (defn-pub add2 [a] (+ 2 (add 0 a))) (defn main [] (add2 2)) (try (main) (catch e (println e)))
defn-pub macro的定義
(defmacro defn-pub [name args & body] (let [e (gensym) arg-names (mapv str args)] `(def ~name (fn ~args (try ~@body (catch js/Object ~e (throw (clj->js {:e ~e, :env (zipmap ~arg-names ~args)}))))))))總結(jié)
?寫到這里其實也沒有一個很好的方式去捕獲案發(fā)現(xiàn)場證據(jù),在入口/公用函數(shù)中加入try/catch是我現(xiàn)階段能想到比較可行的方式,請各位多多指點。
尊重原創(chuàng),轉(zhuǎn)載請注明轉(zhuǎn)自:http://www.cnblogs.com/fsjohn... ^_^肥仔John
http://www.cnblogs.com/exiaha...
http://blog.csdn.net/qiu26584...
http://lucasfcosta.com/2017/0...
http://blog.shaochuancs.com/a...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/89298.html
摘要:我打算分成前端魔法堂異常不僅僅是和前端魔法堂調(diào)用棧,異常實例中的寶藏兩篇分別敘述內(nèi)置自定義異常類,捕獲運行時異常語法異常網(wǎng)絡(luò)請求異常事件,什么是調(diào)用棧和如何獲取調(diào)用棧的相關(guān)信息。 前言 ?編程時我們往往拿到的是業(yè)務(wù)流程正確的業(yè)務(wù)說明文檔或規(guī)范,但實際開發(fā)中卻布滿荊棘和例外情況,而這些例外中包含業(yè)務(wù)用例的例外,也包含技術(shù)上的例外。對于業(yè)務(wù)用例的例外我們別無它法,必須要求實施人員與用戶共同...
摘要:前端日報精選淺談前端和移動端的事件機制字符串轉(zhuǎn)數(shù)字陷阱前端魔法堂調(diào)用棧,異常實例中的寶藏一份完美的前端清單專為現(xiàn)代網(wǎng)站和極致的開發(fā)者打造居中辦法學(xué)習(xí)筆記中文開發(fā)如何在里面優(yōu)雅的解決跨域,路由沖突問題超詳細(xì)前端學(xué)習(xí)譯響應(yīng)式腦電波如何使 2017-10-26 前端日報 精選 淺談前端和移動端的事件機制JavaScript 字符串轉(zhuǎn)數(shù)字:陷阱前端魔法堂——調(diào)用棧,異常實例中的寶藏一份完美的前...
摘要:一前言圖片上傳是一個普通不過的功能,而圖片預(yù)覽就是就是上傳功能中必不可少的子功能了。偶然從上找到純前端圖片預(yù)覽的相關(guān)資料,經(jīng)過整理后記錄下來以便日后查閱。類型為,表示在讀取文件時發(fā)生的錯誤,只讀。 一、前言 圖片上傳是一個普通不過的功能,而圖片預(yù)覽就是就是上傳功能中必不可少的子功能了。在這之前,我曾經(jīng)通過訂閱input[type=file]元素的onchange事件,一旦更改路徑...
摘要:五的子類對象會返回一個集合對象,集合內(nèi)存儲類型的元素。七的子類初看很有可能以為集合元素就是單選表單元素,其實可以存儲任意類型的表單元素。八的子類開始,將返回子類的對象,其行為特征和一致。但在前,我們應(yīng)該先了解清楚的類型的特征。 一、前言 大家先看看下面的js,猜猜結(jié)果會怎樣吧! 可選答案: ①. 獲取id屬性值為id的節(jié)點元素 ②...
摘要:而同步和異步則是描述另一個方面。異步將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間的操作由系統(tǒng)自動處理,然后通知應(yīng)用程序直接使用數(shù)據(jù)即可。 前言 ?上周5在公司作了關(guān)于JS異步編程模型的技術(shù)分享,可能是內(nèi)容太干的緣故吧,最后從大家的表情看出這條粉腸到底在說啥?的結(jié)果:(下面是PPT的講義,具體的PPT和示例代碼在https://github.com/fsjohnhuan...上,有興趣就上去看看吧! ...
閱讀 691·2021-11-23 09:51
閱讀 3434·2021-10-11 10:58
閱讀 15672·2021-09-29 09:47
閱讀 3624·2021-09-01 11:42
閱讀 1327·2019-08-29 16:43
閱讀 1859·2019-08-29 15:37
閱讀 2150·2019-08-29 12:56
閱讀 1762·2019-08-28 18:21