2017年6月20日

[JS小細節] Node Element 在 appendChild 後消失(disappear)!?

為什麼無法正確 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 中都添加一個 .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,畫面會像這樣:
可是我們想要的畫面應該要是這樣:
到底為什麼會這樣呢?
你可能會猜想是 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

參考資料

0 意見:

張貼留言