PJCHENder 未整理筆記

[JS] Node Element 在 appendChild 後消失(disappear)!

2017-09-26

[JS] Node Element 在 appendChild 後消失(disappear)

@(Javascript)[ES6, JavaScript, webAPIs]

為什麼無法正確 appendChild !?

情況描述:Node.appendChild 的使用

Node.appendChild() 是我們在 JavaScript 中操作 DOM 的時候經常會使用到的方法,特別是在我們使用 JS 建立一個 DOM Element 之後。

舉例來說,假設現在我們的 HTML 結構長這樣:

1
2
3
4
5
<div class="demo-1">
<div class="block block-1"></div>
<div class="block block-2"></div>
<div class="block block-3"></div>
</div>

這時候的畫面長這樣子:

img

假設我想要在每一個 .block 中都添加一個 .innerdiv 時,我們直覺上可能會這樣做:

1
2
3
4
5
6
7
8
9
// 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,畫面會像這樣:

img

可是我們想要的畫面應該要是這樣:

img

到底為什麼會這樣呢?

你可能會猜想是 Array.prototype.forEach 的問題,於是我們試著一個一個 appendChild 上去:

1
2
3
4
5
6
7
8
9
10
11
// 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 上了:

img

接著我們來對 .block2 做 appendChild()

1
2
3
// STEP3-2: appendChild 到 block2 上
block1.appendChild(innerElement)
block2.appendChild(innerElement)

不得了了,.block2 有加上 innerElement 了,但是 .block1 的 innerElement 卻不見了:

img

不死心的,我們在把 .block3 appendChild():

1
2
3
4
// STEP3-3: appendChild 到 block3 上
block1.appendChild(innerElement)
block2.appendChild(innerElement)
block3.appendChild(innerElement)

結果畫面變成和我們剛剛用 forEach 寫的狀況一樣,只有最後一個 .block3 有被 appendChild():

img

想必 appendChild() 是有蹊蹺!

使用 appendChild 要注意的小細節

為什麼會這樣呢?其實在使用 appendChild 時,有一個很需要留意的小細節,讓我們來看一下 MDN 怎麼說:

img

要留意的是 如果 appendChild 使用時,append 上去的是一個已存在的 node 時,它會做的是++搬移++,而非複製

這是什麼意思呢?以剛剛的程式碼為例:

1
2
3
4
5
6
7
8
// 把 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 來看看變化:

1
2
<!-- pug -->
button(type="button" id="appendNode") 切換 appendChild
1
2
3
4
5
6
7
8
9
10
11
12
13
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 append 到 .block2 時,innerElement 就會從 .block1 被搬到 .block2,同理,也會從 .block2 搬移到 .block3:

img

使用 Node.cloneNode() 複製 Node Element

從剛剛的範例中,我們可以看到當我們使用 appendChild() 時,對於現存的 Node 它會採用搬移的方式,讓如果我們是想要複製一整個 element 呢?

在 MDN 中也提供的貼心的說明,告訴我們可以使用 Node.cloneNode() 這個方法:

img

Node.cloneNode() 的用法很簡單,在括弧中可以帶一個參數,true 的話表示深層複製(也是就不只複製 tag,還會複製裡面的內容),讓我們來試試看。可以看到這次 Node 不會是搬移,而是不斷的複製新的 Node:

img

1
2
<!-- pug -->
button(type="button" id="cloneNode") 添加 cloneNode

在這裡我們多了一句 cloneElement = innerElement.cloneNode(true) 這樣就會真的複製這個 Node,然後在 appendChild() 進去,而不是搬移同一個 Node。

1
2
3
4
5
6
7
8
9
10
11
12
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

參考資料

掃描二維條碼,分享此文章