事件機(jī)制
事件觸發(fā)三階段
事件觸發(fā)有三個(gè)階段
-
window往事件觸發(fā)處傳播,遇到注冊(cè)的捕獲事件會(huì)觸發(fā) - 傳播到事件觸發(fā)處時(shí)觸發(fā)注冊(cè)的事件
- 從事件觸發(fā)處往
window傳播,遇到注冊(cè)的冒泡事件會(huì)觸發(fā)
事件觸發(fā)一般來(lái)說(shuō)會(huì)按照上面的順序進(jìn)行,但是也有特例,如果給一個(gè)目標(biāo)節(jié)點(diǎn)同時(shí)注冊(cè)冒泡和捕獲事件,事件觸發(fā)會(huì)按照注冊(cè)的順序執(zhí)行。
// 以下會(huì)先打印冒泡然后是捕獲
node.addEventListener('click',(event) =>{
console.log('冒泡')
},false);
node.addEventListener('click',(event) =>{
console.log('捕獲 ')
},true)
注冊(cè)事件
通常我們使用 addEventListener 注冊(cè)事件,該函數(shù)的第三個(gè)參數(shù)可以是布爾值,也可以是對(duì)象。對(duì)于布爾值 useCapture 參數(shù)來(lái)說(shuō),該參數(shù)默認(rèn)值為 false 。useCapture 決定了注冊(cè)的事件是捕獲事件還是冒泡事件。對(duì)于對(duì)象參數(shù)來(lái)說(shuō),可以使用以下幾個(gè)屬性
-
capture,布爾值,和useCapture作用一樣 -
once,布爾值,值為true表示該回調(diào)只會(huì)調(diào)用一次,調(diào)用后會(huì)移除監(jiān)聽 -
passive,布爾值,表示永遠(yuǎn)不會(huì)調(diào)用preventDefault
一般來(lái)說(shuō),我們只希望事件只觸發(fā)在目標(biāo)上,這時(shí)候可以使用 stopPropagation 來(lái)阻止事件的進(jìn)一步傳播。通常我們認(rèn)為 stopPropagation 是用來(lái)阻止事件冒泡的,其實(shí)該函數(shù)也可以阻止捕獲事件。stopImmediatePropagation 同樣也能實(shí)現(xiàn)阻止事件,但是還能阻止該事件目標(biāo)執(zhí)行別的注冊(cè)事件。
node.addEventListener('click',(event) =>{
event.stopImmediatePropagation()
console.log('冒泡')
},false);
// 點(diǎn)擊 node 只會(huì)執(zhí)行上面的函數(shù),該函數(shù)不會(huì)執(zhí)行
node.addEventListener('click',(event) => {
console.log('捕獲 ')
},true)
事件代理
如果一個(gè)節(jié)點(diǎn)中的子節(jié)點(diǎn)是動(dòng)態(tài)生成的,那么子節(jié)點(diǎn)需要注冊(cè)事件的話應(yīng)該注冊(cè)在父節(jié)點(diǎn)上
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
事件代理的方式相對(duì)于直接給目標(biāo)注冊(cè)事件來(lái)說(shuō),有以下優(yōu)點(diǎn)
- 節(jié)省內(nèi)存
- 不需要給子節(jié)點(diǎn)注銷事件
跨域
因?yàn)闉g覽器出于安全考慮,有同源策略。也就是說(shuō),如果協(xié)議、域名或者端口有一個(gè)不同就是跨域,Ajax 請(qǐng)求會(huì)失敗。
我們可以通過(guò)以下幾種常用方法解決跨域的問(wèn)題
JSONP
JSONP 的原理很簡(jiǎn)單,就是利用 <script> 標(biāo)簽沒(méi)有跨域限制的漏洞。通過(guò) <script> 標(biāo)簽指向一個(gè)需要訪問(wèn)的地址并提供一個(gè)回調(diào)函數(shù)來(lái)接收數(shù)據(jù)當(dāng)需要通訊時(shí)。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
JSONP 使用簡(jiǎn)單且兼容性不錯(cuò),但是只限于 get 請(qǐng)求。
在開發(fā)中可能會(huì)遇到多個(gè) JSONP 請(qǐng)求的回調(diào)函數(shù)名是相同的,這時(shí)候就需要自己封裝一個(gè) JSONP,以下是簡(jiǎn)單實(shí)現(xiàn)
function jsonp(url, jsonpCallback, success) {
let script = document.createElement("script");
script.src = url;
script.async = true;
script.type = "text/javascript";
window[jsonpCallback] = function(data) {
success && success(data);
};
document.body.appendChild(script);
}
jsonp(
"http://xxx",
"callback",
function(value) {
console.log(value);
}
);
CORS
CORS需要瀏覽器和后端同時(shí)支持。IE 8 和 9 需要通過(guò) XDomainRequest 來(lái)實(shí)現(xiàn)。
瀏覽器會(huì)自動(dòng)進(jìn)行 CORS 通信,實(shí)現(xiàn)CORS通信的關(guān)鍵是后端。只要后端實(shí)現(xiàn)了 CORS,就實(shí)現(xiàn)了跨域。
服務(wù)端設(shè)置 Access-Control-Allow-Origin 就可以開啟 CORS。 該屬性表示哪些域名可以訪問(wèn)資源,如果設(shè)置通配符則表示所有網(wǎng)站都可以訪問(wèn)資源。
document.domain
該方式只能用于二級(jí)域名相同的情況下,比如 a.test.com 和 b.test.com 適用于該方式。
只需要給頁(yè)面添加 document.domain = 'test.com' 表示二級(jí)域名都相同就可以實(shí)現(xiàn)跨域
postMessage
這種方式通常用于獲取嵌入頁(yè)面中的第三方頁(yè)面數(shù)據(jù)。一個(gè)頁(yè)面發(fā)送消息,另一個(gè)頁(yè)面判斷來(lái)源并接收消息
// 發(fā)送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://test.com') {
console.log('驗(yàn)證通過(guò)')
}
});
Event loop
眾所周知 JS 是門非阻塞單線程語(yǔ)言,因?yàn)樵谧畛?JS 就是為了和瀏覽器交互而誕生的。如果 JS 是門多線程的語(yǔ)言話,我們?cè)诙鄠€(gè)線程中處理 DOM 就可能會(huì)發(fā)生問(wèn)題(一個(gè)線程中新加節(jié)點(diǎn),另一個(gè)線程中刪除節(jié)點(diǎn)),當(dāng)然可以引入讀寫鎖解決這個(gè)問(wèn)題。
JS 在執(zhí)行的過(guò)程中會(huì)產(chǎn)生執(zhí)行環(huán)境,這些執(zhí)行環(huán)境會(huì)被順序的加入到執(zhí)行棧中。如果遇到異步的代碼,會(huì)被掛起并加入到 Task(有多種 task) 隊(duì)列中。一旦執(zhí)行棧為空,Event Loop 就會(huì)從 Task 隊(duì)列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行,所以本質(zhì)上來(lái)說(shuō) JS 中的異步還是同步行為。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
以上代碼雖然 setTimeout 延時(shí)為 0,其實(shí)還是異步。這是因?yàn)?HTML5 標(biāo)準(zhǔn)規(guī)定這個(gè)函數(shù)第二個(gè)參數(shù)不得小于 4 毫秒,不足會(huì)自動(dòng)增加。所以 setTimeout 還是會(huì)在 script end 之后打印。
不同的任務(wù)源會(huì)被分配到不同的 Task 隊(duì)列中,任務(wù)源可以分為 微任務(wù)(microtask) 和 宏任務(wù)(macrotask)。在 ES6 規(guī)范中,microtask 稱為 jobs,macrotask 稱為 task。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
以上代碼雖然 setTimeout 寫在 Promise 之前,但是因?yàn)?Promise 屬于微任務(wù)而 setTimeout 屬于宏任務(wù),所以會(huì)有以上的打印。
微任務(wù)包括 process.nextTick ,promise ,Object.observe ,MutationObserver
宏任務(wù)包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
很多人有個(gè)誤區(qū),認(rèn)為微任務(wù)快于宏任務(wù),其實(shí)是錯(cuò)誤的。因?yàn)楹耆蝿?wù)中包括了 script ,瀏覽器會(huì)先執(zhí)行一個(gè)宏任務(wù),接下來(lái)有異步代碼的話就先執(zhí)行微任務(wù)。
所以正確的一次 Event loop 順序是這樣的
- 執(zhí)行同步代碼,這屬于宏任務(wù)
- 執(zhí)行棧為空,查詢是否有微任務(wù)需要執(zhí)行
- 執(zhí)行所有微任務(wù)
- 必要的話渲染 UI
- 然后開始下一輪 Event loop,執(zhí)行宏任務(wù)中的異步代碼
通過(guò)上述的 Event loop 順序可知,如果宏任務(wù)中的異步代碼有大量的計(jì)算并且需要操作 DOM 的話,為了更快的 界面響應(yīng),我們可以把操作 DOM 放入微任務(wù)中。
Node 中的 Event loop
Node 中的 Event loop 和瀏覽器中的不相同。
Node 的 Event loop 分為6個(gè)階段,它們會(huì)按照順序反復(fù)運(yùn)行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timer
timers 階段會(huì)執(zhí)行 setTimeout 和 setInterval
一個(gè) timer 指定的時(shí)間并不是準(zhǔn)確時(shí)間,而是在達(dá)到這個(gè)時(shí)間后盡快執(zhí)行回調(diào),可能會(huì)因?yàn)橄到y(tǒng)正在執(zhí)行別的事務(wù)而延遲。
下限的時(shí)間有一個(gè)范圍:[1, 2147483647] ,如果設(shè)定的時(shí)間不在這個(gè)范圍,將被設(shè)置為1。
I/O
I/O 階段會(huì)執(zhí)行除了 close 事件,定時(shí)器和 setImmediate 的回調(diào)
idle, prepare
idle, prepare 階段內(nèi)部實(shí)現(xiàn)
poll
poll 階段很重要,這一階段中,系統(tǒng)會(huì)做兩件事情
- 執(zhí)行到點(diǎn)的定時(shí)器
- 執(zhí)行 poll 隊(duì)列中的事件
并且當(dāng) poll 中沒(méi)有定時(shí)器的情況下,會(huì)發(fā)現(xiàn)以下兩件事情
- 如果 poll 隊(duì)列不為空,會(huì)遍歷回調(diào)隊(duì)列并同步執(zhí)行,直到隊(duì)列為空或者系統(tǒng)限制
- 如果 poll 隊(duì)列為空,會(huì)有兩件事發(fā)生
- 如果有
setImmediate需要執(zhí)行,poll 階段會(huì)停止并且進(jìn)入到 check 階段執(zhí)行setImmediate - 如果沒(méi)有
setImmediate需要執(zhí)行,會(huì)等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行回調(diào)
- 如果有
如果有別的定時(shí)器需要被執(zhí)行,會(huì)回到 timer 階段執(zhí)行回調(diào)。
check
check 階段執(zhí)行 setImmediate
close callbacks
close callbacks 階段執(zhí)行 close 事件
并且在 Node 中,有些情況下的定時(shí)器執(zhí)行順序是隨機(jī)的
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 這里可能會(huì)輸出 setTimeout,setImmediate
// 可能也會(huì)相反的輸出,這取決于性能
// 因?yàn)榭赡苓M(jìn)入 event loop 用了不到 1 毫秒,這時(shí)候會(huì)執(zhí)行 setImmediate
// 否則會(huì)執(zhí)行 setTimeout
當(dāng)然在這種情況下,執(zhí)行順序是相同的
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 因?yàn)?readFile 的回調(diào)在 poll 中執(zhí)行
// 發(fā)現(xiàn)有 setImmediate ,所以會(huì)立即跳到 check 階段執(zhí)行回調(diào)
// 再去 timer 階段執(zhí)行 setTimeout
// 所以以上輸出一定是 setImmediate,setTimeout
上面介紹的都是 macrotask 的執(zhí)行情況,microtask 會(huì)在以上每個(gè)階段完成后立即執(zhí)行。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
// 以上代碼在瀏覽器和 node 中打印情況是不同的
// 瀏覽器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
Node 中的 process.nextTick 會(huì)先于其他 microtask 執(zhí)行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1
存儲(chǔ)
cookie,localStorage,sessionStorage,indexDB
| 特性 | cookie | localStorage | sessionStorage | indexDB |
|---|---|---|---|---|
| 數(shù)據(jù)生命周期 | 一般由服務(wù)器生成,可以設(shè)置過(guò)期時(shí)間 | 除非被清理,否則一直存在 | 頁(yè)面關(guān)閉就清理 | 除非被清理,否則一直存在 |
| 數(shù)據(jù)存儲(chǔ)大小 | 4K | 5M | 5M | 無(wú)限 |
| 與服務(wù)端通信 | 每次都會(huì)攜帶在 header 中,對(duì)于請(qǐng)求性能影響 | 不參與 | 不參與 | 不參與 |
從上表可以看到,cookie 已經(jīng)不建議用于存儲(chǔ)。如果沒(méi)有大量數(shù)據(jù)存儲(chǔ)需求的話,可以使用 localStorage 和 sessionStorage 。對(duì)于不怎么改變的數(shù)據(jù)盡量使用 localStorage 存儲(chǔ),否則可以用 sessionStorage 存儲(chǔ)。
對(duì)于 cookie,我們還需要注意安全性。
| 屬性 | 作用 |
|---|---|
| value | 如果用于保存用戶登錄態(tài),應(yīng)該將該值加密,不能使用明文的用戶標(biāo)識(shí) |
| http-only | 不能通過(guò) JS 訪問(wèn) Cookie,減少 XSS 攻擊 |
| secure | 只能在協(xié)議為 HTTPS 的請(qǐng)求中攜帶 |
| same-site | 規(guī)定瀏覽器不能在跨域請(qǐng)求中攜帶 Cookie,減少 CSRF 攻擊 |
Service Worker
Service workers 本質(zhì)上充當(dāng)Web應(yīng)用程序與瀏覽器之間的代理服務(wù)器,也可以在網(wǎng)絡(luò)可用時(shí)作為瀏覽器和網(wǎng)絡(luò)間的代理。它們旨在(除其他之外)使得能夠創(chuàng)建有效的離線體驗(yàn),攔截網(wǎng)絡(luò)請(qǐng)求并基于網(wǎng)絡(luò)是否可用以及更新的資源是否駐留在服務(wù)器上來(lái)采取適當(dāng)?shù)膭?dòng)作。他們還允許訪問(wèn)推送通知和后臺(tái)同步API。
目前該技術(shù)通常用來(lái)做緩存文件,提高首屏速度,可以試著來(lái)實(shí)現(xiàn)這個(gè)功能。
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("sw.js")
.then(function(registration) {
console.log("service worker 注冊(cè)成功");
})
.catch(function(err) {
console.log("servcie worker 注冊(cè)失敗");
});
}
// sw.js
// 監(jiān)聽 `install` 事件,回調(diào)中緩存所需文件
self.addEventListener("install", e => {
e.waitUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
);
});
// 攔截所有請(qǐng)求事件
// 如果緩存中已經(jīng)有請(qǐng)求的數(shù)據(jù)就直接用緩存,否則去請(qǐng)求數(shù)據(jù)
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response;
}
console.log("fetch source");
})
);
});
打開頁(yè)面,可以在開發(fā)者工具中的 Application 看到 Service Worker 已經(jīng)啟動(dòng)了
在 Cache 中也可以發(fā)現(xiàn)我們所需的文件已被緩存
當(dāng)我們重新刷新頁(yè)面可以發(fā)現(xiàn)我們緩存的數(shù)據(jù)是從 Service Worker 中讀取的
渲染機(jī)制
瀏覽器的渲染機(jī)制一般分為以下幾個(gè)步驟
- 處理 HTML 并構(gòu)建 DOM 樹。
- 處理 CSS 構(gòu)建 CSSOM 樹。
- 將 DOM 與 CSSOM 合并成一個(gè)渲染樹。
- 根據(jù)渲染樹來(lái)布局,計(jì)算每個(gè)節(jié)點(diǎn)的位置。
- 調(diào)用 GPU 繪制,合成圖層,顯示在屏幕上。
在構(gòu)建 CSSOM 樹時(shí),會(huì)阻塞渲染,直至 CSSOM 樹構(gòu)建完成。并且構(gòu)建 CSSOM 樹是一個(gè)十分消耗性能的過(guò)程,所以應(yīng)該盡量保證層級(jí)扁平,減少過(guò)度層疊,越是具體的 CSS 選擇器,執(zhí)行速度越慢。
當(dāng) HTML 解析到 script 標(biāo)簽時(shí),會(huì)暫停構(gòu)建 DOM,完成后才會(huì)從暫停的地方重新開始。也就是說(shuō),如果你想首屏渲染的越快,就越不應(yīng)該在首屏就加載 JS 文件。并且 CSS 也會(huì)影響 JS 的執(zhí)行,只有當(dāng)解析完樣式表才會(huì)執(zhí)行 JS,所以也可以認(rèn)為這種情況下,CSS 也會(huì)暫停構(gòu)建 DOM。
Load 和 DOMContentLoaded 區(qū)別
Load 事件觸發(fā)代表頁(yè)面中的 DOM,CSS,JS,圖片已經(jīng)全部加載完畢。
DOMContentLoaded 事件觸發(fā)代表初始的 HTML 被完全加載和解析,不需要等待 CSS,JS,圖片加載。
圖層
一般來(lái)說(shuō),可以把普通文檔流看成一個(gè)圖層。特定的屬性可以生成一個(gè)新的圖層。不同的圖層渲染互不影響,所以對(duì)于某些頻繁需要渲染的建議單獨(dú)生成一個(gè)新圖層,提高性能。但也不能生成過(guò)多的圖層,會(huì)引起反作用。
通過(guò)以下幾個(gè)常用屬性可以生成新圖層
- 3D 變換:
translate3d、translateZ will-change-
video、iframe標(biāo)簽 - 通過(guò)動(dòng)畫實(shí)現(xiàn)的
opacity動(dòng)畫轉(zhuǎn)換 position: fixed
重繪(Repaint)和回流(Reflow)
重繪和回流是渲染步驟中的一小節(jié),但是這兩個(gè)步驟對(duì)于性能影響很大。
- 重繪是當(dāng)節(jié)點(diǎn)需要更改外觀而不會(huì)影響布局的,比如改變
color就叫稱為重繪 - 回流是布局或者幾何屬性需要改變就稱為回流。
回流必定會(huì)發(fā)生重繪,重繪不一定會(huì)引發(fā)回流。回流所需的成本比重繪高的多,改變深層次的節(jié)點(diǎn)很可能導(dǎo)致父節(jié)點(diǎn)的一系列回流。
所以以下幾個(gè)動(dòng)作可能會(huì)導(dǎo)致性能問(wèn)題:
- 改變 window 大小
- 改變字體
- 添加或刪除樣式
- 文字改變
- 定位或者浮動(dòng)
- 盒模型
很多人不知道的是,重繪和回流其實(shí)和 Event loop 有關(guān)。
- 當(dāng) Event loop 執(zhí)行完 Microtasks 后,會(huì)判斷 document 是否需要更新。因?yàn)闉g覽器是 60Hz 的刷新率,每 16ms 才會(huì)更新一次。
- 然后判斷是否有
resize或者scroll,有的話會(huì)去觸發(fā)事件,所以resize和scroll事件也是至少 16ms 才會(huì)觸發(fā)一次,并且自帶節(jié)流功能。 - 判斷是否觸發(fā)了 media query
- 更新動(dòng)畫并且發(fā)送事件
- 判斷是否有全屏操作事件
- 執(zhí)行
requestAnimationFrame回調(diào) - 執(zhí)行
IntersectionObserver回調(diào),該方法用于判斷元素是否可見,可以用于懶加載上,但是兼容性不好 - 更新界面
- 以上就是一幀中可能會(huì)做的事情。如果在一幀中有空閑時(shí)間,就會(huì)去執(zhí)行
requestIdleCallback回調(diào)。
以上內(nèi)容來(lái)自于 HTML 文檔
減少重繪和回流
-
使用
translate替代top<div class="test"></div> <style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style> <script> setTimeout(() => { // 引起回流 document.querySelector('.test').style.top = '100px' }, 1000) </script> 使用
visibility替換display: none,因?yàn)榍罢咧粫?huì)引起重繪,后者會(huì)引發(fā)回流(改變了布局)把 DOM 離線后修改,比如:先把 DOM 給
display:none(有一次 Reflow),然后你修改100次,然后再把它顯示出來(lái)-
不要把 DOM 結(jié)點(diǎn)的屬性值放在一個(gè)循環(huán)里當(dāng)成循環(huán)里的變量
for(let i = 0; i < 1000; i++) { // 獲取 offsetTop 會(huì)導(dǎo)致回流,因?yàn)樾枰カ@取正確的值 console.log(document.querySelector('.test').style.offsetTop) } 不要使用 table 布局,可能很小的一個(gè)小改動(dòng)會(huì)造成整個(gè) table 的重新布局
動(dòng)畫實(shí)現(xiàn)的速度的選擇,動(dòng)畫速度越快,回流次數(shù)越多,也可以選擇使用
requestAnimationFrameCSS 選擇符從右往左匹配查找,避免 DOM 深度過(guò)深
-
將頻繁運(yùn)行的動(dòng)畫變?yōu)閳D層,圖層能夠阻止該節(jié)點(diǎn)回流影響別的元素。比如對(duì)于
video標(biāo)簽,瀏覽器會(huì)自動(dòng)將該節(jié)點(diǎn)變?yōu)閳D層。image