Stories

Detail Return Return

Vue.js 第一章學習筆記 - Stories Detail

前言

最近在深入學習 Vue.js設計與實現 ,深感“理解設計理念”比“記憶API用法”更為重要。

我決定通過文章來記錄和輸出學習內容,這不僅是為了檢驗自己的理解程度,更是為了梳理Vue.js背後的核心思想和實現機制。希望這種“知其所以然”的探索,能為你帶來啓發。

1.1命令式和聲明式

首先什麼是聲明式和命令式,看接下來我們要做的事

 獲取 id 為 app 的 div 標籤 
 它的文本內容為 hello world 
 為其綁定點擊事件 
 當點擊時彈出提示:ok

命令式代碼:你如何去做,自己手動操作DOM,核心概念就是 關注過程

 const div = document.querySelector('#app') // 獲取 div 
 div.innerText = 'hello world' // 設置文本內容 
 div.addEventListener('click', () => { alert('ok') }) // 綁定點擊事件

聲明式代碼:你描述想要的結果,Vue自動幫你實現DOM操作,核心概念就是 關注結果

 <div @click="() => alert('ok')">hello world</div>

更通俗一點的説法:

  • 命令式代碼是廚師,你需要洗菜、切菜、炒菜,每個步驟都需要你來
  • 聲明式代碼是顧客,我只需要跟你説一聲我要吃什麼,接下來就交給你了

1.2 性能和可維護性的權衡

聲明式代碼好是好,但他會有更多的性能消耗,藉助書中的一句結論:聲明式代碼的性能不優於命令式代碼的性能

看上面的例子 要求:把div中的文本要求改成 hzh 真帥

命令式代碼:

div.textContent = 'hzh 真帥'

聲明式代碼:


<div @click="handleClick">{{ message }}</div>

const app = {
  data() {
    return {
      message: 'hello world'
    }
  },
  methods: {
    handleClick() {
      // 聲明式更新:只改變數據,Vue自動更新DOM
      this.message = 'hzh 真帥';
    }
  }
}

//當我們的this.message = 'hzh 真帥'的時候,vue自動做了這麼一步
div.textContent = 'hzh 真帥'

書中結論:

如果我們把直接修改的性能消耗定義為 A,把找出差異的性能消耗定義為 B,那麼有: <br/>
● 命令式代碼的更新性能消耗 = A <br/>
● 聲明式代碼的更新性能消耗 = B + A

那既然聲明式代碼會消耗更多的性能,那我們是不是要更多的使用命令式代碼?答案是不一定,直接上截圖

e9ed3196b63a37f238dee57093920ad4.png

其實對於大多數業務應用,聲明式的可維護性優勢遠大於其性能開銷。而且隨着框架優化技術的進步,這個性能差距正在不斷縮小。這也是為什麼vue,react這些聲明式框架會成為主流

1.3 虛擬DOM的性能到底如何

首先,虛擬DOM是什麼?先看看真實DOM是什麼

真實DOM

// 真實DOM - 瀏覽器中的實際節點
<div class="title" id="app">Hello World</div>

虛擬DOM

// 對應的虛擬DOM
const vnode = {
  type: 'div',
  props: {
    class: 'title',
    id: 'app'
  },
  children: 'Hello World'
}

可以看出,虛擬DOM就是一個js對象,為什麼要有虛擬DOM這個東西呢,回想之前的聲明式代碼,把找出差異的性能消耗定義為 B,虛擬DOM就是為了優化這個'找出差異(B)'的性能消耗(書中原話:虛擬DOM就是為了最小化找到差異這一步性能消耗而出現的)

DOM的更新方式以下有幾種

  1. 原生的js操作
  2. innerHTML
  3. 虛擬DOM

innerHTML更新需要觸發需要刪除以前的元素,在重新更新新的元素

innerHTML創建頁面的性能:HTML 字符串拼接的計算量 + innerHTML 的 DOM 計算量

<div id="list">
        <li>項目1</li>
        <li>項目2</li>
</div>

// 更新列表 - 重新渲染整個列表
const list = document.getElementById('list');
list.innerHTML = `
  <li>項目1</li>
  <li>項目2</li>
  <li>新項目3</li>  <!-- 只增加了這一項 -->
`;

虛擬DOM創建js對象,這個對象可以理解成真實DOM,並使用遞歸地遍歷虛擬 DOM 樹並創建真實 DOM

虛擬DOM創建頁面的性能:創建 JavaScript 對象的計算量 + 創建真實 DOM 的計算量

// 舊的虛擬DOM
const oldVNodes = [
  { type: 'li', children: '項目1' },
  { type: 'li', children: '項目2' }
];

// 新的虛擬DOM  
const newVNodes = [
  { type: 'li', children: '項目1' },
  { type: 'li', children: '項目2' },
  { type: 'li', children: '新項目3' }
];

image.png

那麼他兩的區別在哪呢?

innerHTML的更新過程:innerHTML的更新過程會銷燬所有現有DOM元素,然後重新創建新的DOM元素

大概意思是:

原本:list.innerHTML = `
  <li>項目1</li>
  <li>項目2</li>
`;
->
銷燬之後:list.innerHTML = ``
->
重新更新:list.innerHTML = `
  <li>項目1</li>
  <li>項目2</li>
  <li>新項目3</li>
`;

虛擬DOM的更新過程:javaScript對象+diff,只會更新更改的元素

// 虛擬DOM如何工作
const oldVNode = {
  type: 'ul',
  children: [
    { type: 'li', children: 'Item 1' },
    { type: 'li', children: 'Item 2' }
  ]
};

const newVNode = {
  type: 'ul', 
  children: [
    { type: 'li', children: 'Item 1' },
    { type: 'li', children: 'Updated Item 2' }, // 只有這裏變了
    { type: 'li', children: 'Item 3' } // 新增
  ]
};

// Diff算法發現:
// - 第一個li沒變,複用
// - 第二個li文本變了,只更新文本
// - 新增第三個li,創建新元素

image.png

虛擬DOM的優勢在於:無論頁面有多大,我只更新變化的地方,而innerHTML則是全部銷燬更新,這對性能消耗很大
image.png

書中原話:innerHTML、虛擬 DOM 以及原生 JavaScript 在更新頁面時的性能我們分了幾個維度:心智負擔可維護性性能

image.png

1.4編譯時和運行時

當設計一個框架的時候,我們有三種選擇:純運行時的、運行時 +編譯時的或純編譯時

運行時
假設一個框架,提供一個render函數,我們可以提供一個樹型結構的對象

const obj = { 
tag: 'div', 
children: [ 
{ tag: 'span', children: 'hello world' } 
] 
}

tag代表標籤名稱,children即可以是一個數組(代表子元素),也可以是一段文本內容,接下來實現一下render函數

function Render(obj, root) {
   const el = document.createElement(obj.tag)
   if (typeof obj.children === 'string') {
     const text = document.createTextNode(obj.children)
     el.appendChild(text)
   } else if (obj.children) {
     // 數組,遞歸調用 Render,使用 el 作為 root 參數
     obj.children.forEach((child) => Render(child, el))
   }

   // 將元素添加到 root
   root.appendChild(el)
 }

有了這個函數之後,我們可以這樣使用來渲染到body下

 Render(obj, document.body)

編譯時

有時我們覺得,直接寫一個樹型結構的對象好麻煩啊,我能不能獲取一下html標籤把他變成樹型結構的對象呢?,為此寫了一個Compiler函數,可以實現上述效果

const html = `
 <div>
   <span>hello world</span>
 </div>
 `
 // 調用 Compiler 編譯得到樹型結構的數據對象
 const obj = Compiler(html)

Compiler函數做的事:將html編譯成命令式代碼的過程

image.png

那麼這三種選擇分別有哪些優缺點呢?

運行時:

  • ✅ 無構建步驟:直接引入即可使用
  • ✅ 靈活性高:可以在運行時動態創建任何結構
  • ✅ 調試簡單:沒有編譯過程,代碼就是最終代碼
  • ❌ 性能較差:無法進行編譯時優化
  • ❌ 代碼冗長:需要手動創建DOM和綁定事件
  • ❌ 無語法糖:沒有模板、JSX等便捷語法

編譯時:

  • ✅ 性能極佳:編譯時完成所有優化,運行時幾乎零開銷
  • ✅ 包體積小:不需要包含運行時框架代碼
  • ✅ 無虛擬DOM:直接操作DOM,減少內存佔用
  • ❌ 靈活性差:難以在運行時動態創建複雜結構
  • ❌ 調試困難:編譯後代碼與源代碼差異大
  • ❌ 構建依賴:必須使用構建工具,無法直接運行

編譯時 + 運行時:

  • ✅ 開發體驗好:提供模板、JSX等友好語法
  • ✅ 性能優化:編譯時可以進行靜態分析優化
  • ✅ 靈活性:支持運行時動態創建
  • ✅ 漸進式:可以選擇使用編譯特性
  • ⚠️ 需要構建工具:需要配置webpack、vite等
  • ⚠️ 包體積較大:包含編譯器和運行時
  • ⚠️ 複雜度高:需要處理編譯和運行時的協調

總結

通過對比命令式與聲明式、分析虛擬DOM原理、瞭解編譯時與運行時的選擇,我們看到了Vue.js在性能與可維護性之間的智慧平衡。這種"知其所以然"的理解,能讓我們更好地使用和欣賞這個優秀的框架。

希望這篇文章對你有所啓發!

user avatar tianmiaogongzuoshi_5ca47d59bef41 Avatar Leesz Avatar alibabawenyujishu Avatar haoqidewukong Avatar smalike Avatar qingzhan Avatar kobe_fans_zxc Avatar chongdianqishi Avatar razyliang Avatar longlong688 Avatar banana_god Avatar huichangkudelingdai Avatar
Favorites 161 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.