<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
單執行緒模型指的是,JavaScript 只在一個執行緒上執行。也就是說,JavaScript 同時只能執行一個任務,其他任務都必須在後面排隊等待。注意,JavaScript 只在一個執行緒上執行,不代表 JavaScript 引擎只有一個執行緒。事實上,JavaScript 引擎有多個執行緒,單個指令碼只能在一個執行緒上執行(稱為主執行緒),其他執行緒都是在後臺配合。
JavaScript 之所以採用單執行緒,而不是多執行緒,跟歷史有關係。JavaScript 從誕生起就是單執行緒,原因是不想讓瀏覽器變得太複雜,因為多執行緒需要共用資源、且有可能修改彼此的執行結果,對於一種網頁尾本語言來說,這就太複雜了。
如果 JavaScript 同時有兩個執行緒,一個執行緒在網頁 DOM 節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?是不是還要有鎖機制?
所以,為了避免複雜性,JavaScript 一開始就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。
程式裡面所有的任務,可以分成兩類:同步任務(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)”。
把f2
寫成f1
的回撥函數。
function f1(callback) { // ... callback(); } function f2() { // ... } f1(f2);
回撥函數的優點是簡單、容易理解和實現,缺點是不利於程式碼的閱讀和維護,各個部分之間高度耦合(coupling),使得程式結構混亂、流程難以追蹤(尤其是多個回撥函數巢狀的情況),而且每個任務只能指定一個回撥函數。
f1.on('done', f2); function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); }
f1.trigger('done')
表示,執行完成後,立即觸發done
事件,從而開始執行f2
這種方法的優點是比較容易理解,可以繫結多個事件,每個事件可以指定多個回撥函數,而且可以“去耦合”(decoupling),有利於實現模組化。缺點是整個程式都要變成事件驅動型,執行流程會變得很不清晰。閱讀程式碼的時候,很難看出主流程。
事件完全可以理解成“訊號”,如果存在一個“訊號中心”,某個任務執行完成,就向訊號中心“釋出”(publish)一個訊號,其他任務可以向訊號中心“訂閱”(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。這就叫做“釋出/訂閱模式”(publish-subscribe pattern),又稱“觀察者模式”(observer pattern)。
f2
向訊號中心jQuery
訂閱done
訊號。
jQuery.subscribe('done', f2); function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
上面程式碼中,jQuery.publish('done')
的意思是,f1
執行完成後,向訊號中心jQuery
釋出done
訊號,從而引發f2
的執行。
f2
完成執行後,可以取消訂閱(unsubscribe)。
jQuery.unsubscribe('done', f2);
這種方法的性質與“事件監聽”類似,但是明顯優於後者。因為可以通過檢視“訊息中心”,瞭解存在多少訊號、每個訊號有多少訂閱者,從而監控程式的執行。
Promise 物件通過自身的狀態,來控制非同步操作。Promise 範例具有三種狀態。
上面三種狀態裡面,fulfilled
和rejected
合在一起稱為resolved
(已定型)。
這三種的狀態的變化途徑只有兩種。
一旦狀態發生變化,就凝固了,不會再有新的狀態變化。這也是 Promise 這個名字的由來,它的英語意思是“承諾”,一旦承諾成效,就不得再改變了。這也意味著,Promise 範例的狀態變化只可能發生一次。
因此,Promise 的最終結果只有兩種。
JavaScript 提供原生的Promise
建構函式,用來生成 Promise 範例。
var promise = new Promise(function (resolve, reject) { // ... if (/* 非同步操作成功 */){ resolve(value); } else { /* 非同步操作失敗 */ reject(new Error()); } });
上面程式碼中,Promise
建構函式接受一個函數作為引數,該函數的兩個引數分別是resolve
和reject
。它們是兩個函數,由 JavaScript 引擎提供,不用自己實現。
resolve
函數的作用是,將Promise
範例的狀態從“未完成”變為“成功”(即從pending
變為fulfilled
),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去。reject
函數的作用是,將Promise
範例的狀態從“未完成”變為“失敗”(即從pending
變為rejected
),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
下面是一個例子。
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100)
上面程式碼中,timeout(100)
返回一個 Promise 範例。100毫秒以後,該範例的狀態會變為fulfilled
。
Promise 的用法,簡單說就是一句話:使用then
方法新增回撥函數。但是,不同的寫法有一些細微的差別,請看下面四種寫法,它們的差別在哪裡?
// 寫法一 f1().then(function () { return f2(); }); // 寫法二 f1().then(function () { f2(); }); // 寫法三 f1().then(f2()); // 寫法四 f1().then(f2);
為了便於講解,下面這四種寫法都再用then
方法接一個回撥函數f3
。寫法一的f3
回撥函數的引數,是f2
函數的執行結果。
f1().then(function () { return f2(); }).then(f3);
寫法二的f3
回撥函數的引數是undefined
。
f1().then(function () { f2(); return; }).then(f3);
寫法三的f3
回撥函數的引數,是f2
函數返回的函數的執行結果。
f1().then(f2()) .then(f3);
寫法四與寫法一隻有一個差別,那就是f2
會接收到f1()
返回的結果。
f1().then(f2) .then(f3);
優點:讓回撥函數變成了規範的鏈式寫法,程式流程可以看得很清楚。它有一整套介面,可以實現許多強大的功能,比如同時執行多個非同步操作,等到它們的狀態都改變以後,再執行一個回撥函數;再比如,為多個回撥函數中丟擲的錯誤,統一指定處理方法等等。
而且,Promise 還有一個傳統寫法沒有的好處:它的狀態一旦改變,無論何時查詢,都能得到這個狀態。這意味著,無論何時為 Promise 範例新增回撥函數,該函數都能正確執行。所以,你不用擔心是否錯過了某個事件或訊號。如果是傳統寫法,通過監聽事件來執行回撥函數,一旦錯過了事件,再新增回撥函數是不會執行的。
缺點:編寫的難度比傳統寫法高,而且閱讀程式碼也不是一眼可以看懂。你只會看到一堆then
,必須自己在then
的回撥函數裡面理清邏輯。
Promise 的回撥函數屬於非同步任務,會在同步任務之後執行。
new Promise(function (resolve, reject) { resolve(1); }).then(console.log); console.log(2); // 2 // 1
上面程式碼會先輸出2,再輸出1。因為console.log(2)
是同步任務,而then
的回撥函數屬於非同步任務,一定晚於同步任務執行。
但是,Promise 的回撥函數不是正常的非同步任務,而是微任務(microtask)。它們的區別在於,正常任務追加到下一輪事件迴圈,微任務追加到本輪事件迴圈。這意味著,微任務的執行時間一定早於正常任務。
setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 // 2 // 1
上面程式碼的輸出結果是321
。這說明then
的回撥函數的執行時間,早於setTimeout(fn, 0)
。因為then
是本輪事件迴圈執行,setTimeout(fn, 0)
在下一輪事件迴圈開始時執行。
到此這篇關於Promise非同步程式設計模式的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45