從源碼解惑-$mount執行後,被掛載的節點最後是如何處理的

問題描述

當你用vue-cli建立一個工程後,會看到index-html文件裏有一個div,id叫appjavascript

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app">app</div>
    <!-- built files will be auto injected -->
  </body>
</html>
複製代碼

此時App.vue文件中也有一個div,id也叫apphtml

<template>
  <div id="app">
    hello world!
  </div>
</template>
複製代碼

可是根據規定,id應該具備惟一性,那麼問題來了,咱們在main.js裏面掛載的那個#app是哪一個?最終渲染到頁面上的那個#app又是哪個呢?vue

new Vue({
  router,
  render: (h: any) => h(App),
}).$mount('#app');
複製代碼

想要知道這些問題的答案,咱們能夠作一個簡單的實驗,好比把這兩個文件中的某一個id改掉,看一下頁面中最終渲染的是哪一個,看一下哪一個文件中的id改變會致使執行$mount('#app')報錯,能夠很輕鬆的得出結論。java

可是本着知其然,知其因此然的態度,咱們打開源碼驗證一下猜測node

驗證猜測

爲了方便查找到咱們要看的源碼,因此選擇在瀏覽器打斷點調試。那麼斷點打在哪裏比較合適呢,有兩種思路算法

  1. 將斷點打在app.js的new Vue()這行代碼,順着vue初始化的流程去追,追到頁面dom節點渲染出來時,就能夠往回找,經過打新的斷點不斷往下追,可是由於vue項目很是龐大,這種方法追蹤起來效率很低,很容易迷失。(別問我怎麼知道的)
  2. 第二種方法須要對vue源碼項目的目錄結構有必定了解,由於這種大型項目的文件分組都是有必定規律的。vue將全部dom更新的方法都放在了patch中,因此咱們打開patch.js。這裏須要作一個設想,聲明瞭兩個#app,最後渲染出來只有一個,那麼確定移除了另外一個。因此咱們要在這個文件中找一下有沒有關於移除dom的方法,果真找到了removeNode,因而咱們將斷點打在這個方法上,刷新頁面,果真進入了這個斷點,這也驗證了我剛纔的猜測
function removeNode (el) {
    const parent = nodeOps.parentNode(el)
    // element may have already been removed due to v-html / v-text
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)
    }
  }
複製代碼

咱們將鼠標懸浮在入參el上,能夠看到,要被移除的元素是index.html裏的#app,並且此時App組件中的內容已經渲染到了頁面上,因此另外一個#app很大機率是被append到body裏面的,若是想分析另外一個元素掛載過程,一樣能夠在patch中找到相應的方法打上斷點直接調試 vue-cli

remove node

經過調用堆棧,咱們追到了patch方法,這個方法就是大名鼎鼎的diff算法,能夠看到,在調用patch的地方,index中的#app原生dom節點做爲oldNode傳入,vnode則使用已經處理過的App中的#app虛擬dom瀏覽器

vnode
接下來,會把老的原生dom節點包裝成虛擬dom,而且建立一個和index的#app相鄰的姊妹元素,用來掛載新的虛擬dom

v1
能夠看到,在這個特殊的時間,頁面中會同時存在兩個#app
v2
至此,oldVnode就完成了使命,準備移除
v3

結論

講到這裏,結論基本上已經出來了,那就是在index.html和App.vue中存在兩個#app,通過$mount掛載後,最終渲染在body中的是App.vue中的那個#app,index.html中的#app則會被移除。app

引伸

不知道你們有沒有想過做者爲何要這麼設計呢?乍一看有點畫蛇添足的感受。我說一下個人想法。dom

  1. 咱們如今的spa項目越作越大,即便作了懶加載,可是首屏以然須要加載不少基礎環境地方js包,因此避免不了須要等待,那麼這個時候,若是咱們在index.html的#app裏面寫一個loading動畫,那麼在實際的App.vue內容沒有被渲染出來,咱們就能在頁面上看到一個loading狀態,可以顯著的提高用戶體驗。當App.vue中的內容渲染完後,以前的節點就會被移除,loading動畫天然也就消失了。
  2. 第二點其實仍是關於頁面等待體驗優化的地方,那就是骨架屏。如今的不少h5頁面都採用了骨架屏,無論方案如何,最終骨架屏在正式的內容加載出來後都應該本身消失的,那麼寫在這裏也很合適、
  3. 至於其餘的點,我暫時還沒想出來,歡迎你們評論補充

另外,有了完整的調用堆棧信息,想要深挖$mount過程就很容易了

相關文章
相關標籤/搜索