Vue.js 第一章学习笔记

54 阅读7分钟

前言

最近在深入学习 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,那么有:
● 命令式代码的更新性能消耗 = A
● 声明式代码的更新性能消耗 = 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在性能与可维护性之间的智慧平衡。这种"知其所以然"的理解,能让我们更好地使用和欣赏这个优秀的框架。

希望这篇文章对你有所启发!