跳至主要内容

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

為什麼無法正確 appendChild !?

情況描述:Node.appendChild 的使用

Node.appendChild() 是我們在 JavaScript 中操作 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>

這時候的畫面長這樣子:

img

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

// 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 上去:

// 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()

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

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

img

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

// 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 時,它會做的是++搬移++,而非複製

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

// 把 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 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

<!-- 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

參考資料