事件觸發有三個階段javascript
window
往事件觸發處傳播,遇到註冊的捕獲事件會觸發window
傳播,遇到註冊的冒泡事件會觸發事件觸發通常來講會按照上面的順序進行,可是也有特例,若是給一個目標節點同時註冊冒泡和捕獲事件,事件觸發會按照註冊的順序執行。css
// 如下會先打印冒泡而後是捕獲
node.addEventListener('click',(event) =>{
console.log('冒泡')
},false);
node.addEventListener('click',(event) =>{
console.log('捕獲 ')
},true)
複製代碼
一般咱們使用 addEventListener
註冊事件,該函數的第三個參數能夠是布爾值,也能夠是對象。對於布爾值 useCapture
參數來講,該參數默認值爲 false
。useCapture
決定了註冊的事件是捕獲事件仍是冒泡事件。對於對象參數來講,可使用如下幾個屬性html
capture
,布爾值,和 useCapture
做用同樣once
,布爾值,值爲 true
表示該回調只會調用一次,調用後會移除監聽passive
,布爾值,表示永遠不會調用 preventDefault
通常來講,咱們只但願事件只觸發在目標上,這時候可使用 stopPropagation
來阻止事件的進一步傳播。一般咱們認爲 stopPropagation
是用來阻止事件冒泡的,其實該函數也能夠阻止捕獲事件。stopImmediatePropagation
一樣也能實現阻止事件,可是還能阻止該事件目標執行別的註冊事件。java
node.addEventListener('click',(event) =>{
event.stopImmediatePropagation()
console.log('冒泡')
},false);
// 點擊 node 只會執行上面的函數,該函數不會執行
node.addEventListener('click',(event) => {
console.log('捕獲 ')
},true)
複製代碼
若是一個節點中的子節點是動態生成的,那麼子節點須要註冊事件的話應該註冊在父節點上node
<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>
複製代碼
事件代理的方式相對於直接給目標註冊事件來講,有如下優勢web
由於瀏覽器出於安全考慮,有同源策略。也就是說,若是協議、域名或者端口有一個不一樣就是跨域,Ajax 請求會失敗。json
咱們能夠經過如下幾種經常使用方法解決跨域的問題後端
JSONP 的原理很簡單,就是利用 <script>
標籤沒有跨域限制的漏洞。經過 <script>
標籤指向一個須要訪問的地址並提供一個回調函數來接收數據當須要通信時。api
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
複製代碼
JSONP 使用簡單且兼容性不錯,可是隻限於 get
請求。跨域
在開發中可能會遇到多個 JSONP 請求的回調函數名是相同的,這時候就須要本身封裝一個 JSONP,如下是簡單實現
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須要瀏覽器和後端同時支持。IE 8 和 9 須要經過 XDomainRequest
來實現。
瀏覽器會自動進行 CORS 通訊,實現CORS通訊的關鍵是後端。只要後端實現了 CORS,就實現了跨域。
服務端設置 Access-Control-Allow-Origin
就能夠開啓 CORS。 該屬性表示哪些域名能夠訪問資源,若是設置通配符則表示全部網站均可以訪問資源。
該方式只能用於二級域名相同的狀況下,好比 a.test.com
和 b.test.com
適用於該方式。
只須要給頁面添加 document.domain = 'test.com'
表示二級域名都相同就能夠實現跨域
這種方式一般用於獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另外一個頁面判斷來源並接收消息
// 發送消息端
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('驗證經過')
}
});
複製代碼
衆所周知 JS 是門非阻塞單線程語言,由於在最初 JS 就是爲了和瀏覽器交互而誕生的。若是 JS 是門多線程的語言話,咱們在多個線程中處理 DOM 就可能會發生問題(一個線程中新加節點,另外一個線程中刪除節點),固然能夠引入讀寫鎖解決這個問題。
JS 在執行的過程當中會產生執行環境,這些執行環境會被順序的加入到執行棧中。若是遇到異步的代碼,會被掛起並加入到 Task(有多種 task) 隊列中。一旦執行棧爲空,Event Loop 就會從 Task 隊列中拿出須要執行的代碼並放入執行棧中執行,因此本質上來講 JS 中的異步仍是同步行爲。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
複製代碼
以上代碼雖然 setTimeout
延時爲 0,其實仍是異步。這是由於 HTML5 標準規定這個函數第二個參數不得小於 4 毫秒,不足會自動增長。因此 setTimeout
仍是會在 script end
以後打印。
不一樣的任務源會被分配到不一樣的 Task 隊列中,任務源能夠分爲 微任務(microtask) 和 宏任務(macrotask)。在 ES6 規範中,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
以前,可是由於 Promise
屬於微任務而 setTimeout
屬於宏任務,因此會有以上的打印。
微任務包括 process.nextTick
,promise
,Object.observe
,MutationObserver
宏任務包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
不少人有個誤區,認爲微任務快於宏任務,實際上是錯誤的。由於宏任務中包括了 script
,瀏覽器會先執行一個宏任務,接下來有異步代碼的話就先執行微任務。
因此正確的一次 Event loop 順序是這樣的
經過上述的 Event loop 順序可知,若是宏任務中的異步代碼有大量的計算而且須要操做 DOM 的話,爲了更快的 界面響應,咱們能夠把操做 DOM 放入微任務中。
Node 中的 Event loop 和瀏覽器中的不相同。
Node 的 Event loop 分爲6個階段,它們會按照順序反覆運行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
timers 階段會執行 setTimeout
和 setInterval
一個 timer
指定的時間並非準確時間,而是在達到這個時間後儘快執行回調,可能會由於系統正在執行別的事務而延遲。
下限的時間有一個範圍:[1, 2147483647]
,若是設定的時間不在這個範圍,將被設置爲1。
I/O 階段會執行除了 close 事件,定時器和 setImmediate
的回調
idle, prepare 階段內部實現
poll 階段很重要,這一階段中,系統會作兩件事情
而且當 poll 中沒有定時器的狀況下,會發現如下兩件事情
setImmediate
須要執行,poll 階段會中止而且進入到 check 階段執行 setImmediate
setImmediate
須要執行,會等待回調被加入到隊列中並當即執行回調若是有別的定時器須要被執行,會回到 timer 階段執行回調。
check 階段執行 setImmediate
close callbacks 階段執行 close 事件
而且在 Node 中,有些狀況下的定時器執行順序是隨機的
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 這裏可能會輸出 setTimeout,setImmediate
// 可能也會相反的輸出,這取決於性能
// 由於可能進入 event loop 用了不到 1 毫秒,這時候會執行 setImmediate
// 不然會執行 setTimeout
複製代碼
固然在這種狀況下,執行順序是相同的
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 由於 readFile 的回調在 poll 中執行
// 發現有 setImmediate ,因此會當即跳到 check 階段執行回調
// 再去 timer 階段執行 setTimeout
// 因此以上輸出必定是 setImmediate,setTimeout
複製代碼
上面介紹的都是 macrotask 的執行狀況,microtask 會在以上每一個階段完成後當即執行。
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
會先於其餘 microtask 執行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1
複製代碼
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
數據生命週期 | 通常由服務器生成,能夠設置過時時間 | 除非被清理,不然一直存在 | 頁面關閉就清理 | 除非被清理,不然一直存在 |
數據存儲大小 | 4K | 5M | 5M | 無限 |
與服務端通訊 | 每次都會攜帶在 header 中,對於請求性能影響 | 不參與 | 不參與 | 不參與 |
從上表能夠看到,cookie
已經不建議用於存儲。若是沒有大量數據存儲需求的話,可使用 localStorage
和 sessionStorage
。對於不怎麼改變的數據儘可能使用 localStorage
存儲,不然能夠用 sessionStorage
存儲。
對於 cookie
,咱們還須要注意安全性。
屬性 | 做用 |
---|---|
value | 若是用於保存用戶登陸態,應該將該值加密,不能使用明文的用戶標識 |
http-only | 不能經過 JS 訪問 Cookie,減小 XSS 攻擊 |
secure | 只能在協議爲 HTTPS 的請求中攜帶 |
same-site | 規定瀏覽器不能在跨域請求中攜帶 Cookie,減小 CSRF 攻擊 |
Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也能夠在網絡可用時做爲瀏覽器和網絡間的代理。它們旨在(除其餘以外)使得可以建立有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採起適當的動做。他們還容許訪問推送通知和後臺同步API。
目前該技術一般用來作緩存文件,提升首屏速度,能夠試着來實現這個功能。
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("sw.js")
.then(function(registration) {
console.log("service worker 註冊成功");
})
.catch(function(err) {
console.log("servcie worker 註冊失敗");
});
}
// sw.js
// 監聽 `install` 事件,回調中緩存所需文件
self.addEventListener("install", e => {
e.waitUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
);
});
// 攔截全部請求事件
// 若是緩存中已經有請求的數據就直接用緩存,不然去請求數據
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response;
}
console.log("fetch source");
})
);
});
複製代碼
打開頁面,能夠在開發者工具中的 Application
看到 Service Worker 已經啓動了
在 Cache 中也能夠發現咱們所需的文件已被緩存
當咱們從新刷新頁面能夠發現咱們緩存的數據是從 Service Worker 中讀取的
瀏覽器的渲染機制通常分爲如下幾個步驟
在構建 CSSOM 樹時,會阻塞渲染,直至 CSSOM 樹構建完成。而且構建 CSSOM 樹是一個十分消耗性能的過程,因此應該儘可能保證層級扁平,減小過分層疊,越是具體的 CSS 選擇器,執行速度越慢。
當 HTML 解析到 script 標籤時,會暫停構建 DOM,完成後纔會從暫停的地方從新開始。也就是說,若是你想首屏渲染的越快,就越不該該在首屏就加載 JS 文件。而且 CSS 也會影響 JS 的執行,只有當解析完樣式表纔會執行 JS,因此也能夠認爲這種狀況下,CSS 也會暫停構建 DOM。
Load 事件觸發表明頁面中的 DOM,CSS,JS,圖片已經所有加載完畢。
DOMContentLoaded 事件觸發表明初始的 HTML 被徹底加載和解析,不須要等待 CSS,JS,圖片加載。
通常來講,能夠把普通文檔流當作一個圖層。特定的屬性能夠生成一個新的圖層。不一樣的圖層渲染互不影響,因此對於某些頻繁須要渲染的建議單獨生成一個新圖層,提升性能。但也不能生成過多的圖層,會引發副作用。
經過如下幾個經常使用屬性能夠生成新圖層
translate3d
、translateZ
will-change
video
、iframe
標籤opacity
動畫轉換position: fixed
重繪和迴流是渲染步驟中的一小節,可是這兩個步驟對於性能影響很大。
color
就叫稱爲重繪迴流一定會發生重繪,重繪不必定會引起迴流。迴流所需的成本比重繪高的多,改變深層次的節點極可能致使父節點的一系列迴流。
因此如下幾個動做可能會致使性能問題:
不少人不知道的是,重繪和迴流其實和 Event loop 有關。
resize
或者 scroll
,有的話會去觸發事件,因此 resize
和 scroll
事件也是至少 16ms 纔會觸發一次,而且自帶節流功能。requestAnimationFrame
回調IntersectionObserver
回調,該方法用於判斷元素是否可見,能夠用於懶加載上,可是兼容性很差requestIdleCallback
回調。以上內容來自於 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
,由於前者只會引發重繪,後者會引起迴流(改變了佈局)
把 DOM 離線後修改,好比:先把 DOM 給 display:none
(有一次 Reflow),而後你修改100次,而後再把它顯示出來
不要把 DOM 結點的屬性值放在一個循環裏當成循環裏的變量
for(let i = 0; i < 1000; i++) {
// 獲取 offsetTop 會致使迴流,由於須要去獲取正確的值
console.log(document.querySelector('.test').style.offsetTop)
}
複製代碼
不要使用 table 佈局,可能很小的一個小改動會形成整個 table 的從新佈局
動畫實現的速度的選擇,動畫速度越快,迴流次數越多,也能夠選擇使用 requestAnimationFrame
CSS 選擇符從右往左匹配查找,避免 DOM 深度過深
將頻繁運行的動畫變爲圖層,圖層可以阻止該節點回流影響別的元素。好比對於 video
標籤,瀏覽器會自動將該節點變爲圖層。