下载工作台
前端框架精通

原生 JS 与框架的本质差异

试读上半部分 · 解锁后可读全文

第 7 章 · 原生 JS 与框架的本质差异

本章目标:从虚拟 DOM、响应式、编译时 vs 运行时理解框架底层;用同一功能三版对照(原生 / Vue / React)建立心智模型;掌握数据驱动 vs 命令式 DOM;能判断何时不需要框架、何时必须用;权衡迁移成本、团队技能、包体积;结合贤紫商城真实案例做选型。

学时建议:3~4 小时(阅读 + 对照实验)

前置:完成本模块 ch01~ch06 与 frontend-web ch16 毕业项目。


7.1 三种实现范式总览

范式代表核心思想你写的代码运行时做的事
命令式 DOM原生 JS一步步改 DOMel.textContent = x浏览器渲染
声明式 + 响应式Vue 3描述状态与视图关系{{ title }} + ref依赖追踪 + patch
声明式 + 协调React 18描述 UI = f(state)JSX + HooksReconcile + 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 3React 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
ReactJSX → React.createElementReconcile、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()
})

特点staterenderList 是你维护的契约;事件绑定在每次 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>

特点:无手写 renderListcomputed 表达筛选;模板即 DOM 结构。

7.5.3 React 18

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。