在當前MVVM大行其道的環境下提到DOM一詞,不少人可能會感到有些詫異。這種差詫異或許來自於相似「都什麼年末了還操做DOM啊」的聲音!說的沒錯,MVVM時代,虛擬dom東征西戰,一枝獨秀,着實不能否認其強大的威力。css
然而,DOM操做做爲前端的基礎,自誕生以來便左右着咱們的頁面效果。隨着JQuery十年戎馬生涯的落幕,DOM彷佛暗淡了許多,但其在前端中的左右卻從未動搖。即便是MVVM框架下的例如ElementUl/IView等等最流行的ui庫,打開他們的源碼,依舊會有相似的dom.js/event.js等工具集(這裏暫且稱其工具函數集合吧),這些ui庫裏面,避免不了基本的事件綁定啊/添加移除類啊等等。html
無論任什麼時候候,DOM依舊是前端必須掌握且須要投入必定時間研究的基礎。不能只停留在jq事件的dom操做或者只是掌握那幾個最多見的api。前端
👇下面開始有趣的DOM之旅吧!node
DOM全稱Document Object Modal 文檔對象模型es6
Node是js的構造函數,全部節點都從Node上繼承最多見的屬性和方法,例如:api
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方法。bash
// 移除第二個p節點
p[1].parentNode.removeChild(p[1])
複製代碼
替換節點,先找到父節點,而後在父節點調用replaceChild方法,接收兩個參數,第一個爲新節點,第二個是待替換的節點
// 將第二個p節點替換成一個newdiv節點
p[1].parentNode.replaceChild(newdiv, p[1])
複製代碼
注意:這兩個方法會返回被移除或替換的節點。該操做只是將節點從文檔中移除,並非真正的刪除,其依舊存在於內存中,咱們仍能夠持有其引用。
// 克隆#app節點
app.cloneNode(false)
// 克隆#app及其子節點
app.cloneNode(true)
複製代碼
目前其默認值是false,可是DOM4規範其默認行爲發生了變化爲true。因此考慮到兼容,必須傳參數使用。
返回一個類數組包含全部直屬子節點(包括文本節點/註釋節點)
/// 返回#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也會包含該文本節點。須要注意現代化開發的壓縮代碼,因此後續更多的只使用元素節點。
普通節點
元素節點
調用節點的contains方法,能夠判斷該節點是否包含參數節點,包含則返回true,不然false:
// #app是否包含p[1]這個節點
app.contains(p[1])
複製代碼
具有如下條件,節點才相等:
<input type="text">
<input type="text">
var ipts = document.querySelectorAll('input')
ipts[0].isEqualNode(ipts[1])
// 若是隻是想判斷是不是同一個節點引用,則可使用全等運算符
ipts[0] === ipts[0]
複製代碼
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:
// 添加
a.classList.add('f')
// 移除
a.classList.remove('a')
// 有則移除,無則添加
a.classList.toggle('e')
a.classList.toggle('b')
// 監測是否有某個類名
a.classList.contains('c')
複製代碼
// 獲取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獲取元素相對於視口(可視區域)的各個距離,有以下值:
offsetWidth/offsetHeight
相等。var rect = app.getBoundingClientRect()
rect.bottom
rect.height
rect.left
rect.right
rect.top
rect.width
複製代碼
offsetWidth/offsetHeight
獲取元素寬高(border + 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屬性返回一個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')
複製代碼
// 批量設置多個內聯屬性
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文檔片斷能夠看做是一個空的文檔模板,行爲與實時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 // 頁面解析完成後觸發,資源不必定下載完成
複製代碼
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
複製代碼
阻止事件的默認行爲,例如a標籤的跳轉、輸入框的輸入等 。可是並不能阻止冒泡。
// 假設a是某個a元素
a.addEventListener('click', function (e) {
e.preventDefault()
}, false)
複製代碼
阻止事件冒泡,但不會阻止默認事件。
a.addEventListener('click', function (e) {
e.stopPropagation()
}, false)
複製代碼
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)
複製代碼
參考內容:
百尺竿頭、日進一步。 我是愣錘,一名前端愛好者。