本文最后更新于 2026年10月27日。
结论先说
完全可以,不用 Vue / React 这类大型框架,原生 HTML + JS + CSS 就能做到局部刷新、组件化、路由、状态管理这些能力。 本质上:Vue、React 底层也是操作 DOM,只是封装了虚拟DOM、响应式、调度机制。原生 JS 直接操作 DOM 就是最原始的局部刷新方案。
下面分方案从简单到进阶讲清楚各自优缺点、适合场景。
1. 最基础:手动 DOM 操作(innerHTML / appendChild / replaceChild)
原理:不刷新整个页面,只选中某个 DOM 节点,替换它内部内容。
<div id="container"></div>
<button onclick="loadData()">加载内容</button>
<script>
async function loadData() {
const res = await fetch('/api/list');
const data = await res.json();
// 局部刷新,只改 container,页面不重载
document.getElementById('container').innerHTML = `
<ul>
${data.map(item => `<li>${item.name}</li>`).join('')}
</ul>
`;
}
</script>
✅ 优点:零依赖,最简单 ❌ 缺点:
- innerHTML 会销毁内部所有子DOM、丢失事件绑定、输入框光标/状态
- 数据变了要手动写代码更新对应DOM,没有自动绑定
- 复杂页面维护灾难
小技巧:如果不想销毁DOM,不用 innerHTML,用
document.createElement、append、remove精细增删节点。
2. 模板引擎(轻量,不是SPA框架)
比如:Handlebars、Nunjucks、Mustache,甚至自己写极简模板函数。 把模板字符串抽出来,数据传入生成 html,再局部塞到容器里。 比手写字符串拼接好一点,但依然是生成HTML字符串替换DOM,没有diff。
3. 自己实现简易虚拟DOM + Diff(就能复刻React核心思路)
核心三步:
- 用 JS 对象描述节点(vnode)
- 新旧 vnode 对比 diff
- 只把差异部分更新到真实 DOM,实现精细局部更新,不会整个容器重载
几百行 JS 就能跑通一个极简版,具备:
- 增量局部更新
- 嵌套组件 但是没有 fiber 调度、并发、HMR、生态,适合学习,大型项目自己维护成本极高。
4. 原生 Web Components(浏览器标准,非常推荐)
属于浏览器原生能力,不需要任何框架:
- Custom Elements(自定义标签)
- Shadow DOM(隔离样式、DOM)
- HTML Templates(
<template>) <slot>插槽
<template id="card-tpl">
<style>p{color:red}</style>
<p></p>
</template>
<script>
class MyCard extends HTMLElement {
constructor() {
super();
const tpl = document.getElementById('card-tpl');
this.attachShadow({mode:'open'}).append(tpl.content.cloneNode(true));
}
// 属性变化自动触发更新,原生响应式钩子
static get observedAttributes() { return ['title']; }
attributeChangedCallback(name, oldVal, newVal) {
this.shadowRoot.querySelector('p').textContent = newVal;
}
}
customElements.define('my-card', MyCard);
</script>
<!-- 使用 -->
<my-card title="hello"></my-card>
✅ 能力:
- 组件化、样式隔离
- 属性变更自动局部更新
- 可以组合、嵌套
- 原生标准,可和任意页面混用 ❌ 短板:
- 没有内置状态管理、路由、全局响应式
- 没有虚拟DOM diff,属性更新粒度由你自己控制
- 表单双向绑定、列表渲染需要自己封装
很多轻量框架(Lit、Shoelace)就是基于 Web Components 做的封装,Lit 只有十几KB,远小于 Vue/React。
5. 原生响应式 + 发布订阅(自己做数据驱动)
用 Proxy 封装数据,数据修改时,自动执行对应的更新函数,绑定到指定DOM:
function reactive(obj, effect) {
return new Proxy(obj, {
set(target, key, value) {
target[key] = value;
effect(); // 数据变了,执行局部更新
return true;
}
})
}
const state = reactive({ name: 'test' }, () => {
document.querySelector('#name').textContent = state.name;
});
这就是 Vue2 Object.defineProperty / Vue3 Proxy 响应式的核心原理。 你可以自己写依赖收集,做到只有用到这个数据的DOM才刷新,精准局部更新。
6. 轻量替代框架(如果你不想从零造轮子,但排斥Vue/React)
这些都很小,主打局部刷新、数据驱动:
- Lit:Google,基于Web Components,约5KB,数据驱动、自动更新
- Preact:API和React几乎一致,3KB,有虚拟DOM diff
- Svelte:编译期框架,运行时几乎零开销,没有虚拟DOM,编译成原生JS DOM操作
- Alpine.js:极轻量(~7KB),在HTML上写指令,类似Vue模板,不用构建工具,直接引入脚本就能用,非常适合老页面加局部交互
<div x-data="{count:0}">
<button @click="count++">+1</button>
<span x-text="count"></span>
</div>
<script defer src="[https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js](https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js)"></script>
Alpine 是这类场景的热门选择:不推翻原有HTML页面,按需加局部数据驱动刷新。
7. 路由(SPA无刷新跳转)
原生 API:history.pushState / popstate 不需要框架就能实现前端路由,切换url不重载页面,只更换对应区块内容,搭配上面局部刷新。
关键对比总结
| 方案 | 局部刷新 | 数据驱动 | 组件隔离 | 适合场景 |
|---|---|---|---|---|
| 原生DOM innerHTML | ✅ 整块替换 | ❌ 手动 | ❌ | 简单页面、少量交互 |
| Web Components | ✅ 可控更新 | ⚠️ 自己封装 | ✅ Shadow DOM | 可复用组件、跨框架组件 |
| Proxy响应式 + 订阅 | ✅ 精准更新 | ✅ | ❌ 需要自己封装 | 想自研极简数据驱动 |
| Alpine.js | ✅ | ✅ | ⚠️ | 老HTML页面增强,不想上构建 |
| Svelte/Lit/Preact | ✅ diff/细粒度 | ✅ | ✅ | 轻量SPA,不想用Vue React |
什么时候不建议纯原生手写?
页面规模大:大量列表嵌套、复杂状态联动、大量表单、频繁DOM更新。 自己维护 diff、依赖收集、调度、事件、缓存,工程量会慢慢复刻一个迷你框架,后期维护成本很高。
一句话总结
局部刷新本质就是:不触发页面整体导航重载,只按需修改部分DOM。这件事是DOM API原生就支持的,和Vue/React没有绑定关系。Vue/React只是一套成熟封装方案,不是唯一方案。
如果你想要,我可以给你一份:无任何框架、纯原生JS实现的极简响应式+diff局部刷新demo(包含列表增删,只更新变化行)。