第 7 章 · 原生 JS 与框架的本质差异
本章目标:从虚拟 DOM、响应式、编译时 vs 运行时理解框架底层;用同一功能三版对照(原生 / Vue / React)建立心智模型;掌握数据驱动 vs 命令式 DOM;能判断何时不需要框架、何时必须用;权衡迁移成本、团队技能、包体积;结合贤紫商城真实案例做选型。
学时建议:3~4 小时(阅读 + 对照实验)
前置:完成本模块 ch01~ch06 与 frontend-web ch16 毕业项目。
7.1 三种实现范式总览
| 范式 | 代表 | 核心思想 | 你写的代码 | 运行时做的事 |
|---|---|---|---|---|
| 命令式 DOM | 原生 JS | 一步步改 DOM | el.textContent = x | 浏览器渲染 |
| 声明式 + 响应式 | Vue 3 | 描述状态与视图关系 | {{ title }} + ref | 依赖追踪 + patch |
| 声明式 + 协调 | React 18 | 描述 UI = f(state) | JSX + Hooks | Reconcile + commit |
┌─────────────────────────────────────────────────────────┐
│ 业务状态(课程列表、筛选词、收藏) │
└───────────────┬─────────────────┬───────────────────────┘
▼ ▼
原生:手写同步 框架:自动/半自动同步
│ │
▼ ▼
DOM 树 虚拟 DOM / 响应式代理
│ │
▼ ▼
屏幕像素 屏幕像素
行业案例 · 贤紫优选商城:C 端秒杀倒计时页曾用原生setInterval+ 直接改 DOM,维护 3 个活动后逻辑纠缠;迁移为 Vue 组合式 API 后,倒计时与库存状态统一由reactive驱动,同类页复制成本下降 60%。后台订单列表仍保留部分原生片段(嵌入老报表 iframe),按边界拆分而非一刀切换框架。
7.2 虚拟 DOM:是什么、解决什么
7.2.1 问题背景
频繁直接操作 DOM 时,容易出现:
- 状态与界面不同步(忘了更新某处文本)
- 大量细粒度 DOM 操作触发布局抖动
- 列表重排时手动 diff 成本高
虚拟 DOM(VDOM) 是用 JavaScript 对象描述真实 DOM 树的轻量副本;框架在内存中对比新旧 VDOM,算出最小变更集再批量写入真实 DOM。
7.2.2 Vue 与 React 的差异
| 维度 | Vue 3 | React 18 |
|---|---|---|
| VDOM 角色 | 配合编译优化(静态提升、Block Tree) | 协调算法核心 |
| 更新触发 | 响应式依赖收集 → 组件级更新 | setState → 从根向下 reconcile |
| 编译优化 | 模板编译期标记静态节点 | 主要靠运行时 + 开发者 memo |
| 手写优化 | 较少需要 | useMemo/memo 较常见 |
// 概念示意:VNode 简化结构(非真实 API)
const vnode = {
type: 'div',
props: { class: 'course-card' },
children: [
{ type: 'h3', children: 'Vue 3 入门' },
{ type: 'span', children: '12 课时' }
]
}
误解澄清:VDOM 不一定比原生慢;对于复杂 UI,批量 patch 往往优于散乱 DOM API。原生在极简单、更新点极少时仍可更快。
7.3 响应式:Vue 的「自动同步」
Vue 3 用 Proxy 包装对象/ref 包装标量,在读取时收集依赖,在写入时通知订阅者重新渲染。
// Vue 3 响应式(概念)
import { ref, computed } from 'vue'
const keyword = ref('')
const courses = ref([{ id: 'fe-01', title: 'HTML 基础' }])
const filtered = computed(() =>
courses.value.filter(c => c.title.includes(keyword.value))
)
// keyword 变化 → filtered 重算 → 模板中用到 filtered 的节点更新
React 无内置细粒度响应式:状态变更触发组件函数整函数重执行,由协调算法决定 DOM 是否变化。
| 对比 | Vue 响应式 | React Hooks |
|---|---|---|
| 粒度 | 依赖级(默认组件级边界) | 组件级重渲染 |
| 心智 | 「改数据,视图跟」 | 「setState,函数再跑一遍」 |
| 漏更新 | 少见(模板绑定即依赖) | 闭包陈旧值需警惕 |
7.4 编译时 vs 运行时
| 类型 | 编译时(Build Time) | 运行时(Run Time) |
|---|---|---|
| 做什么 | 模板/JSX 转 JS、类型擦除、Tree-shaking | 在浏览器执行框架逻辑 |
| Vue | <template> → render 函数 + 静态标记 | 响应式系统、patch |
| React | JSX → React.createElement | Reconcile、Hooks 链表 |
| 原生 | 几乎无(Vite 仅打包) | 你的全部逻辑 |
Vue SFC 编译流水线(简化)
.vue 文件
→ @vue/compiler-sfc
→ render 函数 + scopeId + 静态 hoist
→ 浏览器只跑「瘦」运行时
React 典型流水线
.jsx
→ Babel(jsx)
→ 运行时 Reconciler 承担更多工作
工程含义:
- 编译优化多 → 生产包可能更小、更新路径更短(Vue 3 方向)
- 运行时重 → 生态统一、与 JS 表达力一致(React 方向)
- TypeScript 主要在编译时检查,运行时零成本(ch08 展开)
7.5 同一功能三版对照:课程搜索列表
需求:读取 Mock 课程,输入框防抖搜索,展示卡片列表,点击收藏切换心形状态。
7.5.1 原生 JS(ch16 风格)
// main.js 片段 — 命令式 DOM
const state = { keyword: '', courses: [], favorites: new Set() }
const listEl = document.getElementById('course-list')
const inputEl = document.getElementById('search')
function renderList() {
const kw = state.keyword.trim().toLowerCase()
const items = state.courses.filter(c =>
!kw || c.title.toLowerCase().includes(kw)
)
listEl.innerHTML = items.map(c => `
<article class="course-card" data-id="${c.id}">
<h3>${c.title}</h3>
<button type="button" class="fav-btn" aria-pressed="${state.favorites.has(c.id)}">
${state.favorites.has(c.id) ? '♥' : '♡'}
</button>
</article>
`).join('')
listEl.querySelectorAll('.fav-btn').forEach(btn => {
btn.addEventListener('click', onFavClick)
})
}
function onFavClick(e) {
const id = e.target.closest('.course-card').dataset.id
if (state.favorites.has(id)) state.favorites.delete(id)
else state.favorites.add(id)
renderList() // 全量重绘
}
let timer
inputEl.addEventListener('input', () => {
clearTimeout(timer)
timer = setTimeout(() => {
state.keyword = inputEl.value
renderList()
}, 300)
})
fetch('/mock/courses.json').then(r => r.json()).then(data => {
state.courses = data
renderList()
})
特点:state 与 renderList 是你维护的契约;事件绑定在每次 innerHTML 后需重绑或改事件委托。
7.5.2 Vue 3
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
const courses = ref([])
const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 300)
const favorites = ref(new Set())
const filtered = computed(() => {
const kw = debouncedKeyword.value.trim().toLowerCase()
return courses.value.filter(c =>
!kw || c.title.toLowerCase().includes(kw)
)
})
function toggleFav(id) {
const s = new Set(favorites.value)
s.has(id) ? s.delete(id) : s.add(id)
favorites.value = s
}
onMounted(async () => {
courses.value = await fetch('/mock/courses.json').then(r => r.json())
})
</script>
<template>
<input v-model="keyword" placeholder="搜索课程" />
<article v-for="c in filtered" :key="c.id" class="course-card">
<h3>{{ c.title }}</h3>
<button
type="button"
:aria-pressed="favorites.has(c.id)"
@click="toggleFav(c.id)"
>
{{ favorites.has(c.id) ? '♥' : '♡' }}
</button>
</article>
</template>
特点:无手写 renderList;computed 表达筛选;模板即 DOM 结构。