瀏覽器內核機制、JS事件循環(huán)、Promise、Task、事件冒泡、事件委托詳解

前言:
我在學習瀏覽器的Event Loop時看了大量的文章,那些文章都寫的很好,但是往往是每篇文章有那么幾個關鍵的點,很多篇文章湊在一起綜合來看,才可以對這些概念有較為深入的理解,于是,我在看了大量文章之后,想要寫這么一篇作文分享出來,希望可以對走在進步之路上的同學有所幫助。


一、綜述:

JavaScript是一門非阻塞單線程腳本語言。但是你又聽過“瀏覽器”、“多核”,可以“多線程處理事務”等等之類的語言。在寫這篇作文之前我也是比較混沌的。所以在瀏覽了大量文章后,有了一定的概念,下面是我的理解:
?? ?? 瀏覽器是多進程的(看好:是進程喲~),每打開一個Tab頁,就相當于創(chuàng)建了一個獨立的瀏覽器進程,簡單的理解為渲染進程(Render); 而Render進程包括:

  • GUI渲染線程:負責渲染瀏覽器界面的工人,與JS引擎線程互斥。
  • JS引擎線程:負責解釋JS代碼的工人,與GUI渲染線程互斥
  • 事件觸發(fā)線程: 一個會添加任務,發(fā)布任務的流水線
  • 定時觸發(fā)器線程:傳說中的setInterval與setTimeout所在線程
  • 異步http請求線程:處理網絡請求,管理請求任務狀態(tài)的工人

他們之間的關系用車間的例子來形象說明:JS引擎線程、GUI渲染線程、定時觸發(fā)器線程、異步http請求線程相當于4個不同工種的工人站在同一個位置領取流水線(事件觸發(fā)線程)的任務,領取到任務后各自進行處理。如果執(zhí)行過程中,發(fā)現(xiàn)有需要其他工人配合的任務,將任務添加到流水線(任務隊列)中,直到流水線上任務做完。

需要注意的是:這幾條線程是并列的同級關系,真正進行的調度的是Render進程在5個進程之間進行來回調度,而JS引擎處理JS代碼是我們開發(fā)人員主要的關注的,所以會產生JS引擎是主線程的說法,并且Javascript引擎是 \color{red}{單線程機制}。
(若javascript的運行存在兩個線程,彼此操作了同一個資源,這樣會造成同步問題,修改到底以誰為標準。所以,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變)


二、瀏覽器多核

對于普通的前端操作來說,最終要的是什么呢?答案是渲染進程??梢赃@樣理解,頁面的渲染,JS的執(zhí)行,事件的循環(huán),都在這個進程內進行。接下來重點分析這個進程;請牢記,瀏覽器的渲染進程是多線程的( 終于到了線程這個概念了,好親切)。那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):


1.GUI渲染線程:

負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,布局和繪制等。

  • 當界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時,該線程就會執(zhí)行
  • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執(zhí)行時GUI線程會被掛起(相當于被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執(zhí)行。

2.JS引擎線程

  • 也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎)
  • JS引擎線程負責解析Javascript腳本,運行代碼。
  • JS引擎一直等待著任務隊列中任務的到來,然后加以處理,一個Tab頁(renderer進程)中無論什么時候都只有一個JS線程在運行JS程序
  • 同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。

3.事件觸發(fā)線程

  • 歸屬于瀏覽器而不是JS引擎,用來控制事件循環(huán)(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)
  • 當JS引擎執(zhí)行代碼塊如setTimeOut時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
  • 當對應的事件符合觸發(fā)條件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
  • 注意,由于JS的單線程關系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執(zhí)行)

4.定時觸發(fā)器線程

  • 傳說中的setInterval與setTimeout所在線程
  • 瀏覽器定時計數(shù)器并不是由JavaScript引擎計數(shù)的,(因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會影響記計時的準確)
  • 因此通過單獨線程來計時并觸發(fā)定時(計時完畢后,添加到事件隊列中,等待JS引擎空閑后執(zhí)行)
  • 注意,W3C在HTML標準中規(guī)定,規(guī)定要求\color{red}{setTimeout中低于4ms的時間間隔算為4ms}。

5.異步http請求線程

  • 在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求
  • 將檢測到狀態(tài)變更時,如果設置有回調函數(shù),異步線程就產生狀態(tài)變更事件,將這個回調再放入事件隊列中。再由JavaScript引擎執(zhí)行。

三、事件循環(huán)

事件循環(huán)(Event Loop) 是讓 JavaScript 做到既是單線程,又絕對不會阻塞的核心機制,也是 JavaScript 并發(fā)模型(Concurrency Model )的基礎,是用來協(xié)調各種事件、用戶交互、腳本執(zhí)行、UI 渲染、網絡請求等的一種機制。

說的更簡單一點:Event Loop 只不過是實現(xiàn)異步的一種機制而已。

Event Loop 分為兩種,一種存在于 Browsing Context] 中,還有一種在 Worker中。

  1. Browsing Context 是指一種用來將 Document]展現(xiàn)給用戶的環(huán)境。例如瀏覽器中的 tab,window 或 iframe 等,通常都包含 Browsing Context。
  2. Worker 是指一種獨立于 UI 腳本,可在后臺執(zhí)行腳本的 API。常用來在后臺處理一些計算密集型的任務。

本章節(jié)重點介紹的是 Browsing Context 中的 Event Loop,相比 Worker 中的 Event Loop,它也更加復雜一些。

另外,還需要注意的是:Event Loop 并不是在 ECMAScript 標準中定義的,而是在 HTML 標準中定義的:

To coordinate events, user interaction, scripts, rendering, networking, and so forth...

在 JavaScript Engine 中(以 V8 為例),只是實現(xiàn)了 ECMAScript 標準,而并不關心什么 Event Loop。也就是說 Event Loop 是屬于 JavaScript Runtime 的,是由宿主環(huán)境提供的(比如瀏覽器)。所以千萬不要搞錯了。

Event Loop 中的任務隊列

在執(zhí)行和協(xié)調各種任務時,Event Loop 會維護自己的任務隊列。任務隊列又分為 Task QueueMicrotask Queue 兩種。

實際上,稱任務隊列事件隊列(Event Queue)可能會更容易理解。所謂的事件驅動(Event-driven),就是將一切抽象為事件(Event),比如 AJAX 完成、鼠標點擊、I/O 操作等等,都是一個個的事件,而 Event Loop 就是一個事件循環(huán)的過程。

不過本文還是以 HTML 標準中的叫法作為參考。

1. Task Queue

一個 Event Loop 會有一個或多個 Task Queue,這是一個先進先出(FIFO)的有序列表,存放著來自不同 Task Source(任務源)的 Task。

關于 Task,常有人稱它為 Marcotask,但其實 HTML 標準中并沒有這種說法。

在 HTML 標準中,定義了幾種常見的 Task Source:

  1. DOM manipulation(DOM 操作);
  2. User interaction(用戶交互);
  3. Networking(網絡請求);
  4. History traversal(History API操作)。

Task Source 的定義非常的寬泛,常見的鼠標、鍵盤事件,AJAX,數(shù)據庫操作(例如 IndexedDB,以及定時器相關的 setTimeout、setInterval 等等都屬于 Task Source,所有來自這些 Task Source 的 Task 都會被放到對應的 Task Queue 中等待處理。

對于 Task、Task Queue 和 Task Source,有如下規(guī)定:

  1. 來自相同 Task Source 的 Task,必須放在同一個 Task Queue 中;
  2. 來自不同 Task Source 的 Task,可以放在不同的 Task Queue 中;
  3. 同一個 Task Queue 內的 Task 是按順序執(zhí)行的;
  4. 但對于不同的 Task Queue(Task Source),瀏覽器會進行調度,允許優(yōu)先執(zhí)行來自特定 Task Source 的 Task。

例如,鼠標、鍵盤事件和網絡請求都有各自的 Task Queue,當兩者同時存時,瀏覽器可以優(yōu)先從用戶交互相關的 Task Queue 中挑選 Task 并執(zhí)行,比如這里的鼠標、鍵盤事件,從而保證流暢的用戶體驗。

2. Microtask Queue

Microtask Queue 與 Task Queue 類似,也是一個有序列表。不同之處在于,一個 Event Loop 只有一個 Microtask Queue。

在 HTML 標準中,并沒有明確規(guī)定 Microtask Source,通常認為有以下幾種:

  • Promise

在 [Promises/A+ Note 3.1] 中提到了 then、onFulfilled、onRejected 的實現(xiàn)方法,但 Promise 本身屬于平臺代碼,由具體實現(xiàn)來決定是否使用 Microtask,因此在不同瀏覽器上可能會出現(xiàn)執(zhí)行順序不一致的問題。不過好在目前的共識是用 Microtask 來實現(xiàn)事件隊列。

  • MutationObserver
  • Object.observe (已廢棄)

這里要特別提一下:有很多文章把 Node.js 的 process.nextTick 和 Microtask 混為一談,事實上雖然兩者層級(運行時機)非常接近,但并不是同一個東西。process.nextTick 是 Node.js 自身定義實現(xiàn)的一種機制,有自己的 nextTickQueue,與 HTML 標準中的 Microtask 不是一回事。在 Node.js 中,process.nextTick 會先于 Microtask Queue 被執(zhí)行。

JavaScript Runtime 的運行機制

了解了 Event Loop 和隊列的基本概念后,就可以從相對宏觀的角度先了解一下 JavaScript Runtime 的運行機制了,簡化后的步驟如下:

1. 主線程不斷循環(huán);

2. 對于同步任務,創(chuàng)建執(zhí)行上下文 ,按順序進入執(zhí)行棧 ;

3. 對于異步任務

  • 與步驟 2 相同,同步執(zhí)行這段代碼;
  • 將相應的 Task(或 Microtask)添加到 Event Loop 的任務隊列;
  • 由其他線程來執(zhí)行具體的異步操作。

其他線程是指:盡管 JavaScript 是單線程的,但瀏覽器內核是多線程的,它會將 GUI 渲染、定時器觸發(fā)、HTTP 請求等工作交給專門的線程來處理。

另外,在 Node.js 中,異步操作會優(yōu)先由 OS 或第三方系統(tǒng)提供的異步接口來執(zhí)行,然后才由線程池處理。

4. 當主線程執(zhí)行完當前執(zhí)行棧中的所有任務,就會去讀取 Event Loop 的任務隊列,取出并執(zhí)行任務;

5. 重復以上步驟。

還是拿 setTimeout 舉個例子:

主線程同步執(zhí)行這個 setTimeout 函數(shù)本身。
將負責執(zhí)行這個 setTimeout 的回調函數(shù)的 Task 添加到 Task Queue。
定時器開始工作(實際上是靠 Event Loop 不斷循環(huán)檢查系統(tǒng)時間來判斷是否已經到達指定的時間點)。
主線程繼續(xù)執(zhí)行其他任務。
當執(zhí)行棧為空,且定時器觸發(fā)時,主線程取出 Task 并執(zhí)行相應的回調函數(shù)。
很明顯,執(zhí)行 setTimeout 不會導致阻塞。當然,如果主線程很忙的話(執(zhí)行棧一直非空),就會出現(xiàn)明明時間已經到了,卻也不執(zhí)行回調的現(xiàn)象,所以類似 setTimeout 這樣的回調函數(shù)都是沒法保證執(zhí)行時機的。

Event Loop 處理模型

前面簡單介紹了 JavaScript Runtime 的整個運行流程,而 Event Loop 作為其中的重要一環(huán),它的每一次循環(huán)過程也相當復雜,因此將它單獨拿出來介紹。下面我會盡量保持 HTML 標準中對處理模型(Processing Model)的定義,并盡量簡化,步驟如下(3 步):

  1. 執(zhí)行 Task:從 Task Queue 中取出最老的一個 Task 并執(zhí)行;如果沒有 Task,直接跳過。
  2. 執(zhí)行 Microtasks:遍歷 Microtask Queue 并執(zhí)行所有 Microtask。
  3. 進入 Update the rendering(更新渲染)階段
    1. 設置 Performance API 中 now() 的返回值。Performance API屬于 W3C High Resolution Time API的一部分,用于前端性能測量,能夠細粒度的測量首次渲染、首次渲染內容等的各項繪制指標,是前端性能追蹤的重要技術手段,感興趣的同學可關注。
    1. 遍歷本次 Event Loop 相關的 Documents,執(zhí)行更新渲染。在迭代執(zhí)行過程中,瀏覽器會根據各種因素判斷是否要跳過本次更新。
    1. 當瀏覽器確認繼續(xù)本次更新后,處理更新渲染相關工作:
      1. 觸發(fā)各種事件:Resize、Scroll、Media Queries、CSS Animations、Fullscreen API。
      1. 執(zhí)行 animation frame callbacks,window.requestAnimationFrame就在這里。
      1. 更新 intersection observations,也就是 Intersection Observer API(可用于圖片懶加載)。更新渲染和 UI,將最終結果提交到界面上。

至此,Event Loop 的一次循環(huán)結束。。。

Microtask Queue 執(zhí)行時機

在上面介紹的 Event Loop 處理模型中,Microtask Queue 會在第 2 步時被執(zhí)行。實際上按照 HTML 標準,在以下幾種情況中 Microtask Queue 都會被執(zhí)行:

  1. 某個 Task 執(zhí)行完畢時(即上述情況)。
  2. 進入腳本執(zhí)行(Calling scripts)的清理階段(Clean up after running script)時。
  3. 創(chuàng)建和插入節(jié)點時。
  4. 解析 XML 文檔時。

同時,在當前 Event Loop 輪次中動態(tài)添加進來的 Microtasks,也會在本次 Event Loop 循環(huán)中全部執(zhí)行完(上圖其實已經畫出來了)。

最后一定要注意的是,執(zhí)行 Microtasks 是有前提的:當前執(zhí)行棧必須為空,且沒有正在運行的執(zhí)行上下文。否則,就必須等到執(zhí)行棧中的任務全部執(zhí)行完畢,才能開始執(zhí)行 Microtasks。

也就是說:JavaScript 會確保當前執(zhí)行的同步代碼不會被 Microtasks 打斷。

這樣就會導致一些初看上去很詭異的現(xiàn)象,拿一個經典的例子來驗證一下:

首先創(chuàng)建一個由內外兩個 DIV 嵌套組成的簡單結構:

<div id="outer">
    <div id="inner"></div>
</div>

JavaScript 代碼如下:

const inner = document.getElementById("inner");
const outer = document.getElementById("outer");

// 監(jiān)聽 outer 的屬性變化。
new MutationObserver(() => console.log("mutate outer"))
    .observe(outer, { attributes: true });

// 處理 click 事件。
function onClick()
{
    console.log("click");
    setTimeout(() => console.log("timeout"), 0);
    Promise.resolve().then(() => console.log("promise"));
    outer.setAttribute("data-mutation", Math.random());
}

// 監(jiān)聽 click 事件。
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);

這個東西看起來是這樣的:

like this

接下來,分別通過鼠標點擊代碼調用的方式來觸發(fā) inner 和 outer 的 click 事件,我們分別來看:

第一種方式:鼠標點擊黃色方塊,輸出結果如下 :

click 
promise 
mutate outer 
click  
promise 
mutate outer 
timeout 
timeout  

為了容易看明白整個過程,錄了一個簡單的動畫演示:
(視頻來源:請點擊)

Click

第二種方式:改成通過代碼調用方式觸發(fā),在上面例子的最后一行加上

inner.click();
輸出結果如下

click 
click  
promise 
mutate outer 
promise 
timeout 
timeout  

看視頻吧:(視頻來源:請點擊)

code invoke

總結一下,兩次執(zhí)行過程的本質區(qū)別,就在于執(zhí)行 Microtask Queue 前,當前執(zhí)行棧是否為空。因此在例子 2 的兩次 onClick 之間,就不會執(zhí)行 Microtask Queue,也就不會有控制臺輸出了。

PS:有興趣的朋友可以在例子 2最后再加上一行 console.log("end");,看看輸出結果是怎樣的。


四、Promise

Promise是一個構造函數(shù),自己身上有all、reject、resolve這幾個眼熟的方法,原型上有then、catch等同樣很眼熟的方法。那就new一個

var p = new Promise(function(resolve, reject){
    //做一些異步操作
    setTimeout(function(){
        console.log('執(zhí)行完成');
        resolve('隨便什么數(shù)據');
    }, 2000);
});

Promise的構造函數(shù)接收一個參數(shù),是函數(shù),并且傳入兩個參數(shù):resolve,reject,分別表示異步操作執(zhí)行成功后的回調函數(shù)和異步操作執(zhí)行失敗后的回調函數(shù)。其實這里用“成功”和“失敗”來描述并不準確,按照標準來講,resolve是將Promise的狀態(tài)置為fullfiled,reject是將Promise的狀態(tài)置為rejected。不過在我們開始階段可以先這么理解,后面再細究概念。

在上面的代碼中,我們執(zhí)行了一個異步操作,也就是setTimeout,2秒后,輸出“執(zhí)行完成”,并且調用resolve方法。

運行代碼,會在2秒后輸出“執(zhí)行完成”。注意!我只是new了一個對象,并沒有調用它,我們傳進去的函數(shù)就已經執(zhí)行了,這是需要注意的一個細節(jié)。所以我們用Promise的時候一般是包在一個函數(shù)中,在需要的時候去運行這個函數(shù),如:

function runAsync(){
    var p = new Promise(function(resolve, reject){
        //做一些異步操作
        setTimeout(function(){
            console.log('執(zhí)行完成');
            resolve('隨便什么數(shù)據');
        }, 2000);
    });
    return p;            
}
runAsync()

這時候你應該有兩個疑問:1.包裝這么一個函數(shù)有什么用?2.resolve('隨便什么數(shù)據');這又是干什么的?

我們繼續(xù)來講。在我們包裝好的函數(shù)最后,會return出Promise對象,也就是說,執(zhí)行這個函數(shù)我們得到了一個Promise對象。還記得Promise對象上有then、catch方法吧?這就是強大之處了,看下面的代碼

runAsync().then(function(data){
    console.log(data);
    //后面可以用傳過來的數(shù)據做些其他操作
    //......
});

在runAsync()的返回上直接調用then方法,then接收一個參數(shù),是函數(shù),并且會拿到我們在runAsync中調用resolve時傳的的參數(shù)。運行這段代碼,會在2秒后輸出“執(zhí)行完成”,緊接著輸出“隨便什么數(shù)據”。

這時候你應該有所領悟了,原來then里面的函數(shù)就跟我們平時的回調函數(shù)一個意思,能夠在runAsync這個異步任務執(zhí)行完成之后被執(zhí)行。這就是Promise的作用了,簡單來講,就是能把原來的回調寫法分離出來,在異步操作執(zhí)行完后,用鏈式調用的方式執(zhí)行回調函數(shù)。

你可能會不屑一顧,那么牛逼轟轟的Promise就這點能耐?我把回調函數(shù)封裝一下,給runAsync傳進去不也一樣嗎,就像這樣:

function runAsync(callback){
    setTimeout(function(){
        console.log('執(zhí)行完成');
        callback('隨便什么數(shù)據');
    }, 2000);
}

runAsync(function(data){
    console.log(data);
});

效果也是一樣的,還費勁用Promise干嘛。那么問題來了,有多層回調該怎么辦?如果callback也是一個異步操作,而且執(zhí)行完后也需要有相應的回調函數(shù),該怎么辦呢?總不能再定義一個callback2,然后給callback傳進去吧。而Promise的優(yōu)勢在于,可以在then方法中繼續(xù)寫Promise對象并返回,然后繼續(xù)調用then來進行回調操作。

1.鏈式操作的用法

從表面上看,Promise只是能夠簡化層層回調的寫法,而實質上,Promise的精髓是“狀態(tài)”,用維護狀態(tài)、傳遞狀態(tài)的方式來使得回調函數(shù)能夠及時調用,它比傳遞callback函數(shù)要簡單、靈活的多。所以使用Promise的正確場景是這樣的:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});

這樣能夠按順序,每隔兩秒輸出每個異步回調中的內容,在runAsync2中傳給resolve的數(shù)據,能在接下來的then方法中拿到。運行結果如下:

異步任務1執(zhí)行完成
隨便什么數(shù)據1
異步任務2執(zhí)行完成
隨便什么數(shù)據2
異步任務3執(zhí)行完成
隨便什么數(shù)據3

猜猜runAsync1、runAsync2、runAsync3這三個函數(shù)都是如何定義的?沒錯,就是下面這樣

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //做一些異步操作
        setTimeout(function(){
            console.log('異步任務1執(zhí)行完成');
            resolve('隨便什么數(shù)據1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些異步操作
        setTimeout(function(){
            console.log('異步任務2執(zhí)行完成');
            resolve('隨便什么數(shù)據2');
        }, 2000);
    });
    return p;            
}
function runAsync3(){
    var p = new Promise(function(resolve, reject){
        //做一些異步操作
        setTimeout(function(){
            console.log('異步任務3執(zhí)行完成');
            resolve('隨便什么數(shù)據3');
        }, 2000);
    });
    return p;            
}

在then方法中,你也可以直接return數(shù)據而不是Promise對象,在后面的then中就可以接收到數(shù)據了,比如我們把上面的代碼修改成這樣

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return '直接返回數(shù)據';  //這里直接返回數(shù)據
})
.then(function(data){
    console.log(data);
});

那么輸出就變成了這樣:

異步任務1執(zhí)行完成  
隨便什么數(shù)據1 
異步任務2執(zhí)行完成 
隨便什么數(shù)據2 
直接返回數(shù)據 

2.reject的用法

到這里,你應該對“Promise是什么玩意”有了最基本的了解。那么我們接著來看看ES6的Promise還有哪些功能。我們光用了resolve,還沒用reject呢,它是做什么的呢?事實上,我們前面的例子都是只有“執(zhí)行成功”的回調,還沒有“失敗”的情況,reject的作用就是把Promise的狀態(tài)置為rejected,這樣我們在then中就能捕捉到,然后執(zhí)行“失敗”情況的回調??聪旅娴拇a。

function getNumber(){
    var p = new Promise(function(resolve, reject){
        //做一些異步操作
        setTimeout(function(){
            var num = Math.ceil(Math.random()*10); //生成1-10的隨機數(shù)
            if(num<=5){
                resolve(num);
            }
            else{
                reject('數(shù)字太大了');
            }
        }, 2000);
    });
    return p;            
}

getNumber()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);

getNumber函數(shù)用來異步獲取一個數(shù)字,2秒后執(zhí)行完成,如果數(shù)字小于等于5,我們認為是“成功”了,調用resolve修改Promise的狀態(tài)。否則我們認為是“失敗”了,調用reject并傳遞一個參數(shù),作為失敗的原因。

運行getNumber并且在then中傳了兩個參數(shù),then方法可以接受兩個參數(shù),第一個對應resolve的回調,第二個對應reject的回調。所以我們能夠分別拿到他們傳過來的數(shù)據。多次運行這段代碼,你會隨機得到下面兩種結果:

resolved                    rejected 
1                或者       數(shù)字太大了

3.catch的用法

我們知道Promise對象除了then方法,還有一個catch方法,它是做什么用的呢?其實它和then的第二個參數(shù)一樣,用來指定reject的回調,用法是這樣:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

效果和寫在then的第二個參數(shù)里面一樣。不過它還有另外一個作用:在執(zhí)行resolve的回調(也就是上面then中的第一個參數(shù))時,如果拋出異常了(代碼出錯了),那么并不會報錯卡死js,而是會進到這個catch方法中。請看下面的代碼:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此處的somedata未定義
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

在resolve的回調中,我們console.log(somedata);而somedata這個變量是沒有被定義的。如果我們不用Promise,代碼運行到這里就直接在控制臺報錯了,不往下運行了。但是在這里,會得到這樣的結果:

resolved
4
rejected 
ReferenceError:somedata is not defined(...)|

也就是說進到catch方法里面去了,而且把錯誤原因傳到了reason參數(shù)中。即便是有錯誤的代碼也不會報錯了,這與我們的try/catch語句有相同的功能。

4.all的用法

Promise的all方法提供了并行執(zhí)行異步操作的能力,并且在所有異步操作執(zhí)行完后才執(zhí)行回調。我們仍舊使用上面定義好的runAsync1、runAsync2、runAsync3這三個函數(shù),看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

用Promise.all來執(zhí)行,all接收一個數(shù)組參數(shù),里面的值最終都算返回Promise對象。這樣,三個異步操作的并行執(zhí)行的,等到它們都執(zhí)行完后才會進到then里面。那么,三個異步操作返回的數(shù)據哪里去了呢?都在then里面呢,all會把所有異步操作的結果放進一個數(shù)組中傳給then,就是上面的results。所以上面代碼的輸出結果就是:

異步任務1執(zhí)行完畢
異步任務2執(zhí)行完畢
異步任務3執(zhí)行完畢
["隨便什么數(shù)據1","隨便什么數(shù)據2","隨便什么數(shù)據3"]

有了all,你就可以并行執(zhí)行多個異步操作,并且在一個回調中處理所有的返回數(shù)據,是不是很酷?有一個場景是很適合用這個的,一些游戲類的素材比較多的應用,打開網頁時,預先加載需要用到的各種資源如圖片、flash以及各種靜態(tài)文件。所有的都加載完后,我們再進行頁面的初始化。

5.race的用法

all方法的效果實際上是「誰跑的慢,以誰為準執(zhí)行回調」,那么相對的就有另一個方法「誰跑的快,以誰為準執(zhí)行回調」,這就是race方法,這個詞本來就是賽跑的意思。race的用法與all一樣,我們把上面runAsync1的延時改為1秒來看一下:

Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

這三個異步操作同樣是并行執(zhí)行的。結果你應該可以猜到,1秒后runAsync1已經執(zhí)行完了,此時then里面的就執(zhí)行了。結果是這樣的:

異步任務1執(zhí)行完畢
隨便什么數(shù)據1
異步任務2執(zhí)行完畢
異步任務3執(zhí)行完畢

你猜對了嗎?不完全,是吧。在then里面的回調開始執(zhí)行時,runAsync2()和runAsync3()并沒有停止,仍舊再執(zhí)行。于是再過1秒后,輸出了他們結束的標志。

這個race有什么用呢?使用場景還是很多的,比如我們可以用race給某個異步請求設置超時時間,并且在超時后執(zhí)行相應的操作,代碼如下:

//請求某個圖片資源
function requestImg(){
    var p = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
        img.src = 'xxxxxx';
    });
    return p;
}

//延時函數(shù),用于給請求計時
function timeout(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('圖片請求超時');
        }, 5000);
    });
    return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

requestImg函數(shù)會異步請求一張圖片,我把地址寫為"xxxxxx",所以肯定是無法成功請求到的。timeout函數(shù)是一個延時5秒的異步操作。我們把這兩個返回Promise對象的函數(shù)放進race,于是他倆就會賽跑,如果5秒之內圖片請求成功了,那么遍進入then方法,執(zhí)行正常的流程。如果5秒鐘圖片還未成功返回,那么timeout就跑贏了,則進入catch,報出“圖片請求超時”的信息。


五、事件冒泡

\color{red}{首先看看事件冒泡是什么?}
事件冒泡 :當一個元素接收到事件的時候 會把他接收到的事件傳給自己的父級,一直到window 。(注意這里傳遞的僅僅是事件 并不傳遞所綁定的事件函數(shù)。所以如果父級沒有綁定事件函數(shù),就算傳遞了事件 也不會有什么表現(xiàn) 但事件確實傳遞了。)

只看這句話,或許不是那么好理解,下面來看個栗子:

var div1 = document.getElementById("div1");
var div2 = document.getElementById("div2");
   div2.onclick = function(){alert(1);};
   div1.onclick = function(){alert(2);};//父親


//html代碼
 <div id="div1">

    <div id="div2"></div>
 </div>

代碼很簡單,就是兩個父子關系的div,然后分別加了點擊事件,當我們在div2里面點擊的時候,會發(fā)現(xiàn)彈出了一次1,接著又彈出了2,這說明點擊的時候,不僅div2的事件被觸發(fā)了,它的父級的點擊事件也觸發(fā)了,這種現(xiàn)象就叫做冒泡。點擊了div1,自己父級的點擊事件也會被觸發(fā)。再看個例子:

var div1 = document.getElementById("div1");
var div2 = document.getElementById("div2")  

div1.onclick = function(){alert(2);}; // 父親

//html代碼
 <div id="div1"> <div id="div2"></div> </div>
效果圖

大家可以看一下效果圖,相比于第一個例子,代碼已經把兒子的點擊事件去掉,只留下了父級的,測試的結果是當只點擊了兒子,會彈出2,由此證明了當點擊了兒子,父親的點擊事件被觸發(fā),執(zhí)行了自己綁定的函數(shù)。由于一些人會以為顯示出來兒子在父親里面的時候,自然點了兒子相當于點了父親,所以這個例子我故意把兩個盒子絕對定位在了兩個不同的位置,所以點擊事件給頁面顯示出來的位置是沒關系的,而是跟html代碼中的位置有關系。
可能有人會有疑惑下面這種情況,為啥沒有彈出兩次:

var div1 = document.getElementById("div1");
var div2 = document.getElementById("div2");
   div2.onclick = function(){alert(1);}; 
   div1.onclick = function(){};  //父親

//html代碼

 <div id="div1">

    <div id="div2"></div>

 </div>

這里我們要注意,我們傳遞的僅僅是事件觸發(fā),也就是說當點擊div2僅僅觸發(fā)了父級的點擊事件,并沒有把自己的綁定的函數(shù)給父級,父級的執(zhí)行情況,取決于自己所綁定的函數(shù),因為在這里它綁定的函數(shù)是空,自然沒什么表現(xiàn)。有些人在這里有誤解,所以強調一下。


六、 事件委托

\color{red}{事件委托:也稱事件代理 就是利用冒泡的原理 把加事件加到父級上,觸發(fā)執(zhí)行效果}
首先呢,你一定寫過這樣的程序,有一個列表,當鼠標移入每個li,背景顏色變紅,于是我們寫出了這樣的代碼:

window.onload = function(){ 
    var oUl = document.getElementById('ull');
    var aLi = document.getElementsByTagName('li'); //獲取所有列
    for(var i =0;i < aLi.length;i++){ 
         aLi[i].onmouseover = function(){ 
             this.style.background = "red";
         }
}

當然這樣一看代碼也沒什么問題,通過循環(huán)給每個li加事件,但想一想如果我們有很多個li,是不是要加很多次事件,這樣其實是非常耗性能的。那么我們會想,能不能只加一個事件就能實現(xiàn)呢。當然是能的,不然我就不會在這扯了。

那就是通過冒泡原理進行事件委托,我們可以把事件只加給父級oUL,這樣不管移入哪個li,都會觸發(fā)父級的移入事件,但這個時候也有個問題,因為我的需求是,讓對應的li變顏色,不是讓整個列表變,它怎么知道我鼠標移入的是哪個LI,這個時候萬能的事件對象中的一個屬性就要出場了,就是事件源 (不管事件綁定在那個元素中 都指的是實際觸發(fā)事件的那個的目標),就是能獲取到你當前鼠標所在的LI,

\color{red}{其實事件委托還有第二個優(yōu)點:就是新添加的元素還會有之前的事件}

假定我們又有一個需求,點擊某個按鈕,可以在列表中再創(chuàng)建一個li,這個時候一般方法,因為新創(chuàng)建的li沒有加事件,所以是不具備移入變紅的功能的,但是用事件委托的方法,新的li,同樣有這個事件。原理也很容易相同,因為事件是加在父親上面的,父親在,事件在,大家可以自己測試一下。


參考資料:

如果您有什么疑問或者發(fā)現(xiàn)書寫歧義,非常感激您能留言~
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容