从原始 DOM 操作到 Vue 响应式驱动:一次彻底的进化史实操讲解

83 阅读4分钟

从原始 DOM 操作到 Vue 响应式驱动:一次彻底的进化史实操讲解

大家好,今天带大家从「最原始的后端套模板」一步步走到「现代 Vue3 响应式驱动界面」的全过程,用真实代码带你看清每一次架构升级背后的底层逻辑和痛点。 我们会用 5 个真实可运行的阶段代码,带你穿越前端 15 年的进化史。

7d51c79f4346e65c2eafb673286d21c7.jpg

第 1 阶段:远古时代:纯后端套模板(Node + 原生 HTTP)

// 2010 年的典型写法
const http = require("http"); 
const url = require("url"); 
const users = [/* 假数据 */];

function generateUserHTML(users) {
  const userRows = users.map(user => `
    <tr>
      <td>${user.id}</td>
      <td>${user.name}</td>
      <td>${user.email}</td>
    </tr>
  `).join('');
  
  return `...${userRows}...`;
}

http.createServer((req, res) => {
  if (req.url === '/users') {
    res.setHeader('Content-type', 'text/html;charset=utf-8');
    res.end(generateUserHTML(users));
  }
}).listen(1314);

这是十多年前最常见的做法,典型 MVC(其实是 MTV)。

优点:简单、直观、一文件搞定
致命缺点:

  1. 前端一点交互都做不了(加个删除按钮?重新刷新整页)
  2. 后端工程师要写 HTML、CSS,前端工程师失业
  3. SEO 友好,但用户体验极差

我们把它称为「页面 = 数据 + 模板字符串硬拼接」

第 2 阶段2014 年:前后端分离 + 原生 fetch + DOM 编程


<tbody id="tbody"></tbody>


fetch('http://localhost:3000/users')
  .then(res => res.json())
  .then(data => {
    const tbody = document.querySelector('tbody');
    tbody.innerHTML = data.map(user => `
      <tr>
        <td>${user.id}</td>
        <td>${user.name}</td>
        <td>${user.email}</td>
      </tr>
    `).join('');
  })

后端只提供 JSON 接口(json-server 或 Express):

// db.json
{
  &#34;users&#34;: [
  {
    &#34;id&#34;: 1,
    &#34;name&#34;: &#34;小明&#34;,
    &#34;email&#34;: &#34;123@qq.com&#34;
  },...]
}

这时候前后端终于解耦了!

前端终于可以专注做交互了,但代价是:

  • 每次数据变了都要手动操作 DOM
  • 找节点、拼接 HTML 字符串、innerHTML 赋值… 这些都不是业务代码
  • 代码里一大堆 document.querySelector、createElement

我们把它称为「页面 = 静态 HTML + 手动 DOM 编程」

第 3 阶段2020 年后:Vue3 响应式 + 数据驱动模板(我们今天的主角)



import { ref, onMounted } from 'vue'

const users = ref([]) // ← 关键!响应式数据

onMounted(() => {
  fetch('http://localhost:3000/users')
    .then(r => r.json())
    .then(data => {
      users.value = data // ← 赋值给 .value 才会触发更新
    })
})



  <table>
    <tr>
      <td>{{ user.id }}</td>
      <td>{{ user.name }}</td>
      <td>{{ user.email }}</td>
    </tr>
  </table>

只改了这几行代码,页面就从「手动 DOM 操作」进化成了「声明式、自动更新」!

为什么这一步是质的飞跃?

对比项原始 DOM 操作Vue 响应式驱动
数据改变后手动重新 render自动更新视图
主要工作找节点、拼接字符串只关心数据和业务逻辑
代码量随着功能指数级增长几乎线性增长
可维护性灾难丝滑
是否需要理解 Virtual DOM、diff 算法不用管Vue 帮你搞定

响应式到底是怎么工作的?(底层逻辑揭秘)

很多人会背「ref 把普通变量包装成响应式对象」,但到底怎么包装的?

const count = ref(0)
// 实际结构大概是这样的(简化版):
count = {
  value: 0,
  __v_isRef: true,
  dep: new Set(), // 依赖集合
  get value() {
    // 依赖收集:把当前正在渲染的组件 effect 收集起来
    track(this, 'value')
    return this._value
  },
  set value(newVal) {
    this._value = newVal
    // 触发更新:通知所有依赖了 count 的地方重新渲染
    trigger(this, 'value')
  }
}

当模板里出现 {{ count }} 时:

  1. 触发 count 的 getter → 依赖收集(track)
  2. 当你执行 count.value++ 时 → 触发 setter → trigger()
  3. Vue 自动把所有依赖了 count 的组件重新 render

通俗来讲:

Vue 的响应式不是在监视 DOM,也不是在监视变量, 而是在监视『哪段渲染代码(render 函数/effect)读了这个数据』。

get 阶段完成『登记』:把正在执行的 render 函数记下来;

set 阶段完成『通知』:把之前登记的所有 render 函数重新执行一遍。

整个过程就是一次精准的『函数级订阅-发布』, 你只改数据,Vue 负责把所有用过这个数据的渲染函数自动再跑一遍。

这就是「数据驱动视图」的底层魔法!

重要提醒(踩坑合集)

  1. 忘记写 .value(最常见新手错误!)
// 错误:直接赋值普通数组,Vue 检测不到变化
users = data

// 正确
users.value = data
  1. 直接替换整个数组对象可以,但 push/splice 更推荐
// 这两种都能触发更新
users.value = [...users.value, newUser]
users.value.push(newUser) // Vue3 对数组变异方法做了代理
  1. 如果数据是嵌套对象,也要用 ref 或 reactive
const state = reactive({
  user: { name: '小明', info: { age: 18 } }
})
// 直接改深层属性也能触发更新
state.user.info.age = 20 // OK!

阶段对比总结(强烈建议收藏)

阶段代表技术核心思想痛点适合场景
1. 后端模板JSP/PHP/ThinkPHP/Node 字符串拼接数据 + 模板字符串前后端强耦合、无交互管理后台、博客
2. 前后端分离+DOMjQuery + 原生 fetch手动 DOM 操作维护成本高、易错中小型项目
3. 响应式框架Vue3 / React / Svelte数据驱动视图 + 自动 diff学习成本(once)99% 的现代前端项目

最后做个小实验(强烈建议自己敲一遍)

把 App.vue 里的 onMounted 注释掉,改成:

// 模拟 3 秒后数据更新
setTimeout(() => {
  users.value.push({
    id: 999,
    name: '新用户',
    email: 'new@163.com'
  })
}, 3000)

你会看到页面上自动多了一行!
这就是响应式最直观的魅力——你只管改数据,视图自动跟上。

几个细节知识点

fc962ce0cd306c49bc54248e80437e81.jpg

1. MVC 到底是什么?为什么和前端一点交互都做不了?

  • M = Model(数据层)→ 操作数据库、定义数据结构
  • V = View(视图层)→ HTML 模板
  • C = Controller(控制层)→ 接收请求 → 查 Model → 把数据塞进 View → 返回一整个 HTML

典型流程(以前面第一段代码为例): 浏览器请求 → Node 服务器 → 查询 users 数组 → 把数据塞进 HTML 字符串 → 返回完整页面 → 浏览器渲染

为什么“一点交互都做不了”?

因为每次你点按钮、删一条数据、本质上都是发起一个新的 HTTP 请求 → 服务端重新跑一遍 MVC → 重新生成一整个新 HTML → 浏览器把旧页面整个扔掉,换上新页面。

相当于:

  • 你在 Excel 里点“删除一行”
  • Excel 把整个文件关掉,重新打开一个新文件给你看

用户体验:页面白一下、闪一下、滚动条回到顶部、输入框内容全没了……

这才是真正的“一点交互都做不了”——不是不能做,而是做了也超级烂。

2.三种方式应对数据改变的对比(以前后端分离展开来讲)

举个最经典的例子:

JavaScript

let users = [];

// 第一步:发请求拿到数据
fetch('/users').then(r => r.json()).then(data => {
  users = data;                     // 数据已经变了!!
  render();                         // 你必须手动调用 render 才能看到变化
});

function render() {
  const tbody = document.querySelector('tbody');
  tbody.innerHTML = users.map(user => `
    <tr><td>${user.id}</td><td>${user.name}</td></tr>
  `).join('');
}

你改了 users 变量,但页面根本不知道! 除非你手动再去操作 DOM(find节点、改innerHTML、删节点、加节点……

也就是再执行一次render()

这就叫“数据变了,视图没变,必须手动同步”。

我们来看看三种方式对比(以删除数据为例):

  • 传统 MVC 方式: 用户点删除 → 提交表单 → 浏览器发新请求 → Node 重新执行 render() → 返回一整个新 HTML → 浏览器整个页面替换 → 页面闪白、滚动条回到顶部、标题动画重来一遍…

  • 手动 DOM 编程方式: 用户点删除 → fetch DELETE → users = users.filter(...) → 你手动调用 render() → 只有 tbody 内容变了 → 页面不闪白、导航栏不动、搜索框里的字还在 → 用户体验大幅提升!

  • Vue 方式: users.value.splice(i, 1) → 连 render() 都不用你写 → 只有那一行淡出消失 → 其他完全不动

总结来说:

  • MVC(后端渲染) :数据变了 → 整个页面重生
  • 手动 render(早期 SPA) :数据变了 → 局部重绘(你得自己喊 render)
  • Vue(现代 SPA) :数据变了 → 局部自动重绘(你只管改数据)

3.fetch + 轮询

以前后端分离展开: 在我们第一段代码中,fetch执行一次就结束了,后端再怎么改数据,如果不再次调用fetch数据永远不会再动! 那我们怎么实时实现更新数据呢?

那是因为,在我们的后端文件里的 json-server 其实加了个“轮询”功能:

&#34;scripts&#34;: {
    &#34;dev&#34;: &#34;json-server --watch db.json&#34;
  },

它会在文件变化时自动广播(通过 WebSocket),很多前端工具(vite、vue-cli)会自动监听这个广播,然后热更新页面。

所以你改 数据后页面自动刷新,并不是 fetch 在监视,而是开发工具在监视!

3. 三个易错点详细拆解

(1)忘记给 tr 加 :key,后续增删动画会出大问题

vue



<tr> ... </tr>


<tr> ... </tr>

Vue 是靠 key 来判断“这一行到底是新建、删除、还是移动”的。

没 key 时,Vue 只能按“位置”来比对:

原来:A B C

现在:A C D

Vue 会傻傻地认为:B 改成了 C,C 改成了 D,新增一个 D → 全部重绘 + 动画错乱

有 key 时:

原来:id1(A) id2(B) id3(C)

现在:id1(A) id3(C) id4(D)

Vue 立刻知道:id2 被删了,id4 新增,id3 移动了位置 → 精准动画

实际效果你自己试试就知道,列表删除时会“集体抽风”。

(2)innerHTML 有 XSS 风险(生产必须转义)

JavaScript

// 用户恶意输入 name 为:alert('你被黑了
`<tr><td>${user.name}</td></tr>`
// 直接变成:
<tr><td>alert('你被黑了')</td></tr>
// 浏览器直接执行!网站被黑

正确做法(手动防 XSS):

JavaScript

function escapeHtml(str) {
  return str.replace(/[&<>&#34;']/g, match => ({
    '&': '&amp;', '<': '<', '>': '>', '&#34;': '&#34;', &#34;'&#34;: '''
  })[match]);
}

Vue 模板会自动转义,React 也自动转义,唯独你自己用 innerHTML 的时候要自己记得!

(3)多次 render 会造成性能浪费(没做 diff)

你自己写 render() 函数时,一般是:

JavaScript

tbody.innerHTML = '' // 先清空
tbody.innerHTML = 新内容 // 再整个写回去

就算只有第 10 行变了,你也把 1000 行全部删掉重写 → 浏览器全部重排重绘 → 卡!

diff 长什么样?(Vue 的真实做法)

Vue 会先在内存里造一个「假 DOM」(叫 Virtual DOM),然后拿新旧两份假 DOM 去比:

text

旧 Virtual DOM:    [用户1, 用户2, 用户3, 用户4]
新 Virtual DOM:    [用户1, 用户2, 小红, 用户4]
                     ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
                          Vue diff 算法只发现:第 3 项变了
                     ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
真实 DOM 操作:      只改第 3 个 <tr> 的内容,其他不动!

这就是 diff 的全部意义:最小化真实 DOM 操作

Vue/React 会在内存里先做 Virtual DOM diff,只更新真正在 DOM 上变化的那几行,性能提升几十倍。

4. 为什么不写 .value 就检测不到变化?

因为 ref() 返回的不是普通对象,而是一个“炼金术士”:

JavaScript

const count = ref(0)

// 实际长这样(简化版):
count = {
  _value: 0,
  get value() {  // 有人读我的时候触发依赖收集
    track()
    return this._value
  },
  set value(newVal) { // 有人改我的时候触发更新
    this._value = newVal
    trigger()
  }
}

你如果直接写成:

JavaScript

const users = ref([])
users = data // 你把整个炼金术士对象整个替换成了普通数组!!

这时候 Vue 的“依赖收集-触发更新”机制完全失效了,因为你已经不是那个带 getter/setter 的 ref 对象了。

正确做法永远是:

JavaScript

users.value = data  // 只改内部的 _value,getter/setter 还在,机制正常

这就是为什么模板里可以直接写 {{ users }}(Vue 自动帮你读 .value),但在 < script > 里必须手动写 .value。

5. 深层嵌套对象用 ref 只需要包装最外层?

完全正确!这是 Vue3 最爽的地方之一。

JavaScript

const state = ref({
  user: {
    info: {
      profile: {
        age: 18
      }
    }
  }
})

// 你可以随便改 100 层深的地方改!
state.value.user.info.profile.age = 99
// 页面也会立刻更新!!!

原理:Vue3 的 ref + reactive 使用了 ES6 Proxy,对整个对象(包括所有嵌套属性)的 get/set 都做了代理,所以无论多深,只要是通过 ref/reactive 包过的最外层对象,内部随便改都响应式。

对比 Vue2: Vue2 必须一层层 Object.defineProperty,如果新增属性(obj.newProp = xxx)还不触发更新,要用 Vue.set

Vue3:直接干就完事儿了

写在最后

从「手动拼接 HTML 字符串」→「手动操作 DOM」→「只改数据,视图自动更新」,这三次架构升级,每一次都让开发者幸福感翻倍。

Vue3 的 ref/reactive + + 模板语法,堪称现代前端工程化的巅峰之一。它把最复杂的「视图如何高效更新」问题彻底封装,让我们这些普通开发者可以真正「聚焦业务」。

所以,下次有人问你:「Vue 到底比 jQuery 强在哪?」

你就可以甩出这张图:

jQuery 时代:我要改数据 → 找节点 → 改 innerHTML → 祈祷没 bug
Vue 时代   :我要改数据 → users.value.push(...) → 喝茶等自动更新