在當前MVVM大行其道的環境下提到DOM一詞,很多人可能會感到有些詫異。這種差詫異或許來自於類似“都什麼年底了還操作DOM啊”的聲音!說的沒錯,MVVM時代,虛擬dom東征西戰,一枝獨秀,著實不可否認其強大的威力。
然而,DOM操作作為前端的基礎,自誕生以來便左右著我們的頁面效果。隨著JQuery十年戎馬生涯的落幕,DOM似乎暗淡了許多,但其在前端中的左右卻從未動搖。即使是MVVM框架下的例如ElementUl/IView等等最流行的ui庫,開啟他們的原始碼,依舊會有類似的dom.js/event.js等工具集(這裡暫且稱其工具函式集合吧),這些ui庫裡面,避免不了基本的事件繫結啊/新增移除類啊等等。
不管任何時候,DOM依舊是前端必須掌握且需要投入一定時間研究的基礎。不能只停留在jq事件的dom操作或者只是掌握那幾個最常見的api。
?下面開始有趣的DOM之旅吧!
DOM
DOM全稱Document Object Modal 文件物件模型
Node
Node是js的建構函式,所有節點都從Node上繼承最常見的屬性和方法,例如:
- childNodes/firstChild/nodeName/nodeType等
- appendChild()/cloneNode()/removeChild()等
- 其他更多
節點型別
document.nodeType // 文件節點,9
document.doctype.nodeType // 文件型別宣告節點,10
document.createElement('a').nodeType // 元素節點,1
document.createDocumentFragment().nodeType // 11
document.createTextNode('aaa').nodeType // 文字節點,3
複製程式碼
節點的值
可以通過節點的nodeValue
屬性獲取節點的值,但是除了Text
和Comment外
,其餘節點基本都返回null
建立節點
// 建立元素節點,例如div
document.createElement('div')
// 建立文字節點
document.createTextNode('a text')
// 建立註釋節點
document.createComment('a comment 節點')
複製程式碼
插入元素或文字
// 替換#app內部的內容
document.getElementById('app').innerHTML = '<div>asdasdasd</div>'
// 替換#app及其內容,本身也會被替換掉
document.getElementById('app').outerHTML = '<div>asdasdasd</div>'
// 建立一個文字節點,並替換#app內的內容
document.getElementById('app').textContent = 'a text'
---
上面這些方法,如果不是賦值,而是直接作為屬性取值,則會返回取到的節點字串
---
var app = document.getElementById('app')
// 在#app開始標籤之前插入,#app需要有父節點
app.insertAdjacentHTML('beforebegin', '<span>hello</span>')
// 在#app開始標籤之後插入
app.insertAdjacentHTML('beforeend', '<span>beforeEnd</span>')
// #app結束標籤之前插入
app.insertAdjacentHTML('afterbegin', '<span>afterbegin</span>')
// 在#app結束標籤之後插入,#app需要有父節點
app.insertAdjacentHTML('afterend', '<span>afterEnd</span>')
複製程式碼
插入節點
可以通過appendChild()
和insertBefore()
插入節點
// 插入節點
var div = document.createElement('div')
app.appendChild(div)
複製程式碼
insertBefore控制插入的位置,第一個引數是待插入節點,第二個引數是插入位置(即一個插入這個節點的前面,類似於一個參考節點)
// 將div節點插入到#app的第二個p節點的前面
app.insertBefore(div, p[1])
// 如果忽略第二個引數,則和appendChild一樣,預設插入到最後面
app.insertBefore(div)
複製程式碼
移除節點/替換節點
移除一個節點,首先要找到該節點的父節點,然後在父節點上呼叫removeChild方法。
// 移除第二個p節點
p[1].parentNode.removeChild(p[1])
複製程式碼
替換節點,先找到父節點,然後在父節點呼叫replaceChild方法,接收兩個引數,第一個為新節點,第二個是待替換的節點
// 將第二個p節點替換成一個newdiv節點
p[1].parentNode.replaceChild(newdiv, p[1])
複製程式碼
注意:這兩個方法會返回被移除或替換的節點。該操作只是將節點從文件中移除,並不是真正的刪除,其依舊存在於記憶體中,我們仍可以持有其引用。
克隆節點
- node.cloneNode()方法用來克隆節點,接收一個引數,如果為true則克隆該節點及其子節點,如果為false則只克隆該節點。
- 該方法會克隆節點的所有屬性和內聯事件,但是不會克隆addEventListener或node.onclick等形式新增的事件。
// 克隆#app節點
app.cloneNode(false)
// 克隆#app及其子節點
app.cloneNode(true)
複製程式碼
目前其預設值是false,但是DOM4規範其預設行為發生了變化為true。所以考慮到相容,必須傳引數使用。
childNodes
返回一個類陣列包含所有直屬子節點(包括文字節點/註釋節點)
/// 返回#app的所有直屬子節點
app.childNodes
// 驗證該節點集合是實時的,而不是某一時刻的快照
var ns = app.childNodes
app.innerHTML = ""
console.log(ns) // #app被清空後,雖然之前定義了引用,這裡依舊輸出了空陣列,因為是實時的。
// 可以借用陣列方法將類陣列轉換成陣列, es5:
Array.prototype.forEach.call(ns)
// es6中可以使用Array.from()
Array.from(ns).forEach(e => console.log(e))
複製程式碼
childNodes是實時的,而不是某一時刻的快照
html標籤的換行會有文字節點產生,所以childNodes也會包含該文字節點。需要注意現代化開發的壓縮程式碼,所以後續更多的只使用元素節點。
遍歷DOM節點
普通節點
- app.parentNode 父節點
- app.firstChild 第一個子節點
- app.lastChild 最後一個子節點
- app.nextSibling 上一個兄弟節點
- app.previousSibling 下一個兄弟節點
元素節點
- app.parentElement 父元素節點
- app.children 所有子元素節點
- app.firstElementChild 第一個子元素節點
- app.lastElementChild 最後一個子元素節點
- app.nextElementSibling 上一個元素節點
- app.previousElementSibling 下一個元素節點
判斷節點是否包含另一個節點
呼叫節點的contains方法,可以判斷該節點是否包含引數節點,包含則返回true,否則false:
// #app是否包含p[1]這個節點
app.contains(p[1])
複製程式碼
判斷節點是否相等
具備以下條件,節點才相等:
- 節點型別相等
- 這些屬性相等: nodeName/localName/namespaceURI/prefix/nodeValue
- attributes NameNodeMaps相等
- childNodes NodeLists相等 可以通過節點的isEqualNode方法判斷
<input type="text">
<input type="text">
var ipts = document.querySelectorAll('input')
ipts[0].isEqualNode(ipts[1])
// 如果只是想判斷是否是同一個節點引用,則可以使用全等運算子
ipts[0] === ipts[0]
複製程式碼
document下的節點
var doc = document
doc.doctype // 指向<!DOCTYPE>
doc.documentElement // 指向<html lang="en">
doc.head // 指向<head>
doc.body // 指向<body>
複製程式碼
獲取文件中聚焦/啟用狀態的元素引用
// 返回文件中聚焦或者啟用狀態的節點
document.activeElement
// 判斷文件是否有啟用或聚焦狀態的節點,返回true/false
document.hasFocus()
複製程式碼
全域性物件
可以通過document.defaultView
獲取頂部的物件(全域性物件),在瀏覽器中全域性物件是window,document.defaultView
指向的是這個值,在非瀏覽器環境則訪問到的是頂部物件的作用域。
元素節點
// 建立,接收一個引數,即元素型別tagName,元素節點的tagName和nodeName的一樣。
// 傳入的值在被建立元素前都會被轉換成小寫。
document.createElement('div')
// 獲取元素標籤名,返回的都是大寫
var div = document.createElement('div')
div.nodeName // DIV
div.tagName // DIV
複製程式碼
獲取元素屬性與值的集合
該屬性是實時的類陣列
doc.getElementById('txt').attributes
複製程式碼
操作元素的屬性節點
<a href="http://www.baidu.com" id="a" data-other="other prop">百度網</a>
var a = document.getElementById('a')
// 獲取屬性節點
a.getAttribute('href')
a.getAttribute('data-other')
// 設定屬性節點
a.setAttribute('data-src', 'src string')
// 移除屬性節點
a.removeAttribute('href')
// 監測元素是否含有某個屬性節點
a.hasAttribute('href')
複製程式碼
getAttribute如果沒取到則返回null
setAttribute必須傳2個引數
hasAttribute不管這個屬性有沒有值,都返回true
元素類名
可以通過a.className
或a.classList
獲取元素類名。
className:
- 如果沒有類名,返回""
- 類名會原樣返回字串,即使前後都有空格等
- 更改通過對其進行重新賦值
classList:
- ie9不支援
- 可以通過className模擬實現,有類似等profill庫
- 有add/remove/contains/toggle等方法
// 新增
a.classList.add('f')
// 移除
a.classList.remove('a')
// 有則移除,無則新增
a.classList.toggle('e')
a.classList.toggle('b')
// 監測是否有某個類名
a.classList.contains('c')
複製程式碼
data-屬性
// 獲取data-屬性:a.dataset.屬性名,不存在則返回undefined
a.dataset.other
// 設定
a.dataset-other2 = 'data2'
複製程式碼
dataset在ie9中不支援,不過完全可以依舊使用getAttribute等屬性使用
選擇器
// id選擇器
document.getElementById('app')
// 返回符合條件的首個元素節點
document.querySelector('#app')
// 返回符合條件的元素節點列表
document.querySelectorAll('li')
// 返回符合條件的標籤列表
document.getElementsByTagName('div')
// 返回符合條件類名的節點
document.getElementsByClassName('flex1')
複製程式碼
querySelectorAll、getElementsByTagName、getElementsByClassName都是實時的,而不是快照。
這些方法都可以作用在節點上,從而在上下文中進行區域性查詢。
// children: 查詢所有直接子元素
document.querySelector('ul').children
// html文件中方便使用的類陣列列表
document.forms // 獲取文件中所有的表單
document.images // 獲取文件中所有的圖片
document.links // 獲取文件中所有的a標籤
document.scripts // 獲取文件中所有的scripts
document.styleSheets // 獲取文件中所有的link和style
複製程式碼
元素偏移量
首先普及offsetParent概念:一個元素的祖元素中第一個position值不為static的那個元素。
offsetTop
與offsetLeft
是計算距其offsetParent
元素的頂部距離和左邊距離。(即距離祖元素中第一個position值不為static的祖元素的上邊距離和左邊距離)
getBoundingClientRect
getBoundingClientRect獲取元素相對於視口(可視區域)的各個距離,有如下值:
- left/right:元素左邊/右邊距視口左邊的距離
- top/bottom:元素上邊/下邊距視口上邊的距離
- width/height:元素的寬高(border+padding+content),該屬性和
offsetWidth/offsetHeight
相等。
var rect = app.getBoundingClientRect()
rect.bottom
rect.height
rect.left
rect.right
rect.top
rect.width
複製程式碼
元素尺寸
offsetWidth/offsetHeight
獲取元素寬高(border + padding + content)- clientWidth/clientHeight獲取元素寬高(padding + content)
元素滾動距離
// 獲取視窗的滾動距離
document.documentElement.scrollTop
document.body.scrollTop // ie
document.documentElement.scrollLeft
document.body.scrollLeft // ie
// 設定視窗的滾動位置
document.documentElement.scrollTop = 0
document.documentElement.scrollLeft = 0
document.body // ie
// 使某個元素滾動到可視區域
// 接收一個引數,true為滾動到可視區域頂部,false為滾動到可視區域底部。預設ture
document.querySelector('#app').scrollIntoView()
document.querySelector('#app').scrollIntoView(false)
複製程式碼
滾動元素的尺寸
如果一個元素設定為超出滾動後,那麼scrollHeight將獲取其滾動元素的尺寸,例如一個div寬高50,overflow: scroll;裡面有一個高度為1000px的p,那麼該div的scrollHeight尺寸為1000。
div.scrollHeight
div.scrollWidth
複製程式碼
style
元素的style屬性返回一個CSSStyleDeclaration物件,該物件包含元素的內聯樣式,而不是計算後的樣式,如果沒有給元素寫樣式,則通過該屬性獲取的值是空置。
var domStyle = document.querySelector('#app').style
// 獲取高度,寬度
style.height
style.width
// 連字元的屬性需要使用駝峰命名法
domStyle.fontSize
// 對於暴露字屬性在前面加上css
domStyle.cssFloat // domStyle.float 谷歌上測試也可以
複製程式碼
style獲取的是內聯的屬性,如果是寫在樣式表中的屬性,是獲取不到的。
獲取的是實際的內聯屬性,而不是計算後的值。即使樣式表中通過important等方式使得權重高於內聯的,獲取到的依舊是內聯樣式中寫的值。 獲取的顏色值是rgb
的
style物件獲取/設定/移除的其他方法
// 設定屬性,不能寫複合屬性,例如background/margin,而是分開的寫法:background-color/margin-left等
// 用-分割的寫法,而不是駝峰
dom.setProperty('background-color', '#f00')
domStyle.setProperty('background-color', '#f00')
// 獲取
domStyle.getPropertyValue(屬性名)
domStyle.getPropertyValue('background-color')
// 移除
domStyle.removeProperty('background-color')
複製程式碼
style物件設定/獲取/移除多個內聯屬性
// 批量設定多個內聯屬性
domStyle.cssText = 'background-color: #000;color: 20px;margin: 30px;'
// 移除全部內聯屬性
domStyle.cssText = ''
// 獲取style屬性的內聯屬性
domStyle.cssText
// 通過setAttribute/getAttibute/removeAttribute也是可以實現相同的效果
dom.setAttribute('style', 'background-color: #000;color: #f1f1f1; 20px;margin: 30px;')
dom.getAttribute('style')
dom.removeAttribute('style')
複製程式碼
獲取計算後的屬性
var winStyle = window.getComputedStyle(dom)
winStyle.color
winStyle.border
winStyle.backgroundColor // 獲取的是rgb顏色格式
winStyle.marginTop // 不能獲取簡寫的格式,例如margin
複製程式碼
返回的顏色格式是rgb的格式,背景色返回的是rgba
不能獲取簡寫的屬性,例如margin/padding,而是marginTop
修改樣式的最佳實踐
更多的我們會通過給元素新增/移除某個class/id方式,來新增修改樣式
DocumentFragment文件片段
DocumentFragment文件片段可以看作是一個空的文件模板,行為與實時DOM樹類似,但是僅在記憶體中存在,可以附加到實時DOM中。
// 建立
document.createDocumentFragment()
// 例如:
var lis = ['hello! ', 'Every', 'bady'];
var fragment = document.createDocumentFragment();
lis.forEach(e => {
var liElem = document.createElement('li');
liElem.textContent = e;
fragment.appendChild(liElem)
})
dom.appendChild(fragment)
// 文件片段插入到dom後,自身的節點內容就沒了。例如上面的例子:
dom.appendChild(fragment) // 第一次將文件片段的內容插入到dom後
dom.appendChild(fragment) // 執行相同的操作,並不會插入了,因為此時的文件片段內容沒了。
// 為了文件片段的內容可以多次利用,可以利用克隆的方式
dom.appendChild(fragment.cloneNode(true))
複製程式碼
繫結事件
// 內聯事件,基本不用
<div onclick="alert('a')"></div>
// 屬性事件(DOM 0 級事件)
window.onload = function () {}
// 繫結事件(DOM 2 級事件),ps:沒有1級事件
window.addEventListener('scroll', (e) => {
console.log(e)
}, false)
複製程式碼
易混事件區分
常見的click/onload/scroll/resize等事件就不介紹了。
// 滑鼠按下,都是在輸入法接收到鍵值之前
keydown // 任何按鍵按下都會觸發,不管他是否產生字元碼
keypress // 只有實際產生字元碼才會觸發,例如command鍵/option鍵/shift鍵等並不會觸發
// 滑鼠滑入
mouseenter // 滑鼠滑入元素及其子元素時觸發,不冒泡
mouseover // 滑鼠滑入某個元素時觸發,會冒泡
// 頁面展示
window.onpageshow = function () {} // 展示頁面時,觸發
window.onload = function () {} // 頁面載入完成後觸發
// 兩者的區別在於,從瀏覽器快取讀取的頁面,並不會觸發load事件,例如操作瀏覽記錄的前進後退時
// 其他
offline // 離線時觸發
online // 線上時觸發
message // 跨文件傳遞時觸發
hashchange // url中hash值的變化時觸發
DOMContentLoaded // 頁面解析完成後觸發,資源不一定下載完成
複製程式碼
事件中的this/target/currentTarget
document.body.addEventListener('click', function (e) {
console.log(this)
console.log(e.currentTarget)
console.log(e.target)
console.log(this === e.target, this === e.currentTarget)
}, false)
// this
this指的是該事件繫結的元素或物件,這裡指向body
// currentTarget
指的是該事件繫結的元素或物件,這裡指向body,同this
// target
指的是事件的目標,可以理解為開始觸發冒泡時的那個元素,或者說是滑鼠點選的巢狀在最裡面的那個元素。
這裡指向div
複製程式碼
preventDefault
阻止事件的預設行為,例如a標籤的跳轉、輸入框的輸入等 。但是並不能阻止冒泡。
// 假設a是某個a元素
a.addEventListener('click', function (e) {
e.preventDefault()
}, false)
複製程式碼
stopPropagation
阻止事件冒泡,但不會阻止預設事件。
a.addEventListener('click', function (e) {
e.stopPropagation()
}, false)
複製程式碼
stopImmediatePropagation
stopImmediatePropagation方法不僅會阻止事件冒泡,還會阻止該元素在呼叫該方法後面的繫結事件的觸發
app.addEventListener('click', function () {
console.log('app first')
}, false)
app.addEventListener('click', function (e) {
console.log('app second, 阻止app後面繫結的click事件的冒泡')
e.stopImmediatePropagation()
}, false)
// 此次的app事件繫結不會觸發,因為已經被上面的stopImmediatePropagation方法阻止掉了
app.addEventListener('click', function (e) {
console.log('app third')
}, false)
document.body.addEventListener('click', function () {
console.log('body click')
}, false)
// 最終輸出如下:
// app first
// style.html:98 app second, 阻止app後面繫結的click事件的冒泡
複製程式碼
自定義事件
// 自定義事件
var cusEvent = document.createEvent('CustomEvent');
// 配置自定義事件的詳情
cusEvent.initCustomEvent('myNewEvent', true, true, {
myNewEvent: 'hello this is my new custom event!'
})
// 給#app繫結我們自定義的事件
var app = document.querySelector('#app');
app.addEventListener('myNewEvent', function (e) {
console.log(e.detail.myNewEvent)
}, false)
// 在app上觸發自定義事件
app.dispatchEvent(cusEvent)
複製程式碼
initCustomEvent接收四個引數:事件名稱,是否冒泡,是否可以取消事件,傳遞給event.detail的值
事件委託
事件委託利用事件流來完成,給父級繫結事件,然後判斷觸發事件的target,執行對應的事件。
例如:給表箇中的td新增事件。
var tableBox = document.querySelector('#table-box');
tableBox.addEventListener('click', function (e) {
var target = e.target
if (target.tagName.toLowerCase() === 'td') {
console.log(target.textContent)
}
}, false)
複製程式碼
參考內容:
- 文章參考《dom啟蒙》一書
- MDN資料文件
百尺竿頭、日進一步。
我是愣錘,一名前端愛好者。