前端面试之前准备(Vue原理)

Vue原理

组件化和MVVM

数据驱动视图

  1. 传统组件,只是静态渲染,更新还要依赖操作DOM
  2. 数据驱动视图 - vue MVVM
  3. 数据驱动视图 - react setState() useState();

MVVM

MVVM架构
  1. Model(模型)
    • Model 代表应用的数据和业务逻辑。它负责存储和管理应用的状态。Model 可以包含从服务器获取的数据,或应用程序中需要操作的数据
    • Model 不关心 UI,它只处理数据和与数据相关的操作
  2. View(视图)
    • View 是应用的 UI 部分,负责显示数据(即 UI 展现)。它通常是由 HTML、CSS 等组成的界面。View 关注的是如何呈现数据。
    • View 被动地显示数据变化,但它不能直接修改数据
  3. ViewModel(视图模型)
    • ViewModel 是 View 和 Model 之间的桥梁,它负责将 Model 中的数据处理为 View 所需的格式,并且处理用户交互(如点击、输入等)。ViewModel 监听用户对 View 的操作,并更新 Model 数据,然后再将更新后的数据展示给 View
    • ViewModel 不直接操作 DOM,而是通过数据绑定(如 Vue 中的双向数据绑定)来驱动 UI 的更新

响应式原理

核心API

  1. Vue 2 主要采用 Object.defineProperty() 实现数据的劫持和依赖收集。
  2. Vue 3 使用 Proxy 替代了 Object.defineProperty()

响应式的核心机制

Vue2.0通过数据劫持(数据监听)+ 发布 - 订阅模式来实现响应式

  1. Observer(数据监听): 使用Object.defineProperty()劫持对象的属性,并在属性被读取或修改时触发回调
  2. Dep(依赖收集):用于管理依赖(订阅者),当数据变更,通知所有依赖更新
  3. Watcher(观察者): 作为Vue组件与数据的桥梁,当庭数据变化后,触发视图更新

响应式流程

  1. 数据初始化
  2. 依赖收集
  3. 视图更新

Object.definedProperty 缺点

  1. 深度监听,需要递归到底,一次性计算量大
  2. 无法监听新增刷新/删除属性(Vue.set Vue.delete)
    • Vue.set(obj, "name", "value");
    • Vue.delete(obj, key)
  3. 无法监听原生数组,通过重写数组方法(如 push、pop、splice)来监听数组变更,但不能监听数组索引的直接修改

vDom和diff

  1. vdom是实现vue和react的重要基石
  2. diff算法是vdom中最核心、最关键的部分

虚拟DOM(Virtual DOM)

Virtual DOM(虚拟 DOM) 是 Vue、React 等前端框架中用于优化 DOM 操作的一种技术。它的核心思想是用 JavaScript 对象模拟真实 DOM 结构,然后在数据变化时对比新旧 VDOM 生成最小的更新补丁,再应用到真实 DOM,从而提高渲染性能。

为什么要有vdom

  1. 频繁操作DOM开销大,每次操作都需要触发回流和重绘
  2. 数据驱动UI复杂: 手动管理DOM变化需要写大量的代码,容易出错,难为维护

优势

  1. 减少直接DOM操作,在js中先计算变更,再一次性更新DOM
  2. 优化渲染性能:使用diff算法计算最小的dom更新,提高页面性能
  3. 跨平台能力,vdom不依赖浏览器,react native等移动端框架也基于vdom构建

VDOM的实现

Vue2参考的是Snabbdom作为Virtual DOM库,它的实现主要包括:

  1. VNode(虚拟节点):用javascript对象表示DOM结构
    const vnode = {
        tag: "div",
        data: { id: "app", class: "container" },
        children: [
            { tag: "p", data: {}, text: "Hello Virtual DOM" }
        ]
    };
    
    相当于
    <div id="app" class="container">
        <p>Hello Virtual DOM</p>
    </div>
    
  2. Diff算法: 新旧VNode进行对比,计算最小修改
    • 同级比较:只比较同一层的节点,不进行夸层对比,减少计算量
    • Key机制: 通过key标识节点,提高Diff速度
    • 双端对比(双指针优化):减少无用遍历,优化列表更新
    • 四种节点变更情况:
      • 新增节点 -> 直接插入
      • 删除节点 -> 直接移除
      • 修改节点 -> 更新属性、文本
      • 移动节点 -> 调整顺序,减少dom操作
  3. Patch过程:把差异更新到真实DOM

通过snabbdom学习VDOM

  1. h函数
  2. vdom数据结构
  3. patch函数

VDOM总结

  1. 用js模拟DOM结构
  2. 新旧vnode对比,得出最小的更新范围,最后更新DOM
  3. 数据驱动视图的模式下,有效控制DOM操作

diff算法

概览

Diff(差异对比算法)用于比较新旧 Virtual DOM(VNode),找出最小变化,并将变更高效地应用到真实 DOM。

  1. 新旧VNode是否是同一个节点
  2. 对比属性和文本内容
  3. 对比子节点(双端对比 + key机制优化)

背景

  1. 树diff的时间 复杂度O(n^3)
    • 遍历tree1
    • 遍历tree2
    • 排序
  2. 优化时间复杂度到O(n)
    • 只比较统一层级,不跨级比较
    • tag不相同,则直接删除重建,不再深度比较
    • tag和key,两者都相同,则认为是相同节点,不再深度比较

diff算法详细过程

  1. 判断是否是同一节点
    • 相同:直接对比属性&子节点
    • 不相同:直接删除旧节点,创建新节点
    • 判断规则:
      • 标签相同
      • key相同
      • 都是文本节点
      const oldVNode = { tag: "p", text: "Hello" };
      const newVNode = { tag: "p", text: "Hello Vue" };
      
      // 只修改文本,不删除 p 标签
      patch(oldVNode, newVNode);
      
  2. 对比属性和文本
    • 属性不同 - 更新属性
    • 文本不同 - 更新textContent
    // 旧 VNode
    const oldVNode = { tag: "p", data: { class: "text" }, text: "Hello" };
    
    // 新 VNode
    const newVNode = { tag: "p", data: { class: "text updated" }, text: "Hello Vue" };
    
    // Vue Patch 过程:
    // 1. class 变更 → 仅修改 class
    // 2. 文本变更 → 仅修改 textContent
    patch(oldVNode, newVNode);
    
    最终更新:
    <p class="text">Hello</p>
    ↓
    <p class="text updated">Hello Vue</p>
    
  3. 对比子节点
    如果节点相同,vue递归比较chidren:
    • 旧节点有,新节点没有 -> 删除旧节点
    • 新节点有,旧节点没有 -> 直接新增
    • 新旧都有 -> 进行双端对比+key机制优化
  4. 双端diff优化
    • 从头开始比较(左侧)
    • 从尾部开始比较(右侧)
    • 移动节点:使用key识别相同节点,避免删除+重建

vdom和diff算法总结

  1. patchVNode
  2. addVNodes removeVNodes
  3. updateChildren(key的重要性)
  4. 细节不重要,updateChildren的过程不重要,不要深究
  5. vdom核心概念很重要:h、vnode、patch、diff、key
  6. vdom存在的价值更加重要:数据驱动视图,控制DOM

LIS算法

在 Vue 2 的 Diff 过程中,列表比较采用双端比较(双指针优化),但如果有大量元素位置变化,Vue 2 可能会执行多次插入和删除,导致不必要的 DOM 操作。

Vue 3 进一步优化了 Diff 算法,引入了 LIS(最长递增子序列,Longest Increasing Subsequence),减少 DOM 移动次数,使列表更新更高效。

LIS 是指在一个数组中,找到最长的递增子序列(不要求连续), Vue 3 的 Diff 算法中,LIS 用来找到可以复用的元素,避免不必要的移动。

输入: [10, 9, 2, 5, 3, 7, 101, 18]
最长递增子序列: [2, 3, 7, 18](长度 4)

vue3Diff过程

  1. 从前往后找到相同前缀(头部相同不变)
  2. 从后往前找到相同后缀(尾部相同不变)
  3. 对比中间不同部分
    • 查找最长递增子序列
    • 只移动非LIS部分
    • LIS部分直接复用,避免多余操作

vue2和vu3 diff更新对比

<!-- 旧列表 -->
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
  <li key="d">D</li>
</ul>

<!-- 新列表(调整顺序) -->
<ul>
  <li key="a">A</li>
  <li key="c">C</li>
  <li key="d">D</li>
  <li key="b">B</li>
</ul>
  1. vue2处理方式:
    • 发现 A 没变
    • 发现 B 位置变了(误认为B被删除,再新增到最后)
    • C 和 D 位置不变
    • 最终:删除B,创建B,导致额外的dom操作
  2. vue3处理方式
    • A 位置不变
    • C->D 不一定能够
    • B(需要调整)
    • 最终:只移动B,而不是先删除+新增

模版编译

js的with语法

with 语句用于扩展作用域链,让代码在一个对象的作用域内执行,避免重复书写对象名。

const person = {
  name: "Alice",
  age: 25,
  job: "Engineer"
};

with (person) {
  console.log(name);  // 直接访问 person.name
  console.log(age);   // 直接访问 person.age
  console.log(job);   // 直接访问 person.job
}

with 会影响作用域链的解析,导致代码难以阅读和调试,因此被认为是不安全的,在严格模式(strict mode)下被禁止。

vue template complier将模版编译为render函数

具体代码见vue原理 - 模版编译 - vue-template-compiler-demo

背景

  1. 模版不是html,有指令、插值、js表达式、能实现判断、循环
  2. html是标签语言,只有js才能实现判断、虚幻(图灵完备的)
  3. 因此,模版一定是转换为某种js代码,即编译模版

过程

  1. 解析,将vue模版解析为AST(抽象语法树),即一个树型结构数据表示
  2. 转换,在ast的基础上进行转换处理
    • 指令解析(v-if, v-for)
    • 表达式解析({{message}})
    • 静态优化(标记静态节点,提高更新性能)
  3. 代码生成,将AST转换为可执行的函数(render function),即h函数(vNode)
  4. 最终 render() 函数在组件渲染时执行,生成 Virtual DOM(虚拟 DOM),再通过 Diff 算法 更新真实 DOM。

总结

  1. 模版编译为render函数,执行render函数会返回vnode
  2. 基于vnode再执行patch和diff
  3. 使用webpack vue-loader 会在开发环境下编译模版(重要),属于优化手段

执行render函数生成vnode

vue组件中使用render代替了template

react一直都用render

组件更新渲染过程

组件是异步渲染的,汇总数据data修改,一次性更新
[图片上传失败...(image-441047-1741339501073)]

  1. 初次渲染过程
    • 创建vue实例
    • 解析template为render函数
    • 触发响应式,监听data属性的getter 和 setter
    • 执行render函数,生成vNode
    • patch(elem, newVNode),将vNode转换为真实的DOM并挂载到页面上:
  2. 更新过程
    • 修改data,触发setter(此前在getter中已被监听)
    • 重新生成render函数,生成新的vNode
    • patch(oldVNode, newVNode),通过diff,找到变更的节点,渲染到页面上

前端路由

前端路由的原理

前端路由的本质是在不刷新页面的情况下,更新URL并改变页面内容,他主要依赖History API 或 Hash模式来监听URL变化,并根据不同的url渲染组件

原因

  1. 传统的后端路由方式下,每次访问新的页面都会向服务器请求新的html,每次页面跳转都会重新加载,影响用户体验
  2. 前端路由让页面切换更快:
    • 避免整页刷新
    • 只更新部分视图
    • 更适合单页面应用

前端路由的两种模式

hash路由

  1. hash变化会触发网页跳转,即浏览器的前进,后退
  2. hash变化不会刷新页面,spa必需的特点
  3. hash永远不会提交到server端

history路由

  1. 用url规范的路由,但跳转是不刷新页面
  2. history.pushState
  3. window.onpopstate
  4. 需要服务端支持

如何选择

  1. to b系统推荐用hash,简单易用,对url规范不敏感
  2. to c系统,可以考虑h5 history,但需要服务端支持
  3. 能选择简单的,就别用复杂的,要考虑成本和收益
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容