為什麼無法正確 appendChild !?
情況描述:Node.appendChild 的使用
Node.appendChild() 是我們在 JavsScripy 中操作 DOM 的時候經常會使用到的方法,特別是在我們使用 JS 建立一個 DOM Element 之後。
舉例來說,假設現在我們的 HTML 結構長這樣:
<div class="demo-1">
<div class="block block-1"></div>
<div class="block block-2"></div>
<div class="block block-3"></div>
</div>
這時候的畫面長這樣子:
假設我想要在每一個 .block 中都添加一個 .inner 的 div 時,我們直覺上可能會這樣做:
// STEP1: 利用 document.createElement 建立 DOM Element
let innerElement = document.createElement('div')
innerElement.classList.add('inner')
// STEP2: 選擇每一個 .blocks 並且 appendChild 上去
const blocks = document.querySelectorAll('.block')
blocks.forEach(block => {
block.appendChild(innerElement)
})
但這時候卻不會出現你預想的畫面,而是只有最後一個 .block 有添加到 .inner 這個 div,畫面會像這樣:
可是我們想要的畫面應該要是這樣:
到底為什麼會這樣呢?
你可能會猜想是 Array.prototype.forEach 的問題,於是我們試著一個一個 appendChild 上去:
// STEP1: 利用 document.createElement 建立 DOM Element
let innerElement = document.createElement('div')
innerElement.classList.add('inner')
// STEP2: 分別選擇各個 block
const block1 = document.querySelector('.block-1')
const block2 = document.querySelector('.block-2')
const block3 = document.querySelector('.block-3')
// STEP3-1: 先 appendChild 到 block1 上
block1.appendChild(innerElement)
看起來好像沒有太大的問題,如我們所料的,appendChild 到 .block1 這個 div 上了:
接著我們來對 .block2 做 appendChild()
// STEP3-2: appendChild 到 block2 上
block1.appendChild(innerElement)
block2.appendChild(innerElement)
不得了了,.block2 有加上 innerElement 了,但是 .block1 的 innerElement 卻不見了:
不死心的,我們在把 .block3 appendChild():
// STEP3-3: appendChild 到 block3 上
block1.appendChild(innerElement)
block2.appendChild(innerElement)
block3.appendChild(innerElement)
結果畫面變成和我們剛剛用 forEach 寫的狀況一樣,只有最後一個 .block3 有被 appendChild():
想必 appendChild() 是有蹊蹺!
使用 appendChild 要注意的小細節
為什麼會這樣呢?其實在使用 appendChild 時,有一個很需要留意的小細節,讓我們來看一下 MDN 怎麼說:
要留意的是 如果 appendChild 使用時,append 上去的是一個已存在的 node 時,它會做的是搬移,而非複製。
這是什麼意思呢?以剛剛的程式碼為例:
// 把 innerElement append 到 block1 上
block1.appendChild(innerElement)
// 這時候 innerElement 已經是存在的 Node 了,所會把這個 Node 進行"搬移",於是原本在 .block1 的 innerElement 被搬到 .block2
block2.appendChild(innerElement)
// 同理,原本在 .block2 的 innerElement 被搬到 .block3
block3.appendChild(innerElement)
重點:如果 appendChild 使用時,append 上去的是一個已存在的 node 時,它會做的是搬移,而非複製。
我們可以怎麼證明這一點呢?
我們可以寫一個按鈕,每點一次它就會依序 append 到 .block1, .block2, .block3 來看看變化:
<!-- pug -->
button(type="button" id="appendNode") 切換 appendChild
let i = 0
const buttonAppend = document.querySelector('#appendNode')
buttonAppend.addEventListener('click', function(){
console.log(i)
if (i === 0) {
block1.appendChild(innerElement)
} else if (i === 1) {
block2.appendChild(innerElement)
} else {
block3.appendChild(innerElement)
}
i = (i + 1) % 3 // i 會在 0 ~ 2 之間依序循環
})
操作的畫面會像下面這樣,你可以看到當我們把 innerElement appen 到 .block2 時,innerElement 就會從 .block1 被搬到 .block2,同理,也會從 .block2 搬移到 .block3:
使用 Node.cloneNode() 複製 Node Element
從剛剛的範例中,我們可以看到當我們使用 appendChild() 時,對於現存的 Node 它會採用搬移的方式,讓如果我們是想要複製一整個 element 呢?
在 MDN 中也提供的貼心的說明,告訴我們可以使用 Node.cloneNode() 這個方法:
Node.cloneNode() 的用法很簡單,在括弧中可以帶一個參數,true 的話表示深層複製(也是就不只複製 tag,還會複製裡面的內容),讓我們來試試看。可以看到這次 Node 不會是搬移,而是不斷的複製新的 Node:
重點:如果 appendChild 使用時要複製而非搬移,記得先使用 Node.cloneNode() 這個方法複製 Node Element。。
<!-- pug -->
button(type="button" id="cloneNode") 添加 cloneNode
在這裡我們多了一句 cloneElement = innerElement.cloneNode(true) 這樣就會真的複製這個 Node,然後在 appendChild() 進去,而不是搬移同一個 Node。
const buttonClone = document.querySelector('#cloneNode')
buttonClone.addEventListener('click', function () {
let cloneElement = innerElement.cloneNode(true)
if (i === 0) {
block1.appendChild(cloneElement)
} else if (i === 1) {
block2.appendChild(cloneElement)
} else {
block3.appendChild(cloneElement)
}
i = (i + 1) % 3
})
程式範例
appendChild 小細節 @ PJCHENder CodePen
參考資料
- Node.appendChild() @ WebAPIs MDN
- Node.cloneNode() @ WebAPIs MDN
0 意見:
張貼留言