一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

[譯]No JQuery! 原生JavaScript操作DOM

 ipilipala 2017-04-10

翻譯自sitepoint的一篇文章,作者是Sebastian Seitz。雖然日常工作中很少再寫(xiě)原生js來(lái)操作DOM了,大家可能都在用主流的前端框架,我也是,但是看到這篇很淺顯易懂的文章,還是忍不住想細(xì)讀一下,複習(xí)的同時(shí)也會(huì)有新的發(fā)現(xiàn)。

無(wú)論何時(shí)我們需要操作DOM的時(shí)候,我們都會(huì)很快去用jQuery。然而,原生的JavaScript DOM API其實(shí)以它自己的方式已經(jīng)可以解決非常多的需求。因?yàn)?1以下的IE版本已經(jīng)被官方丟棄,我們可以沒(méi)有任何擔(dān)憂地使用它。

在這篇文章,我將展示如何用原生JavaScript來(lái)完成一些最普遍的DOM操作任務(wù),即:

  • 查找並修改DOM
  • 修改class和屬性
  • 事件監(jiān)聽(tīng)
  • 動(dòng)畫(huà)

我將在最後展示給各位,如何來(lái)創(chuàng)建一個(gè)可以用在任何項(xiàng)目裡的你自己的超精簡(jiǎn)DOM庫(kù)。與此同時(shí),各位可以學(xué)到用原生JS操作DOM其實(shí)並不難,很多jQuery的方法事實(shí)上都有對(duì)等的native API。

那麼我們開(kāi)始吧...

DOM操作:查找DOM

請(qǐng)注意:我不會(huì)詳細(xì)地講解原生DOM API的細(xì)節(jié),只是停留在表面。在用例裡,你可能會(huì)遇到我並沒(méi)有清楚介紹的方法。這時(shí)你可以參考Mozilla Developer Network。

可以用.querySelector()方法來(lái)查詢DOM。需要傳入任意的CSS選擇器作為參數(shù):

const myElement = document.querySelector('#foo > div.bar')

這行代碼返回第一個(gè)匹配的元素(深度優(yōu)先)。相反的,我們可以檢查一個(gè)元素是否匹配一個(gè)選擇器:

myElement.matches('div.bar') === true

如果我們想得到所有匹配元素,我們可以用:

const myElements = document.querySelectorAll('.bar')

如果我們已經(jīng)得到一個(gè)父元素的引用,我們可以只查找它的子元素,而不是整個(gè)document。像這樣縮小查找範(fàn)圍,我們可以簡(jiǎn)化選擇器提高查找性能。

const myChildElemet = myElement.querySelector('input[type="submit"]')

// Instead of
// document.querySelector('#foo > div.bar input[type="submit"]')

那麼我們?yōu)槭颤N還要用其他的不那麼方便的方法呢?比如.getElementsByTagName()?一個(gè)重要的區(qū)別是.querySelector()的結(jié)果不是實(shí)時(shí)的,所以當(dāng)我們動(dòng)態(tài)地添加一個(gè)匹配該選擇器的元素(參考第三部分)的時(shí)候,元素集合不會(huì)更新。

const elements1 = document.querySelectorAll('div')
const elements2 = document.getElementsByTagName('div')
const newElement = document.createElement('div')

document.body.appendChild(newElement)
elements1.length === elements2.length // false

另一個(gè)原因是這樣的實(shí)時(shí)的元素集合不需要預(yù)先獲得所有的元素信息,而.querySelectorAll()會(huì)立刻收集所有的信息到一個(gè)靜態(tài)的列表裡,因而會(huì)降低性能。

元素列表

關(guān)於.querySelectorAll()有兩個(gè)坑。一個(gè)是我們不能在結(jié)果集上調(diào)用Node方法從而獲得它的元素(像jQuery對(duì)象那樣用)。我們不得不明確地遍歷這些元素。另一個(gè)是返回的結(jié)果是一個(gè)NodeList,不是數(shù)組。也就是說(shuō)只能直接調(diào)用數(shù)組的方法。NodeList自己有一些數(shù)組方法的實(shí)現(xiàn),比如.forEach,但是任何版本的IE瀏覽器都不支持。所以我們必須先把它轉(zhuǎn)換成數(shù)組,或者從Array原型上「借用」那些方法。

// Using Array.from()
Array.from(myElements).forEach(doSomethingWithEachElement)

// Or prior to ES6
Array.prototype.forEach.call(myElements, doSomethingWithEachElement)

// Shorthand:
[].forEach.call(myElements, doSomethingWithEachElement)

每個(gè)元素都有一些非常語(yǔ)義化的只讀的屬性,都是實(shí)時(shí)更新的:

myElement.children
myElement.firstElementChild
myElement.lastElementChild
myElement.previousElementSibling
myElement.nextElementSibling

因?yàn)?a target="_blank">Element接口繼承自Node接口,它也有以下的屬性:

myElement.childNodes
myElement.firstChild
myElement.lastChild
myElement.previousSibling
myElement.nextSibling
myElement.parentNode
myElement.parentElement

前一組屬性的值只可以是元素節(jié)點(diǎn),而後一組屬性(除了.parentElement)的值可以是任何節(jié)點(diǎn),比如文本節(jié)點(diǎn)。我們可以像這樣檢查節(jié)點(diǎn)的類型:

myElement.firstChild.nodeType === 3 // this would be a text node

像任何對(duì)象那樣,我們可以用instanceof操作符檢查節(jié)點(diǎn)的原型鏈:

myElement.firstChild.nodeType instanceof Text

修改class和屬性

修改元素的class像下面的代碼這樣簡(jiǎn)單:

myElement.classList.add('foo')
myElement.classList.remove('bar')
myElement.classList.toggle('baz')

你可以在quick tip by Yaphi Berhanu讀到關(guān)於如何修改class的更深度的討論。元素屬性值可以像其他任何對(duì)象屬性一樣得到。

// Get an attribute value
const value = myElement.value

// Set an attribute as an element property
myElement.value = 'foo'

// Set multiple properties using Object.assign()
Object.assign(myElement, {
  value: 'foo',
  id: 'bar'
})

// Remove an attribute
myElement.value = null

注意還有.getAttibute(), .setAttribute().removeAttribute()這三個(gè)方法。這些方法直接修改的是元素的HTML屬性(與DOM屬性相對(duì)),因此會(huì)使瀏覽器重新渲染(你可以用你的瀏覽器自帶的開(kāi)發(fā)調(diào)試工具來(lái)檢查元素觀察它的變化)。瀏覽器重新渲染不僅比只是設(shè)置DOM屬性代價(jià)更高,而且還會(huì)產(chǎn)生不期望的後果。

作為一個(gè)小原則,除非你真的想對(duì)HTML「持久化」那些改變,你就只用上面的方法修改與DOM屬性不相關(guān)的HTML屬性(比如colspan)。(比如當(dāng)克隆一個(gè)元素或者修改它的父元素的.innerHTML的時(shí)候想保持這些改變,參考第三部分)

添加CSS樣式

CSS規(guī)則可以像其他屬性那樣設(shè)置。需要注意的是在JavaScript裡要寫(xiě)成駝峰形式:

myElement.style.marginLeft = '2em'

如果我們想獲得CSS規(guī)則的值,我們可以通過(guò).style屬性。然而,通過(guò)它只能拿到我們明確設(shè)置過(guò)的樣式。想拿到計(jì)算後的樣式值,我們可以用.window.getComputedStyle()。它可以拿到這個(gè)元素並返回一個(gè)CSSStyleDeclaration。這個(gè)返回值包括了這個(gè)元素自己的和繼承自父元素的全部樣式。

window.getComputedStyle(myElement).getPropertyValue('margin-left')

修改DOM

我們可以像下面這樣移動(dòng)元素:

// Append element1 as the last child of element2
element1.appendChild(element2)

// Insert element2 as child of element 1, right before element3
element1.insertBefore(element2, element3)

如果我們不想移動(dòng)元素,而是插入一個(gè)拷貝,我們可以這樣克隆它:

// Create a clone
const myElementClone = myElement.cloneNode()
myParentElement.appendChild(myElementClone)

.cloneNode()方法可選地接受一個(gè)boolean類型的參數(shù);如果傳入的是true, 將會(huì)創(chuàng)建一個(gè)深拷貝,也就是它的所有子元素也會(huì)被克隆。

當(dāng)然我們可以創(chuàng)建一個(gè)全新的元素或文本節(jié)點(diǎn):

const myNewElement = document.createElement('div')
const myNewTextNode = document.createTextNode('some text')

然後我們可以像上面展示的代碼那樣插入創(chuàng)建的元素。如果我們想刪除一個(gè)元素,我們不能直接刪除,而要採(cǎi)用從它的父元素刪除子元素的辦法來(lái)實(shí)現(xiàn),像這樣:

myParentElement.removeChild(myElement)

這給了我們一個(gè)優(yōu)雅的解決辦法,也就是可以通過(guò)它的父元素間接的刪除一個(gè)元素:

myElement.parentNode.removeChild(myElement)
元素屬性

每個(gè)元素都有.innerHTML.textContent(還有.innerText,跟.textContent類似,但是有一些重要的區(qū)別。它們分別表示HTML內(nèi)容和純文本內(nèi)容。它們是可寫(xiě)的屬性,也就是說(shuō)我們可以直接修改元素和它們的內(nèi)容:

// Replace the inner HTML
myElement.innerHTML = `
  <div>
    <h2>New content</h2>
    <p>beep boop beep boop</p>
  </div>
`

// Remove all child nodes
myElement.innerHTML = null

// Append to the inner HTML
myElement.innerHTML += `
  <a href="foo.html">continue reading...</a>
  <hr/>
`

像上面的代碼那樣向HTML添加標(biāo)記是通常是一個(gè)不好的注意,因?yàn)檫@樣是丟失之前對(duì)影響元素的屬性做的修改(除非我們把那些修改作為HTML屬性而保留下來(lái),參考第二部分)和已經(jīng)綁定的事件監(jiān)聽(tīng)。設(shè)置.innerHTML可以適合用在需要完全丟棄原來(lái)的而替換成新的標(biāo)記的場(chǎng)景,比如服務(wù)端渲染。所以添加元素這樣做比較好:

const link = document.createElement('a')
const text = document.createTextNode('continue reading...')
const hr = document.createElement('hr')

link.href = 'foo.html'
link.appendChild(text)
myElement.appendChild(link)
myElement.appendChild(hr)

但是這個(gè)辦法會(huì)引起兩次瀏覽器的重新渲染-每次添加元素都會(huì)渲染一次-而用設(shè)置.innerHTML的辦法的話只會(huì)重新渲染一次。我們可以先把所有的節(jié)點(diǎn)組合在一個(gè)DocumentFragment裡,然後把這一個(gè)片段添加到DOM裡,這樣可以解決這個(gè)性能問(wèn)題。

const fragment = document.createDocumentFragment()

fragment.appendChild(link)
fragment.appendChild(hr)
myElement.appendChild(fragment)

事件監(jiān)聽(tīng)

這可能是最知名的綁定事件監(jiān)聽(tīng)的方法:

myElement.onclick = function onclick (event) {
  console.log(event.type + ' got fired')
}

但是這是通常應(yīng)該避免採(cǎi)用的方法。這裡,.onclick是一個(gè)元素的屬性,也就是說(shuō)你可以修改它,但是你不能用它再綁定其他的監(jiān)聽(tīng)函數(shù)-你只能把新的函數(shù)賦給它,覆蓋掉舊函數(shù)的引用。

我們可以用更加強(qiáng)大的.addEventListener()方法來(lái)盡情地添加各種類型的各種事件的監(jiān)聽(tīng)器。它接受三個(gè)參數(shù):事件類型(比如click),一個(gè)無(wú)論何時(shí)在這個(gè)綁定元素上該事件發(fā)生都會(huì)觸發(fā)的函數(shù)(這個(gè)函數(shù)會(huì)得到一個(gè)事件對(duì)象傳進(jìn)去作為參數(shù))和一個(gè)可選的配置參數(shù),下面會(huì)更詳細(xì)的解釋。

myElement.addEventListener('click', function (event) {
  console.log(event.type + ' got fired')
})

myElement.addEventListener('click', function (event) {
  console.log(event.type + ' got fired again')
})

在監(jiān)聽(tīng)函數(shù)內(nèi)部,event.target指向這個(gè)事件觸發(fā)的元素(this也是,當(dāng)然除非你用的是箭頭函數(shù)。譯者註:如果監(jiān)聽(tīng)函數(shù)是箭頭函數(shù),裡面的this指向的是window對(duì)象,如果是普通的function函數(shù),裡面的this指向的跟event.target相同,都是該元素本身)。因此你可以輕鬆的拿到它的屬性:

// The `forms` property of the document is an array holding
// references to all forms
const myForm = document.forms[0]
const myInputElements = myForm.querySelectorAll('input')

Array.from(myInputElements).forEach(el => {
  el.addEventListener('change', function (event) {
    console.log(event.target.value)
  })
})
阻止默認(rèn)行為

注意在監(jiān)聽(tīng)函數(shù)內(nèi)部總是可以拿到event,但是當(dāng)需要的時(shí)候明確地傳入這個(gè)參數(shù)是一個(gè)好的實(shí)踐(當(dāng)然參數(shù)名稱可以隨意設(shè)置)(譯者註:即使沒(méi)有明確地給監(jiān)聽(tīng)函數(shù)傳入任何參數(shù),在內(nèi)部仍然可以拿到原生event對(duì)象,變量名就是event)。先不詳細(xì)解釋Event接口,一個(gè)特別需要注意的方法是.preventDefault()。它可以用來(lái)阻止瀏覽器的默認(rèn)行為,比如跳轉(zhuǎn)鏈接。另一個(gè)常見(jiàn)的應(yīng)用場(chǎng)景是當(dāng)前端的表單校驗(yàn)失敗的時(shí)候,可以根據(jù)判斷條件阻止表單提交。

myForm.addEventListener('submit', function (event) {
  const name = this.querySelector('#name')

  if (name.value === 'Donald Duck') {
    alert('You gotta be kidding!')
    event.preventDefault()
  }
})

另一個(gè)重要的事件方法是.stopPropagation(),它可以阻止事件冒泡。也就是說(shuō)在一個(gè)子元素上綁定了阻止事件冒泡的點(diǎn)擊事件監(jiān)聽(tīng)函數(shù),而在它的某一個(gè)父元素上也監(jiān)聽(tīng)了點(diǎn)擊事件,在子元素上觸發(fā)的點(diǎn)擊事件,不會(huì)觸發(fā)它的這個(gè)父元素的點(diǎn)擊事件監(jiān)聽(tīng)函數(shù)-否則,父子元素都會(huì)觸發(fā)。

現(xiàn)在我們看一下.addEventListener()的可選的配置對(duì)象這個(gè)第三個(gè)參數(shù),它可以有以下的布爾屬性(它們的默認(rèn)值都是false):

  • capture: 這個(gè)事件會(huì)先在父元素觸發(fā),然後再向下傳遞給它的子元素(關(guān)於事件捕獲和事件冒泡更詳細(xì)地解釋可以參考這裡
  • once: 你已經(jīng)猜到,這個(gè)屬性表示這個(gè)事件只會(huì)被觸發(fā)一次
  • passive: 它的意思是event.preventDefault()會(huì)被忽略(通常在控制臺(tái)都會(huì)打印一句警告)

最常用的選項(xiàng)是.capture;事實(shí)上,因?yàn)樗浅3S?,所以可以只傳入它的一個(gè)布爾值,而不必傳入整個(gè)配置對(duì)象:

myElement.addEventListener(type, listener, true)

事件監(jiān)聽(tīng)可以用.removeEventListener()方法刪除。它接受事件類型和回調(diào)函數(shù)的引用兩個(gè)參數(shù);例如,once選項(xiàng)也可以像這樣實(shí)現(xiàn):

myElement.addEventListener('change', function listener (event) {
  console.log(event.type + ' got triggered on ' + this)
  this.removeEventListener('change', listener)
})
事件委託

另一個(gè)有用的模式是事件委託:假如我們有一個(gè)表單,並且想給它的每一個(gè)input元素綁定一個(gè)change事件的監(jiān)聽(tīng)函數(shù)。一種方法是上面已經(jīng)介紹過(guò)的那樣用myForm.querySelectorAll('input')取到所有的input元素,然後再通過(guò)遍歷綁定事件。然而,我們其實(shí)只需要給表單本身綁定這個(gè)事件監(jiān)聽(tīng)函數(shù),然後檢查event.target是否是input元素就可以了。

myForm.addEventListener('change', function (event) {
  const target = event.target
  if (target.matches('input')) {
    console.log(target.value)
  }
})

用這種模式的另一個(gè)優(yōu)勢(shì)就是它對(duì)動(dòng)態(tài)插入的子元素同樣有效,而不需要給每一個(gè)綁定新的監(jiān)聽(tīng)函數(shù)。

動(dòng)畫(huà)

通常,最優(yōu)雅的生成動(dòng)畫(huà)的方式是結(jié)合transition屬性用CSS的類,或者用CSS的@keyframes。但是如果你需要更加靈活的方式(比如做遊戲),也可以用JavaScript。

簡(jiǎn)單的方法就是有一個(gè)window.setTimeout()函數(shù),不斷地調(diào)用自己直到期望的動(dòng)畫(huà)完成。然而,這會(huì)低效地強(qiáng)迫文檔進(jìn)行迅速的重排;並且結(jié)構(gòu)的抖動(dòng)會(huì)很快使頁(yè)面卡頓,特別是在移動(dòng)設(shè)備上。替代方案是,我們可以用window.requestAnimationFrame()同步頁(yè)面的更新,把當(dāng)前的所有改變安排到下一次瀏覽器重繪。它接受一個(gè)回調(diào)函數(shù)作為參數(shù)。這個(gè)回調(diào)函數(shù)會(huì)接收到當(dāng)前的時(shí)間戳作為參數(shù):

const start = window.performance.now()
const duration = 2000

window.requestAnimationFrame(function fadeIn (now)) {
  const progress = now - start
  myElement.style.opacity = progress / duration

  if (progress < duration) {
    window.requestAnimationFrame(fadeIn)
  }
}

用這個(gè)方法我們可以得到非常流暢的動(dòng)畫(huà)。想瞭解更加詳細(xì)的討論,可以參考Mark Brown寫(xiě)的這篇文章。

寫(xiě)你自己的幫助函數(shù)

確實(shí),與jQuery簡(jiǎn)潔的鏈?zhǔn)降?code>$('.foo').css({color: 'red'})表達(dá)式相比,總是要遍歷元素去做什麼可能是非常的繁瑣。所以為什麼我們不像下面這樣寫(xiě)我們自己的快捷的方法呢?

const $ = function $ (selector, context = document) {
const elements = Array.from(context.querySelectorAll(selector))

  return {
    elements,

    html (newHtml) {
      this.elements.forEach(element => {
        element.innerHTML = newHtml
      })

      return this
    },

    css (newCss) {
      this.elements.forEach(element => {
        Object.assign(element.style, newCss)
      })

      return this
    },

    on (event, handler, options) {
      this.elements.forEach(element => {
        element.addEventListener(event, handler, options)
      })

      return this
    }

    // etc.
  }
}

因此我們有了一個(gè)沒(méi)有向下兼容負(fù)擔(dān)的只有我們需要的方法的超簡(jiǎn)潔的DOM庫(kù)。儘管通常在元素的原型鏈上已經(jīng)有了那些方法。這裡有一個(gè)gist(更加詳細(xì)深入一些),它展示了一些實(shí)現(xiàn)這些幫助函數(shù)的辦法。我們還可以這樣保持簡(jiǎn)單:

const $ = (selector, context = document) => context.querySelector(selector)
const $$ = (selector, context = document) => context.querySelectorAll(selector)

const html = (nodeList, newHtml) => {
  Array.from(nodeList).forEach(element => {
    element.innerHTML = newHtml
  })
}

// And so on...

Demo

為使文章圓滿結(jié)束,下面的CodePen通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的燈箱效果展示了上面提到的很多概念。我鼓勵(lì)你們花點(diǎn)時(shí)間去看一下源碼,如果你們有任何想法或者疑問(wèn)請(qǐng)?jiān)谙旅嬖u(píng)論來(lái)讓我知道。
CodePen上的Demo代碼

結(jié)論

我希望我已經(jīng)證明了用原生JavaScript來(lái)操作DOM並不是什麼高科技,而且事實(shí)上,很多jQuery裡的方法在原生DOM的API裡有直接對(duì)應(yīng)的實(shí)現(xiàn)。這意味著在一些日常的應(yīng)用場(chǎng)景裡(比如導(dǎo)航菜單或者是跳出的模態(tài)框),額外的加載過(guò)重的DOM庫(kù)是不合適的。

雖然一部分原生API確實(shí)繁瑣或是不方便(比如必須總是要手動(dòng)遍歷節(jié)點(diǎn)列表),但是我們能夠非常輕鬆的把這些重複工作抽象出來(lái)寫(xiě)成我們自己的短小的幫助函數(shù)。

但是現(xiàn)在輪到你了。你怎麼看?你更願(yuàn)意在你可以的地方避免使用第三方庫(kù),還是使自己捲入根本不值得的認(rèn)知開(kāi)銷裡面?請(qǐng)?jiān)谙旅娴脑u(píng)論中讓我知道。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    国产韩国日本精品视频| 在线观看视频日韩成人| 又大又长又粗又黄国产| 中国一区二区三区不卡| 99福利一区二区视频| 五月激情婷婷丁香六月网| 国产不卡最新在线视频| 久久综合九色综合欧美| 欧美日韩国产精品第五页| 视频在线播放你懂的一区| 久久亚洲精品成人国产| 夜夜嗨激情五月天精品| 99久热只有精品视频最新| 日本深夜福利在线播放| 狠狠做深爱婷婷久久综合| 午夜福利大片亚洲一区| 深夜视频成人在线观看| 丰满的人妻一区二区三区| 免费黄色一区二区三区| 欧美日韩国产亚洲三级理论片| 亚洲av熟女一区二区三区蜜桃 | 国产一区二区熟女精品免费 | 欧美日韩国产欧美日韩| 亚洲一区二区三区三州| 国产在线视频好看不卡| 男女午夜在线免费观看视频| 国产不卡免费高清视频| 丰满人妻少妇精品一区二区三区| 丰满少妇高潮一区二区| 成人午夜视频精品一区| 沐浴偷拍一区二区视频| 国产又色又爽又黄的精品视频| 亚洲黑人精品一区二区欧美| 欧美黑人黄色一区二区| 欧美区一区二在线播放| 麻豆看片麻豆免费视频| 国产在线成人免费高清观看av| 国内外免费在线激情视频| 欧美日韩人妻中文一区二区| 国产亚洲成av人在线观看| 白丝美女被插入视频在线观看|