【愣錘筆記】MVVM時代下仍需掌握的DOM - 基礎篇
在當前MVVM大行其道的環境下提到DOM一詞,很多人可能會感到有些詫異。這種差詫異或許來自於類似“都什麼年底了還操作DOM啊”的聲音!說的沒錯,MVVM時代,虛擬dom東征西戰,一枝獨秀,著實不可否認其強大的威力。
然而,DOM操作作為前端的基礎,自誕生以來便左右著我們的頁面效果。隨著JQuery十年戎馬生涯的落幕,DOM似乎暗淡了許多,但其在前端中的左右卻從未動搖。即使是MVVM框架下的例如ElementUl/IView等等最流行的ui庫,開啟他們的原始碼,依舊會有類似的dom.js/event.js等工具集(這裡暫且稱其工具函式集合吧),這些ui庫裡面,避免不了基本的事件繫結啊/新增移除類啊等等。
不管任何時候,DOM依舊是前端必須掌握且需要投入一定時間研究的基礎。不能只停留在jq事件的dom操作或者只是掌握那幾個最常見的api。
:point_down:下面開始有趣的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獲取元素相對於視口(可視區域)的各個距離,有如下值:
offsetWidth/offsetHeight
var rect = app.getBoundingClientRect() rect.bottom rect.height rect.left rect.right rect.top rect.width 複製程式碼
元素尺寸
offsetWidth/offsetHeight
元素滾動距離
// 獲取視窗的滾動距離 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資料文件
百尺竿頭、日近一步。
我是愣錘,一名前端愛好者。