本文在github做了收錄 https://github.com/Michael-lz...
MutationObserver
MutationObserver 是一個可以監聽 DOM 結構變化的介面。當 DOM 物件樹發生任何變動時,MutationObserver 會得到通知。
API
MutationObserver 是一個構造器,接受一個 callback 引數,用來處理節點變化的回撥函式,返回兩個引數:
- mutations:節點變化記錄列表
(sequence<MutationRecord>)
- observer:構造 MutationObserver 物件。
MutationObserver 物件有三個方法,分別如下:
- observe:設定觀察目標,接受兩個引數,target:觀察目標,options:通過物件成員來設定觀察選項
- disconnect:阻止觀察者觀察任何改變
- takeRecords:清空記錄佇列並返回裡面的內容
//選擇一個需要觀察的節點
var targetNode = document.getElementById('root')
// 設定observer的配置選項
var config = { attributes: true, childList: true, subtree: true }
// 當節點發生變化時的需要執行的函式
var callback = function (mutationsList, observer) {
for (var mutation of mutationsList) {
if (mutation.type == 'childList') {
console.log('A child node has been added or removed.')
} else if (mutation.type == 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.')
}
}
}
// 建立一個observer示例與回撥函式相關聯
var observer = new MutationObserver(callback)
//使用配置檔案對目標節點進行觀測
observer.observe(targetNode, config)
// 停止觀測
observer.disconnect()
observe 方法中 options 引數有已下幾個選項:
- childList:設定 true,表示觀察目標子節點的變化,比如新增或者刪除目標子節點,不包括修改子節點以及子節點後代的變化
- attributes:設定 true,表示觀察目標屬性的改變
- characterData:設定 true,表示觀察目標資料的改變
- subtree:設定為 true,目標以及目標的後代改變都會觀察
- attributeOldValue:如果屬性為 true 或者省略,則相當於設定為 true,表示需要記錄改變前的目標屬性值,設定了 attributeOldValue 可以省略 attributes 設定
- characterDataOldValue:如果 characterData 為 true 或省略,則相當於設定為 true,表示需要記錄改變之前的目標資料,設定了 characterDataOldValue 可以省略 characterData 設定
- attributeFilter:如果不是所有的屬性改變都需要被觀察,並且 attributes 設定為 true 或者被忽略,那麼設定一個需要觀察的屬性本地名稱(不需要名稱空間)的列表
特點
MutationObserver 有以下特點:
- 它等待所有指令碼任務完成後才會執行,即採用非同步方式
- 它把 DOM 變動記錄封裝成一個陣列進行處理,而不是一條條地個別處理 DOM 變動。
- 它即可以觀察發生在 DOM 節點的所有變動,也可以觀察某一類變動
當 DOM 發生變動會觸發 MutationObserver 事件。但是,它與事件有一個本質不同:事件是同步觸發,也就是說 DOM 發生變動立刻會觸發相應的事件;MutationObserver 則是非同步觸發,DOM 發生變動以後,並不會馬上觸發,而是要等到當前所有 DOM 操作都結束後才觸發。
舉例來說,如果在文件中連續插入 1000 個段落(p 元素),會連續觸發 1000 個插入事件,執行每個事件的回撥函式,這很可能造成瀏覽器的卡頓;而 MutationObserver 完全不同,只在 1000 個段落都插入結束後才會觸發,而且只觸發一次,這樣較少了 DOM 的頻繁變動,大大有利於效能。
IntersectionObserver
網頁開發時,常常需要了解某個元素是否進入了"視口"(viewport),即使用者能不能看到它。
傳統的實現方法是,監聽到 scroll 事件後,呼叫目標元素的 getBoundingClientRect()方法,得到它對應於視口左上角的座標,再判斷是否在視口之內。這種方法的缺點是,由於 scroll 事件密集發生,計算量很大,容易造成效能問題。
目前有一個新的 IntersectionObserver API,可以自動"觀察"元素是否可見,Chrome 51+ 已經支援。由於可見(visible)的本質是,目標元素與視口產生一個交叉區,所以這個 API 叫做"交叉觀察器"。
API
IntersectionObserver 是瀏覽器原生提供的建構函式,接受兩個引數:callback 是可見性變化時的回撥函式,option 是配置物件(該引數可選)。
var io = new IntersectionObserver(callback, option)
// 開始觀察
io.observe(document.getElementById('example'))
// 停止觀察
io.unobserve(element)
// 關閉觀察器
io.disconnect()
如果要觀察多個節點,就要多次呼叫這個方法。
io.observe(elementA)
io.observe(elementB)
目標元素的可見性變化時,就會呼叫觀察器的回撥函式 callback。callback 一般會觸發兩次。一次是目標元素剛剛進入視口(開始可見),另一次是完全離開視口(開始不可見)。
var io = new IntersectionObserver((entries) => {
console.log(entries)
})
callback 函式的引數(entries)是一個陣列,每個成員都是一個 IntersectionObserverEntry 物件。舉例來說,如果同時有兩個被觀察的物件的可見性發生變化,entries 陣列就會有兩個成員。
- time:可見性發生變化的時間,是一個高精度時間戳,單位為毫秒
- target:被觀察的目標元素,是一個 DOM 節點物件
- isIntersecting: 目標是否可見
- rootBounds:根元素的矩形區域的資訊,getBoundingClientRect()方法的返回值,如果沒有根元素(即直接相對於視口滾動),則返回 null
- boundingClientRect:目標元素的矩形區域的資訊
- intersectionRect:目標元素與視口(或根元素)的交叉區域的資訊
- intersectionRatio:目標元素的可見比例,即 intersectionRect 佔 boundingClientRect 的比例,完全可見時為 1,完全不可見時小於等於 0
舉個例子
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style>
#div1 {
position: sticky;
top: 0;
height: 50px;
line-height: 50px;
text-align: center;
background: black;
color: #ffffff;
font-size: 18px;
}
</style>
</head>
<body>
<div id="div1">首頁</div>
<div style="height: 1000px;"></div>
<div id="div2" style="height: 100px; background: red;"></div>
<script>
var div2 = document.getElementById('div2')
let observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (element, index) {
console.log(element)
if (element.isIntersecting) {
div1.innerText = '我出來了'
} else {
div1.innerText = '首頁'
}
})
},
{
root: null,
threshold: [0, 1]
}
)
observer.observe(div2)
</script>
</body>
</html>
相比於 getBoundingClientRect,它的優點是不會引起重繪迴流。相容性如下
圖片懶載入
圖片懶載入的原理主要是判斷當前圖片是否到了可視區域這一核心邏輯實現的。這樣可以節省頻寬,提高網頁效能。傳統的突破懶載入是通過監聽 scroll 事件實現的,但是 scroll 事件會在很短的時間內觸發很多次,嚴重影響頁面效能。為提高頁面效能,我們可以使用 IntersectionObserver 來實現圖片懶載入。
const imgs = document.querySelectorAll('img[data-src]')
const config = {
rootMargin: '0px',
threshold: 0
}
let observer = new IntersectionObserver((entries, self) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let img = entry.target
let src = img.dataset.src
if (src) {
img.src = src
img.removeAttribute('data-src')
}
// 解除觀察
self.unobserve(entry.target)
}
})
}, config)
imgs.forEach((image) => {
observer.observe(image)
})
無限滾動
無限滾動(infinite scroll)的實現也很簡單。
var intersectionObserver = new IntersectionObserver(function (entries) {
// 如果不可見,就返回
if (entries[0].intersectionRatio <= 0) return
loadItems(10)
console.log('Loaded new items')
})
// 開始觀察
intersectionObserver.observe(document.querySelector('.scrollerFooter'))
getComputedStyle()
DOM2 Style 在 document.defaultView
上增加了 getComputedStyle()方法,該方法返回一個 CSSStyleDeclaration
物件(與 style 屬性的型別一樣),包含元素的計算樣式。
API
document.defaultView.getComputedStyle(element[,pseudo-element])
// or
window.getComputedStyle(element[,pseudo-element])
這個方法接收兩個引數:要取得計算樣式的元素和偽元素字串(如":after")。如果不需要查詢偽元素,則第二個引數可以傳 null。
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
#myDiv {
background-color: blue;
width: 100px;
height: 200px;
}
</style>
</head>
<body>
<div id="myDiv" style="background-color: red; border: 1px solid black"></div>
</body>
<script>
function getStyleByAttr(obj, name) {
return window.getComputedStyle ? window.getComputedStyle(obj, null)[name] : obj.currentStyle[name]
}
let node = document.getElementById('myDiv')
console.log(getStyleByAttr(node, 'backgroundColor'))
console.log(getStyleByAttr(node, 'width'))
console.log(getStyleByAttr(node, 'height'))
console.log(getStyleByAttr(node, 'border'))
</script>
</html>
和 style 的異同
getComputedStyle 和 element.style 的相同點就是二者返回的都是 CSSStyleDeclaration 物件。而不同點就是:
- element.style 讀取的只是元素的內聯樣式,即寫在元素的 style 屬性上的樣式;而 getComputedStyle 讀取的樣式是最終樣式,包括了內聯樣式、嵌入樣式和外部樣式。
- element.style 既支援讀也支援寫,我們通過 element.style 即可改寫元素的樣式。而 getComputedStyle 僅支援讀並不支援寫入。我們可以通過使用 getComputedStyle 讀取樣式,通過 element.style 修改樣式
getBoundingClientRect
getBoundingClientRect() 方法返回元素的大小及其相對於視口的位置。
API
let DOMRect = object.getBoundingClientRect()
它的返回值是一個 DOMRect 物件,這個物件是由該元素的 getClientRects() 方法返回的一組矩形的集合,就是該元素的 CSS 邊框大小。返回的結果是包含完整元素的最小矩形,並且擁有 left, top, right, bottom, x, y, width, 和 height 這幾個以畫素為單位的只讀屬性用於描述整個邊框。除了 width 和 height 以外的屬性是相對於檢視視窗的左上角來計算的。
應用場景
1、獲取 dom 元素相對於網頁左上角定位的距離
以前的寫法是通過 offsetParent 找到元素到定位父級元素,直至遞迴到頂級元素 body 或 html。
// 獲取dom元素相對於網頁左上角定位的距離
function offset(el) {
var top = 0
var left = 0
do {
top += el.offsetTop
left += el.offsetLeft
} while ((el = el.offsetParent)) // 存在相容性問題,需要相容
return {
top: top,
left: left
}
}
var odiv = document.getElementsByClassName('markdown-body')
offset(a[0]) // {top: 271, left: 136}
現在根據 getBoundingClientRect 這個 api,可以寫成這樣:
var positionX = this.getBoundingClientRect().left + document.documentElement.scrollLeft
var positionY = this.getBoundingClientRect().top + document.documentElement.scrollTop
2、判斷元素是否在可視區域內
function isElView(el) {
var top = el.getBoundingClientRect().top // 元素頂端到可見區域頂端的距離
var bottom = el.getBoundingClientRect().bottom // 元素底部端到可見區域頂端的距離
var se = document.documentElement.clientHeight // 瀏覽器可見區域高度。
if (top < se && bottom > 0) {
return true
} else if (top >= se || bottom <= 0) {
// 不可見
}
return false
}
requestAnimationFrame
window.requestAnimationFrame() 告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前呼叫指定的回撥函式更新動畫。
API
該方法需要傳入一個回撥函式作為引數,該回撥函式會在瀏覽器下一次重繪之前執行。
window.requestAnimationFrame(callback)
相容性處理
window._requestAnimationFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60)
}
)
})()
結束動畫
var globalID
function animate() {
// done(); 一直執行
globalID = requestAnimationFrame(animate) // Do something animate
}
globalID = requestAnimationFrame(animate) //開始
cancelAnimationFrame(globalID) //結束
與 setTimeout 相比,requestAnimationFrame 最大的優勢是由系統來決定回撥函式的執行時機。具體一點講,如果螢幕重新整理率是 60Hz,那麼回撥函式就每 16.7ms 被執行一次,如果重新整理率是 75Hz,那麼這個時間間隔就變成了 1000/75=13.3ms,換句話說就是,requestAnimationFrame 的步伐跟著系統的重新整理步伐走。它能保證回撥函式在螢幕每一次的重新整理間隔中只被執行一次,這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。這個 API 的呼叫很簡單,如下所示:
var progress = 0
//回撥函式
function render() {
progress += 1 //修改影像的位置
if (progress < 100) {
//在動畫沒有結束前,遞迴渲染
window.requestAnimationFrame(render)
}
}
//第一幀渲染
window.requestAnimationFrame(render)
優點:
- CPU 節能:使用 setTimeout 實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在後臺執行動畫任務,由於此時頁面處於不可見或不可用狀態,重新整理動畫是沒有意義的,完全是浪費 CPU 資源。而 requestAnimationFrame 則完全不同,當頁面處理未啟用的狀態下,該頁面的螢幕重新整理任務也會被系統暫停,因此跟著系統步伐走的 requestAnimationFrame 也會停止渲染,當頁面被啟用時,動畫就從上次停留的地方繼續執行,有效節省了 CPU 開銷。
- 函式節流:在高頻率事件(resize,scroll 等)中,為了防止在一個重新整理間隔內發生多次函式執行,使用 requestAnimationFrame 可保證每個重新整理間隔內,函式只被執行一次,這樣既能保證流暢性,也能更好的節省函式執行的開銷。一個重新整理間隔內函式執行多次時沒有意義的,因為顯示器每 16.7ms 重新整理一次,多次繪製並不會在螢幕上體現出來。
應用場景
1、監聽 scroll 函式
頁面滾動事件(scroll)的監聽函式,就很適合用這個 api,推遲到下一次重新渲染。
$(window).on('scroll', function () {
window.requestAnimationFrame(scrollHandler)
})
平滑滾動到頁面頂部
const scrollToTop = () => {
const c = document.documentElement.scrollTop || document.body.scrollTop
if (c > 0) {
window.requestAnimationFrame(scrollToTop)
window.scrollTo(0, c - c / 8)
}
}
scrollToTop()
2、大量資料渲染
比如對十萬條資料進行渲染,主要由以下幾種方法:
(1)使用定時器
//需要插入的容器
let ul = document.getElementById('container')
// 插入十萬條資料
let total = 100000
// 一次插入 20 條
let once = 20
//總頁數
let page = total / once
//每條記錄的索引
let index = 0
//迴圈載入資料
function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
//每頁多少條
let pageCount = Math.min(curTotal, once)
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loop(total, index)
(2)使用 requestAnimationFrame
//需要插入的容器
let ul = document.getElementById('container')
// 插入十萬條資料
let total = 100000
// 一次插入 20 條
let once = 20
//總頁數
let page = total / once
//每條記錄的索引
let index = 0
//迴圈載入資料
function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
//每頁多少條
let pageCount = Math.min(curTotal, once)
window.requestAnimationFrame(function () {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
監控卡頓方法
每秒中計算一次網頁的 FPS,獲得一列資料,然後分析。通俗地解釋就是,通過 requestAnimationFrame API 來定時執行一些 JS 程式碼,如果瀏覽器卡頓,無法很好地保證渲染的頻率,1s 中 frame 無法達到 60 幀,即可間接地反映瀏覽器的渲染幀率。
var lastTime = performance.now()
var frame = 0
var lastFameTime = performance.now()
var loop = function (time) {
var now = performance.now()
var fs = now - lastFameTime
lastFameTime = now
var fps = Math.round(1000 / fs)
frame++
if (now > 1000 + lastTime) {
var fps = Math.round((frame * 1000) / (now - lastTime))
frame = 0
lastTime = now
}
window.requestAnimationFrame(loop)
}
我們可以定義一些邊界值,比如連續出現 3 個低於 20 的 FPS 即可認為網頁存在卡頓。
推薦文章
你必須知道的webpack外掛原理分析
webpack的非同步載入原理及分包策略
總結18個webpack外掛,總會有你想要的!
搭建一個 vue-cli4+webpack 移動端框架(開箱即用)
從零構建到優化一個類似vue-cli的腳手架
封裝一個toast和dialog元件併發布到npm
從零開始構建一個webpack專案
總結幾個webpack打包優化的方法
總結vue知識體系之高階應用篇
總結vue知識體系之實用技巧
總結vue知識體系之基礎入門篇
總結移動端H5開發常用技巧(乾貨滿滿哦!)