JavaScript進階(二十二):兄弟節點,DOM 操作,文檔碎片,DOM 操作爲什麼慢

實際上來說,我們平常真正在用 DOM 節點的時候,不止是父級子級,我們偶爾的時候,還會用到一個東西,叫做兄弟節點。

那什麼叫兄弟節點?

就比如,對於這個 aaa 的 span 來說,它前一個兄弟節點就是上面那一堆字。

 

兄弟節點很簡單,就 2 個東西:previousSibling 和 nextSibling。

Sibling 其實翻譯過來就是兄弟姐妹的意思。

previousSibling 就是上一個兄弟節點。

next 自然就是往下走,所以 nextSibling,就是下一個兄弟節點。

 

那這兩個東西怎麼用呢?我們來實驗一下。

現在我想打印出 s1 的上一個兄弟節點:

實際上來說,你會發現一個有意思的問題,它打印出來的是個 text:

很簡單,s1 的前一個兄弟節點,不就是一個 text 嗎,可不是前一個兄弟元素。

如果我們把空格去了之後,你就發現,就是 aaa 了:

但這時候有一個問題,我想要的是元素,那我如何能得到元素?

我們可以藉助另一個東西,叫做 previousElementSibling,它指的就是上一個兄弟元素。

當然,因爲我們平常用的時候,一般是不太會去操作文本節點,所以用 previousElementSibling 的更多。

但是這時候,又要說到另外一個問題,就是一個兼容的問題,previousElementSibling 這個東西,是隻有高級瀏覽器認的。

說白了,IE 6、7、8是不認的,那 IE 6、7、8 用什麼呢?

它用的是 previousSibling。那有人會說,這不是算文本的嗎?

在 IE 6、7、8 下,previousSibling 是不包括文本節點的,它只給你元素節點,所以這個東西還是比較好兼容的。

所以說如果你真的想要搞一個兼容的東西,我們一般情況下,可以這麼來寫:

當然 nextSibling 和 previousSibling 是完全相同的,所以我們就不用再去說一遍了。

然後到現在爲止,我們就又知道了一個東西,就是我也可以去使用它的兄弟節點。

 

那麼接下來,我們就準備來說另外一個事,關於 DOM 的操作問題。

所謂 DOM 操作,不管是操作元素還是節點,一般來講,我們有幾大類的操作:

1,創建,就是我憑空的去創建一個新的元素出來。

2,加入,就是我可以把它加入到頁面當中去。

3,刪除,就是我可以去刪除某一個元素,或者把它替換掉。

 

首先,我們最常見的,相信大家都用過的:document.createElement()

這個是我們平常最常見的一種,用來創建 DOM 元素的方式,就是創建一個元素。

比如我們來試試:

然後現在你就可以看到,頁面上有 4 個 span,多了一個 ddd,就這麼簡單。

 

然後除了能創建元素以外,還能創建別的東西。

因爲我們上篇博客也說了,除了 ElementNode,其實還有別的 Node。

比如我還可以創建一個文本節點:document.createTextNode(),既然是一個文本,那它的參數就不是標籤了,所以放點字就行了。

那麼我們現在也來做個試驗,試試看:

那麼這時候,你可以發現,頁面上就多了一堆字了。

 

然後再接下來,還有一些不是那麼常用的東西。其實所有的這些節點類型,其實理論上都是可以創建的。

那麼我能不能去創建一個註釋節點?也是可以的,document.createComment()

那麼它的參數是什麼?對於一個註釋來說,它需要內容。所以你可以寫一堆字,也可以空着,都沒問題。

我們也來試試:

然後你可以看到,多了一堆註釋,就這麼簡單。

當然我們不給,空着,也是可以的:

大家可能覺得,這個註釋節點看不見,也沒啥用啊?其實註釋節點有大用處。

我們往往在真正的操作當中,用到註釋節點的,就一種情況:創建一個佔位符,將來會把它替換成爲一個真正的節點。

比如我們 vue 裏的 v-if,它的原理就是這個。

 

然後,還有一種比較特殊的,到現在爲止,我們創造的所有東西,都是地球人,什麼意思呢?

元素、字、註釋,這些都是 HTML 裏面的東西。

實際上來說,你如果想的話,你還能夠創建其他標準之下的那個元素。說白了,就是你可以生個外星人出來。

我們還有一個東西,叫做 document.createElementNS()

這個 NS,是命名空間 namespace 的一個簡稱,意思就是說,我要創建一個其他命名空間當中的元素。

當然,這個東西大家可能會奇怪,幹嘛的,爲什麼要創建?很簡單,你比方說 SVG。

SVG 標籤其實並不是真正的 HTML,它只是能夠方便的寫在 HTML 內部而已,但它其實是另一套標準下的東西。

 

那麼到這爲止,我們就大概的知道了,如何去創建一個你需要的東西。

然後接下來,你除了創建以外,我們還有一個重要的步驟,你東西創建的再好,一點用沒有。爲什麼呀?

你必須得把它加到你的 DOM 結構當中,才能夠真正的起到作用。

所以我們一般情況下,有兩種插入的方法:

第一種是 appendChild,這個大家基本都用過,append 就是追加的意思,所以它的特點就是添加到末尾。

那我就要往前加怎麼辦?

所以它還有另外一種 insertBefore,這個東西從名字上就可以看出來,它是在某個東西之前去插入,插入到 xxx 之前。

 

appendChild 我們太常用了,上面的例子也看到了,它都是插到最後。

那麼接下來,我們就來看看能不能往前插:

這時候,我們不像 appendChild,appendChild 就一個參數,你就告訴它,你要插入的那個元素是誰就行了:appendChild(元素)

而 insertBefore,它有2個參數:

第一,你要插入誰。

第二,你要插到誰的前面。

insertBefore(元素,誰的前面)

所以現在,我們要插入一個新建的 span,比如要插入到 bbb 的前面:

那麼現在我們就可以看到,這個 555 就插到了 bbb 的前面了。

 

當然這時候大家可能又有個疑問,頁面上別人都空開了,怎麼他倆貼一塊了?

這個很簡單,因爲你這些東西之間,都有空格,空格就造成了中間間隔的部分。

新插進去的元素,它是不帶空格的,所以就造成了 555 和 bbb 這兩個東西是貼在一起了。

以及我們平常其實也不太依賴這個空格,我們一般還會想辦法去除這個空格,比如加個浮動之類的。

所以 insertBefore,可以幫助我們把一個元素插入到某個指定的位置,插到誰前面,就幹這個用的。

 

然後還有另外一些事,比如,我要如何刪除一個元素,以及我要如何替換一個元素。

removeChild 是專門用來刪東西的,你就告訴它,你要刪誰就行了:removeChild(元素)

 

當然, 它們前面都有一個必要的,你需要告訴它,從誰裏面去刪這個元素:父.removeChild(元素)

包括 appendChild 和 insertBefore,也都需要這個父級:

父.appendChild(元素)

父.insertBefore(元素,誰的前面)

 

以及還有一個,我們前面的博客中其實用過,叫做 replaceChild。

這個 replaceChild,如它的名字一樣,是專門用來替換掉某個子元素的,它需要 2 個參數:

第一,我要把哪個元素放進去。(第一個參數都是元素,你可以看到,幾乎是所有人的一個標準參數。)

第二,我要替換誰。

父.replaceChild(元素,替換誰)

 

那我們來試試:

那麼你可以看到,結果就是 555 把 bbb 給替換掉了。

 

到這爲止,我們說的其實都是一些特別簡單的基礎,那麼接下來,我們就來涉及到一個比較重要的東西。

當然理論上你可以完全不用,但是這個東西對我們操作的方便性,以及性能的優化,確實很明顯的。

是什麼呢?叫做文檔碎片 DocumentFragment。

文檔碎片這個東西,它是一個特別有歷史和年代感的東西,但是在之前的時候,這個文檔碎片幾乎沒人用。

事實上就是因爲沒什麼太多的場景去用它,但是在我們這,它卻有很大的作用。

我們先來看看它是個什麼。

 

首先我們來給它做個簡單的描述。

我們平常吃藥的時候,那個藥丸,一般表面上是不是都有一層膠囊,你喫下去,其實過一會,那層膠囊就化了。

然後這個文檔碎片,就有點類似於那個膠囊。

也就是說,等到我真正的把這個文檔碎片,插到頁面裏面去之後,這一層膠囊的殼就化了,它裏面的那個內容就都出來了。

說白了,如果我們創建一個 div,把一堆子元素都塞到 div 裏面去,然後再把這個 div 插入到頁面裏面去,這時候會有一個問題:這個頁面裏面就多了一層,因爲這個 div 是不會融化掉的。

而如果你用了文檔碎片,它最大的一個好處,就是這層就沒了,因爲它本來就是一個虛擬的東西。

 

那麼我們來試一試,首先,它是這麼寫的:

DocumentFragment 就是文檔碎片的意思,createDocumentFragment 就是創建一個文檔碎片。

那麼這時候,你其實就可以把它當做一個普通的元素來用。

比方說,它也有自己的 appendChild、insertBefore。removeChild 等等一系列的方法,這些東西一應俱全。

但是,它又不是一個真實的元素,所以有一些普通元素有的東西,它沒有。

比方說 DocumentFragment 有沒有 innerHTML?普通元素都有,它沒有。

 

那麼我們接着試試,我們創建幾個元素,然後都給它 appendChild,仍到 DocumentFragment 裏面去。

當然,到現在爲止什麼都不會出來。

原因很簡單,因爲 DocumentFragment 這個東西本身它是沒有進到頁面裏面去的。

所以我們就要把這個 DocumentFragment 給仍到 div 裏面去:

那麼這時候你可以看到,h2 和 ul 都是有的。

而且最神奇的,它就像是個膠囊一樣,你可以看到,加完了之後,那個 DocumentFragment 那一層並不存在。

如果我們平常是用一個 div 來操作這事的話,它裏面一定是多了一層,而且那一層是非常討厭的。

 

這裏順便一提,像我們平常用的一些框架,不論是 vue 還是 react,在我們這種框架裏面,我們經常會寫一個組件。

那個組件,一般它外面會有一層 template,或者別的什麼東西。

我們一般真正寫完了之後,你會發現 template 那層不見了。

爲什麼不見了?因爲它就是一個 DocumentFragment。

 

然後到這爲止,我們就算是對 DocumentFragment 有了一個最基本的認識。

就是說,它可以幫助我們來添加元素的時候,它自己不會多搞一層出來。

然後其次,它還有個好處,就是它對我們的性能是有優勢的。

 

既然說到性能這塊了,那麼我們就來說說 DOM 操作這個東西爲什麼慢。

實際上來說,像我們平常那種操作,比如 for 循環,if 之類的它比較快,那可能有人說它是在內存裏面進行操作的,所以比較快。那 DOM 也是在內存裏面操作的,它也不是飄在電腦外面的東西呀,那爲什麼都說 DOM 比較慢呢?

DOM 操作慢的第一個原因,是因爲臃腫。

就是說,假設有這麼一個人站出來,把所有這些 DOM 的實現,比方說 chrom 內核裏面的 DOM 實現,我給它全都推翻了,從零開始寫,那麼理論上來說,這個 DOM 的性能將得到極大的一個改進和提升。

說白了,就是現代這些瀏覽器,它們內核其實都有了幾十年的歷史,像 chrom,它本身歷史不長,但是它的內核是 webkit,webkit 是從 safari 年代就有的,都幾十年前的東西。

說的再直白一點,因爲這個 DOM 操作,它太過於複雜了,它裏面大量亂七八糟的東西,所以它這個東西,很多很多年以前就存在,一直延續到現在,一直都是修修補補,你可以想象它的體積是越滾越大,到最後就變的很臃腫,所以它本來不應該很慢,但這算是一個歷史的原因。

然後第二個原因,其實它還設計到一個圖形輸出的問題。

大家都知道,如果電腦的配置不夠,玩遊戲的時候就會特別卡,爲什麼?原因很簡單,因爲它對性能要求比較高,那爲什麼玩個遊戲要這麼高的性能?

因爲它有大量的圖形操作,地圖、背景、人物、裝備、特效、怪物等等,這些東西都是圖形。

而這種圖形操作本身其實就特別的消耗資源,而 DOM 操作本質上也是圖形操作,所以也會比較慢,這個其實就是重繪。

並且這裏面還有一個特殊的操作,叫做重排,那什麼叫重排呢?

比方說,我在前面插了一個 div,高度是 300 px,那底下所有人的位置都有可能多多少少受到影響,所有人都需要重新計算自己的位置在哪,自己佔多大地,等等這一系列的事,所以就會導致整個 DOM 樹發生一些更新和變化。

說白了,你任何一個元素產生變化,其實都會引起其他元素的位置、大小等等產生相應的變化,所以這個過程本身也很佔資源。

所以說,無論如何,DOM 操作它之所以慢,就是因爲:

第一,歷史遺留問題,一直修修補補,導致體積越來越大,越來越臃腫。

第二,它是屬於圖形化操作的一部分,本身就慢。

第三,DOM 操作,它本身內部就有一些很複雜的操作存在,所以會慢。

所以你經常可以聽到一個言論,就是能不用 DOM 操作就不用,能用虛擬 DOM 就別用真實 DOM。

 

那麼我們回到正題,DocumentFragment 爲什麼對我們的性能有優勢?

實際上來說,你每插入一個元素,就會引起我們這個 HTML 結構的一些變化,包括重排,重繪,各種各樣的事。

那麼你想象一下,如果你把 h2、ul 這幾個元素,不是插在 DocumentFragment 裏面,而是直接往 div 裏面插。

首先,這樣做行不行?

你可以看到,一樣可以解決問題。

但是你有沒有想過,我如果這麼做的話,雖然結果相同,但是我其實做了 2 次的重排,以及做了 2 次的 DOM 樹更新,等等一系列的事。

而用了 DocumentFragment 之後,它是最終把它作爲一個整體,一把全都塞進去,說白了,你甭管多少東西,反正進去了之後,只需要更新一遍就可以了,那這個時候性能是不是就會高上很多呀。

所以,文檔碎片的好處:

第一,不會多出一層。

第二,它的性能其實是比較高的,因爲它可以做到一點,把所有的更新、重排一次搞定。

 

那麼在這篇博客的最後,有個東西我們也順便提下。

首先,我們要知道,元素這個東西,如果你不去克隆它,如果你不去再創建一份的話,元素就只一個。

比如 div,就是一個 div,你不能直接就把它複製成 2 份,除非你用 cloneNode。

可能有人奇怪,你在說啥?我知道不能複製啊,說明什麼呢?

我們一起來看看就知道了,比如,現在我有 2 個 div,並且有 1 個 span 在 div1 裏面:

接下來你會看到一個比較好玩的現象。

比如,我現在去給 div2 去 appendChild 這個 span:

我們知道 appendChild 是添加。

那現在是不是 div1 和 div2 裏面都有了一個 span 呢?

但是你卻發現,原本在 div1 裏面的 span,直接跑下來了。

實際上來說,當你去把某一個元素,添加到一個新的父級的時候,它會自動的把這個元素跟原來的父級解除關係。

這事其實不奇怪,比方說你買房子的時候,能說這房子既屬於你,還屬於原來的房主嗎?不可能。說白了,在你買的過程當中,它就自動的跟原來那個人解除了所有關係,我們這也是一樣。

換句話說,當我 appendChild 某個東西的時候,它就會自動的進到一個新的父級下面,原來的父級就沒這東西了。

 

當然,這事還沒完,如果我們是把 span 添到 DocumentFragment 裏面呢?

比如,現在就一個 div,在我不添的情況下,這個 span 肯定是有的:

而當我添到這個 DocumentFragment 裏面去之後,你會發現,它也沒了:

所以說這個倒容易理解。

因爲我原來的父級是 div,而現在的父級是 DocumentFragment 文檔碎片。

所以,當我們去把一個元素添加一個新的父級下的時候,它會自動的跟原來那個父級解除關係,並且自動的進入到新的父級裏邊,那自然原來的父級也就沒這個東西了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章