【愣錘筆記】MVVM時代下仍需掌握的DOM - 基礎篇

愣錘發表於2019-05-05

在當前MVVM大行其道的環境下提到DOM一詞,很多人可能會感到有些詫異。這種差詫異或許來自於類似“都什麼年底了還操作DOM啊”的聲音!說的沒錯,MVVM時代,虛擬dom東征西戰,一枝獨秀,著實不可否認其強大的威力。

然而,DOM操作作為前端的基礎,自誕生以來便左右著我們的頁面效果。隨著JQuery十年戎馬生涯的落幕,DOM似乎暗淡了許多,但其在前端中的左右卻從未動搖。即使是MVVM框架下的例如ElementUl/IView等等最流行的ui庫,開啟他們的原始碼,依舊會有類似的dom.js/event.js等工具集(這裡暫且稱其工具函式集合吧),這些ui庫裡面,避免不了基本的事件繫結啊/新增移除類啊等等。

不管任何時候,DOM依舊是前端必須掌握且需要投入一定時間研究的基礎。不能只停留在jq事件的dom操作或者只是掌握那幾個最常見的api。

?下面開始有趣的DOM之旅吧!

【愣錘筆記】MVVM時代下仍需掌握的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屬性獲取節點的值,但是除了TextComment外,其餘節點基本都返回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()
複製程式碼

【愣錘筆記】MVVM時代下仍需掌握的DOM - 基礎篇

全域性物件

可以通過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.classNamea.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的那個元素。

  • offsetTopoffsetLeft是計算距其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
複製程式碼

【愣錘筆記】MVVM時代下仍需掌握的DOM - 基礎篇

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)
複製程式碼

【愣錘筆記】MVVM時代下仍需掌握的DOM - 基礎篇

參考內容:

百尺竿頭、日進一步。
我是愣錘,一名前端愛好者。

相關文章