深入理解 Vue 的 MVVM 架构与响应式原理

Hi,我是石小石~


现代前端开发中,Vue 已成为最受欢迎的框架之一。其核心设计理念与 MVVM(Model-View-ViewModel) 架构密不可分。理解 MVVM 及 Vue 的响应式系统,可以帮助我们更高效地构建和优化前端应用。

本文将从 Vue 的底层机制与核心原理出发,深入解析 MVVM 架构及其响应式系统的实现原理。

MVVM 架构概述

MVVM(Model-View-ViewModel) 是一种用于构建用户界面的架构模式,用于现代的前端开发框架(Vue、Angular)。它通过 数据绑定视图模型 提供了高效的 UI 更新和数据同步机制。

MVVM 模式主要由Model(模型)、View(视图)、ViewModel(视图模型)三个部分组成。

  • Model表示程序的核心数据和业务逻辑,它不关心用户界面,只负责数据的获取、存储和处理,并提供与外界交互的接口。
  • View负责展示数据和用户交互,简单来说他就是我们看到的UI 组件或 HTML 页面。
  • ViewModel是连接 ViewModel 的桥梁,它不直接操作视图或模型,而是通过数据绑定将两者连接起来。

参考下面的示例哦:

js 复制代码
<div id="app">
  <input v-model="message"/>
  <p>{{ computedValue }}</p>
</div>

<script setup>
  const message = ref('Hello, MVVM!');

  const computedValue = computed(()=> {
    return "用户输入值变为:" + message.value
  })
</script>

上述代码展示了一个输入框,当用户输入内容的时候,输入框下面的计算值会随之变化。在这个示例中,message 变量属于 Model,它包含了应用的核心数据。输入框与页面展示就属于View,负责展示数据和用户交互。computedv-model语法糖 作为 ViewModel,用于更新视图和数据。

Vue 双向数据绑定的实现原理

Vue 实现双向数据绑定的核心是通过响应式系统数据劫持观察者模式来实现的。

数据劫持

Vue 2.x 使用 Object.defineProperty对数据对象的每个属性递归添加 getter/setter,当数据的属性被访问时,触发 getter,当属性被修改时,触发 setter通知视图进行更新。通过这种方式,Vue 可以监控数据的变化,并在数据变化时通知视图更新。

Vue 3.x 使用 Proxy通过代理对象拦截整个对象的操作,无需递归初始化所有属性,性能更好。

观察者模式

Vue 的响应式系统通过 观察者模式 来实现数据与视图的同步更新,简化的流程如下:

  • 依赖收集 :当 Vue 组件的视图模板渲染时,它会读取数据对象的属性(例如 {{ message }})。在读取属性时,getter方法会将视图组件与该数据属性建立依赖关系。
  • 观察者(Watcher) :每个依赖的数据都会对应一个观察者。观察者的作用是监听数据的变化,一旦数据发生变化,观察者会收到通知,进而触发视图的更新。
  • 通知视图更新(Notify View Update) :当数据通过 setter 修改时,Vue 会触发相应的观察者,通知相关的视图组件更新。

通过这种方式,Vue 可以监控数据的变化,并在数据变化时通知视图更新。

Vue 模板编译流程

Vue 的模板编译过程是将开发者编写的模板语法(例如 {{ message }}v-bind 等)转换为 JavaScript 代码的过程。它主要分为三个阶段:模板解析AST优化代码生成

模板解析

Vue 使用其解析器将 HTML 模板转换为 抽象语法树(AST) 。在这个阶段,Vue 会分析模板中的标签、属性和指令,生成一颗树形结构。每个节点表示模板中的一个元素或属性。

如:

js 复制代码
<div>
  <p>{{ message }}</p>
  <button v-on:click="handleClick">点击</button>
</div>

被解析成的 AST 类似于下面的结构:

js 复制代码
{
  type: 1, // 节点类型:1 表示元素节点
  tag: 'div', // 元素的标签名
  children: [ // 子节点(嵌套的 HTML 元素)
    {
      type: 1, // 子节点是一个元素节点
      tag: 'p',
      children: [
        {
          type: 2, // 2 表示插值表达式节点
          expression: 'message' // 表达式 'message'
        }
      ]
    },
    {
      type: 1, // 另一个元素节点
      tag: 'button',
      events: { // 事件监听
        click: 'handleClick' // 绑定 click 事件,执行 handleClick 方法
      },
      children: [
        {
          type: 3, // 文本节点
          text: '点击' // 按钮文本
        }
      ]
    }
  ]
}

AST优化

Vue 在生成渲染函数前,会对 AST 进行优化。优化的核心目标是标记 静态节点,在渲染时,Vue 可以跳过这些静态节点,提升性能。

静态节点指所有的渲染过程中都不变化的内容,比如某个div标签内的静态文本

在vue3中,如果一个节点及其子树都不依赖于动态数据,那么该节点会被提升到渲染函数外部(静态提升),仅在组件初次渲染时创建。

代码生成

生成渲染函数是编译的最终阶段,这个阶段会将优化后的 AST 转换成 JavaScript 渲染函数。

例如,像这样的模板:

js 复制代码
<div id="app">{{ message }}</div>

最终会生成类似这样的渲染函数:

js 复制代码
function render() {
  return createVNode("div", { id: "app" }, [
    createTextVNode(this.message)
  ])
}

渲染函数的返回值是一个虚拟 DOM(VDOM)树,Vue 会根据虚拟 DOM 来更新实际的 DOM。由于渲染函数被 Vue 的响应式系统包裹,当数据发生变化时,渲染函数会被重新执行生成新的虚拟 DOM,因此页面也会实时更新。

Vue diff 算法

Vue的diff算法执行,依赖数据的的响应式系统:当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者会重新执行渲染函数,渲染函数内部通过diff 算法用于比较新旧虚拟 DOM 树的差异,并计算出最小的更新操作,最终更新相应的视图。

Vue diff 算法流程

diff 算法的核心算法流程如下:

  • 节点对比

如果新旧节点类型相同,则继续比较它们的属性。如果节点类型不同(如元素和文本节点不同),则直接替换整个节点。

  • 属性更新

如果节点类型相同,接下来检查节点的属性。对于不同的属性值进行更新,移除旧属性,添加新属性。

  • 子节点比对

对于有子节点的元素(如 div),Vue 会使用不同的策略来优化子节点更新:

文本节点的更新:如果新旧子节点都是文本节点,直接更新文本内容。

数组类型子节点的比对 :如果新旧子节点都是数组,Vue 会通过 LIS 算法来优化节点的重新排列,避免过多的 DOM 操作。

Vue3 diff 算法做了哪些优化

  • 静态标记与动态节点的区分

Vue3引入了静态标记(Static Marking)机制,通过在模板编译阶段为静态节点添加标记,避免了对这些节点的重复比较。这使得Vue3能够更高效地处理静态内容,减少不必要的DOM操作。

  • 双端对比策略

Vue3的Diff算法采用了双端对比策略,即从新旧节点的头部和尾部同时开始比较,快速定位无序部分。这种策略显著减少了全量对比的复杂度,提升了性能。

  • 最长递增子序列(LIS)优化

在处理节点更新时,Vue3利用最长递增子序列(LIS)算法来优化对比流程。通过找到新旧节点之间的最长递增子序列,Vue3可以减少不必要的DOM操作,从而提高更新效率。

  • 事件缓存与静态提升

事件缓存:Vue3将事件缓存为静态节点,避免每次渲染时重新计算事件处理逻辑,从而减少性能开销。

静态提升:对于不参与更新的元素,Vue3将其提升为静态节点,仅在首次创建时进行处理,后续不再重复计算。

  • 类型检查与属性对比

Vue3在Diff算法中增加了类型检查和属性对比功能。如果节点类型不同,则直接替换;如果类型相同,则进一步对比节点的属性,生成更新操作。

  • 动态插槽的优化

Vue3对动态插槽进行了优化,通过动态节点的类型化处理,进一步提升了Diff算法的效率

总结

通过上述内容,一个清晰的Vue 组件的渲染和更新过程如下:

Vue 组件的渲染更新 过程涉及从模板编译 到虚拟 DOM 的构建 、更新和最终的实际 DOM 更新。下面是 Vue 组件渲染和更新的主要步骤

组件渲染过程

Vue 的组件的渲染过程核心是其模板编译过程,大致流程如下:

首先,Vue会通过其响应式系统 完成组件的 data、computed 和 props 等数据和模板的绑定,这个过程Vue 会利用 Object.defineProperty(Vue2)Proxy(Vue3) 来追踪数据的依赖,保证数据变化时,视图能够重新渲染。随后,Vue会将模板编译成渲染函数 ,这个渲染函数会在每次更新时被调用,从而生成虚拟 DOM。

最终,虚拟DOM被渲染成真实的 DOM 并插入到页面中,组件渲染完成,组件渲染的过程中,Vue 会依次触发相关的生命周期钩子。

组件更新过程

当组件的状态(如 datapropscomputed)发生变化时,响应式数据的setter方法会让调用Dep.notify通知所有订阅者Watcher,重新执行渲染函数触发更新。

渲染函数在执行时,会使用 diff 算法(例如:双端对比、静态标记优化等)生成新的虚拟DOM。计算出需要更新的部分后(插入、删除或更新 DOM),然后对实际 DOM 进行最小化的更新。在组件更新的过程中,Vue 会依次触发beforeUpdateupdated等相关的生命周期钩子。

相关推荐
egghead2631611 分钟前
React常用hooks
前端·react.js
whysqwhw11 分钟前
Http与Https
面试
科粒KL14 分钟前
前端学习笔记-浏览器渲染管线/一帧生命周期/框架更新
前端·面试
whysqwhw19 分钟前
GET 与 POST
面试
凉_橙19 分钟前
什么是抽象语法树?
前端·javascript
成小白20 分钟前
前端实现分片上传
前端
页面魔术23 分钟前
尤雨溪: 我们没有放弃虚拟 dom
前端·javascript·vue.js
欧阳码农25 分钟前
langgraph开发Deep Research智能体-项目搭建
前端·后端·langchain
再吃一根胡萝卜26 分钟前
TCP三次握手机制的深入解析
前端
用户479492835691526 分钟前
Biome:用 Rust 重构前端开发体验
前端·代码规范