##### 4 屬性選擇器 屬性選擇器是通過元素的屬性及屬性值來選擇元素的。下麵介紹屬性選擇器的用法。 1. 第一種用法 ``` 作用:選擇含有指定屬性的元素。 語法:[屬性名]{} ``` 示例如下: ```html 屬性選擇器 用戶名: 密 碼: 數據量: ``` 運行結果: ![image]( ...
這一部分首先我們考慮一下,如果我們是瀏覽器或者 Node 的開發者,我們該如何使用 JavaScript 引擎。
當拿到一段 JavaScript 代碼時,瀏覽器或者 Node 環境首先要做的就是;傳遞給 JavaScript 引擎,並且要求它去執行。
然而,執行 JavaScript 並非一錘子買賣,宿主環境當遇到一些事件時,會繼續把一段代碼傳遞給 JavaScript 引擎去執行,此外,我們可能還會提供 API 給 JavaScript 引擎,比如 setTimeout 這樣的 API,它會允許 JavaScript 在特定的時機執行。
所以,我們首先應該形成一個感性的認知:一個 JavaScript 引擎會常駐於記憶體中,它等待著我們(宿主)把 JavaScript 代碼或者函數傳遞給它執行。
在 ES3 和更早的版本中,JavaScript 本身還沒有非同步執行代碼的能力,這也就意味著,宿主環境傳遞給 JavaScript 引擎一段代碼,引擎就把代碼直接順次執行了,這個任務也就是宿主發起的任務。
但是,在 ES5 之後,JavaScript 引入了 Promise,這樣,不需要瀏覽器的安排,JavaScript 引擎本身也可以發起任務了。
由於我們這裡主要講 JavaScript 語言,那麼採納 JSC 引擎的術語,我們把宿主發起的任務稱為巨集觀任務,把 JavaScript 引擎發起的任務稱為微觀任務。
巨集觀和微觀任務
JavaScript 引擎等待宿主環境分配巨集觀任務,在操作系統中,通常等待的行為都是一個事件迴圈,所以在 Node 術語中,也會把這個部分稱為事件迴圈。
不過,術語本身並非我們需要重點討論的內容,我們在這裡把重點放在事件迴圈的原理上。在底層的 C/C++ 代碼中,這個事件迴圈是一個跑在獨立線程中的迴圈,我們用偽代碼來表示,大概是這樣的:
while(TRUE) { r = wait(); execute(r);}
我們可以看到,整個迴圈做的事情基本上就是反覆“等待 - 執行”。當然,實際的代碼中並沒有這麼簡單,還有要判斷迴圈是否結束、巨集觀任務隊列等邏輯,這裡為了方便你理解,我就把這些都省略掉了。
這裡每次的執行過程,其實都是一個巨集觀任務。我們可以大概理解:巨集觀任務的隊列就相當於事件迴圈。
在巨集觀任務中,JavaScript 的 Promise 還會產生非同步代碼,JavaScript 必須保證這些非同步代碼在一個巨集觀任務中完成,因此,每個巨集觀任務中又包含了一個微觀任務隊列:
有了巨集觀任務和微觀任務機制,我們就可以實現 JavaScript 引擎級和宿主級的任務了,例如:Promise 永遠在隊列尾部添加微觀任務。setTimeout 等宿主 API,則會添加巨集觀任務。
接下來,我們來詳細介紹一下 Promise。
Promise
Promise 是 JavaScript 語言提供的一種標準化的非同步管理方式,它的總體思想是,需要進行 io、等待或者其它非同步操作的函數,不返回真實結果,而返回一個“承諾”,函數的調用方可以在合適的時機,選擇等待這個承諾兌現(通過 Promise 的 then 方法的回調)。
Promise 的基本用法示例如下:
function sleep(duration) { return new Promise(function(resolve, reject) { setTimeout(resolve,duration); })}sleep(1000).then( ()=> console.log("finished"));
這段代碼定義了一個函數 sleep,它的作用是等候傳入參數指定的時長。
Promise 的 then 回調是一個非同步的執行過程,下麵我們就來研究一下 Promise 函數中的執行順序,我們來看一段代碼示例:
var r = new Promise(function(resolve, reject){ console.log("a"); resolve()});r.then(() => console.log("c"));console.log("b")
我們執行這段代碼後,註意輸出的順序是 a b c。在進入 console.log(“b”) 之前,毫無疑問 r 已經得到了 resolve,但是 Promise 的 resolve 始終是非同步操作,所以 c 無法出現在 b 之前。
接下來我們試試跟 setTimeout 混用的 Promise。
在這段代碼中,我設置了兩段互不相干的非同步操作:通過 setTimeout 執行 console.log(“d”),通過 Promise 執行 console.log(“c”)。
var r = new Promise(function(resolve, reject){ console.log("a"); resolve()});setTimeout(()=>console.log("d"), 0)r.then(() => console.log("c"));console.log("b")
我們發現,不論代碼順序如何,d 必定發生在 c 之後,因為 Promise 產生的是 JavaScript 引擎內部的微任務,而 setTimeout 是瀏覽器 API,它產生巨集任務。
為了理解微任務始終先於巨集任務,我們設計一個實驗:執行一個耗時 1 秒的 Promise。
setTimeout(()=>console.log("d"), 0)var r = new Promise(function(resolve, reject){ resolve()});r.then(() => { var begin = Date.now(); while(Date.now() - begin < 1000); console.log("c1") new Promise(function(resolve, reject){ resolve() }).then(() => console.log("c2"))});
這裡我們強制了 1 秒的執行耗時,這樣,我們可以確保任務 c2 是在 d 之後被添加到任務隊列。
我們可以看到,即使耗時一秒的 c1 執行完畢,再 enque 的 c2,仍然先於 d 執行了,這很好地解釋了微任務優先的原理。
通過一系列的實驗,我們可以總結一下如何分析非同步執行的順序:
-
首先我們分析有多少個巨集任務;
-
在每個巨集任務中,分析有多少個微任務;
-
根據調用次序,確定巨集任務中的微任務執行次序;
-
根據巨集任務的觸發規則和調用次序,確定巨集任務的執行次序;
-
確定整個順序。
我們再來看一個稍微複雜的例子:
function sleep(duration) { return new Promise(function(resolve, reject) { console.log("b"); setTimeout(resolve,duration); })}console.log("a");sleep(5000).then(()=>console.log("c"));
這是一段非常常用的封裝方法,利用 Promise 把 setTimeout 封裝成可以用於非同步的函數。
我們首先來看,setTimeout 把整個代碼分割成了 2 個巨集觀任務,這裡不論是 5 秒還是 0 秒,都是一樣的。
第一個巨集觀任務中,包含了先後同步執行的 console.log(“a”); 和 console.log(“b”);。
setTimeout 後,第二個巨集觀任務執行調用了 resolve,然後 then 中的代碼非同步得到執行,所以調用了 console.log(“c”),最終輸出的順序才是: a b c。
Promise 是 JavaScript 中的一個定義,但是實際編寫代碼時,我們可以發現,它似乎並不比回調的方式書寫更簡單,但是從 ES6 開始,我們有了 async/await,這個語法改進跟 Promise 配合,能夠有效地改善代碼結構。
新特性:async/await
async/await 是 ES2016 新加入的特性,它提供了用 for、if 等代碼結構來編寫非同步的方式。它的運行時基礎是 Promise,面對這種比較新的特性,我們先來看一下基本用法。
async 函數必定返回 Promise,我們把所有返回 Promise 的函數都可以認為是非同步函數。
async 函數是一種特殊語法,特征是在 function 關鍵字之前加上 async 關鍵字,這樣,就定義了一個 async 函數,我們可以在其中使用 await 來等待一個 Promise。
function sleep(duration) { return new Promise(function(resolve, reject) { setTimeout(resolve,duration); })}async function foo(){ console.log("a") await sleep(2000) console.log("b")}
這段代碼利用了我們之前定義的 sleep 函數。在非同步函數 foo 中,我們調用 sleep。
async 函數強大之處在於,它是可以嵌套的。我們在定義了一批原子操作的情況下,可以利用 async 函數組合出新的 async 函數。
function sleep(duration) { return new Promise(function(resolve, reject) { setTimeout(resolve,duration); })}async function foo(name){ await sleep(2000) console.log(name)}async function foo2(){ await foo("a"); await foo("b");}
這裡 foo2 用 await 調用了兩次非同步函數 foo,可以看到,如果我們把 sleep 這樣的非同步操作放入某一個框架或者庫中,使用者幾乎不需要瞭解 Promise 的概念即可進行非同步編程了。
此外,generator/iterator 也常常被跟非同步一起來講,我們必須說明 generator/iterator 並非非同步代碼,只是在缺少 async/await 的時候,一些框架(最著名的要數 co)使用這樣的特性來模擬 async/await。
但是 generator 並非被設計成實現非同步,所以有了 async/await 之後,generator/iterator 來模擬非同步的方法應該被廢棄。