07、原生的Vuejs:从入门到企业实战,一篇搞懂无构建开发

前言
你是否有过这样的经历:接手一个老旧的Java Web项目,前端页面全是JSP+jQuery,想加个交互功能得手动操作十几个DOM节点,改一个数据要同步更新三四个地方,代码越写越乱,bug越修越多?
或者你是一个前端新手,看到网上的Vue教程一上来就是npm install、webpack、vue-cli、vite,光环境配置就折腾了半天,还没写一行Vue代码就已经劝退了?
这篇文章就是为你准备的。我们将回到Vue最纯粹的形态------原生的Vuejs,不依赖任何构建工具,一个HTML文件加一个CDN链接就能跑起来。你会发现,Vue的核心思想其实非常简单,而掌握了原生Vue,你再去学工程化的Vue项目就会事半功倍。
本文将从痛点场景出发,讲清楚原生Vuejs是什么、为什么要用、它是怎么演进过来的、具体怎么用、在企业项目中如何落地,最后附上竞品对比和高频面试题,让你一篇文章彻底搞懂。
文章目录
- 07、原生的Vuejs:从入门到企业实战,一篇搞懂无构建开发
-
- 前言
- 一、痛点场景:原生JS开发的噩梦
-
- [1.1 真实场景还原](#1.1 真实场景还原)
- [1.2 大白话理解](#1.2 大白话理解)
- [1.3 生活案例](#1.3 生活案例)
- 二、痛点的解决方案:Vue.js的数据驱动
-
- [2.1 什么是数据驱动](#2.1 什么是数据驱动)
- [2.2 声明式 vs 命令式](#2.2 声明式 vs 命令式)
- 三、什么是原生的Vuejs
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话理解](#3.2 大白话理解)
- [3.3 两种引入方式](#3.3 两种引入方式)
- 四、为什么要学习原生Vuejs
-
- [4.1 学习成本低,快速建立信心](#4.1 学习成本低,快速建立信心)
- [4.2 理解核心概念的最佳途径](#4.2 理解核心概念的最佳途径)
- [4.3 老旧项目改造的利器](#4.3 老旧项目改造的利器)
- [4.4 快速原型开发](#4.4 快速原型开发)
- [4.5 生活案例](#4.5 生活案例)
- 五、Vue.js是怎么演进过来的
-
- [5.1 前传:前端开发的黑暗时代](#5.1 前传:前端开发的黑暗时代)
- [5.2 Vue的诞生与成长](#5.2 Vue的诞生与成长)
- [5.3 从Options API到Composition API](#5.3 从Options API到Composition API)
- [5.4 生活案例](#5.4 生活案例)
- 六、怎么用原生Vuejs
- 七、企业项目中如何使用原生Vuejs
-
- [7.1 场景一:渐进式增强老旧项目](#7.1 场景一:渐进式增强老旧项目)
- [7.2 场景二:管理后台快速开发](#7.2 场景二:管理后台快速开发)
- [7.3 场景三:WordPress等CMS中的交互组件](#7.3 场景三:WordPress等CMS中的交互组件)
- [7.4 企业使用注意事项](#7.4 企业使用注意事项)
- [八、竞品对比:Vue vs React vs Angular](#八、竞品对比:Vue vs React vs Angular)
-
- [8.1 核心对比表](#8.1 核心对比表)
- [8.2 各框架的优势](#8.2 各框架的优势)
- [8.3 各框架的劣势](#8.3 各框架的劣势)
- [8.4 大白话理解](#8.4 大白话理解)
- [8.5 选型建议](#8.5 选型建议)
- 九、常用场景教学
-
- [9.1 场景一:TodoList待办事项](#9.1 场景一:TodoList待办事项)
- [9.2 场景二:实时搜索过滤](#9.2 场景二:实时搜索过滤)
- [9.3 场景三:选项卡切换](#9.3 场景三:选项卡切换)
- [9.4 场景四:表单验证](#9.4 场景四:表单验证)
- 十、面试官高频面试题
-
- [10.1 Vue的响应式原理是什么?Vue2和Vue3有什么区别?](#10.1 Vue的响应式原理是什么?Vue2和Vue3有什么区别?)
- [10.2 computed和watch的区别?](#10.2 computed和watch的区别?)
- [10.3 v-if和v-show的区别?](#10.3 v-if和v-show的区别?)
- [10.4 为什么v-for必须加key?](#10.4 为什么v-for必须加key?)
- [10.5 Vue组件之间的通信方式有哪些?](#10.5 Vue组件之间的通信方式有哪些?)
- [10.6 Vue的生命周期有哪些?每个阶段做什么?](#10.6 Vue的生命周期有哪些?每个阶段做什么?)
- [10.7 虚拟DOM是什么?有什么优势?](#10.7 虚拟DOM是什么?有什么优势?)
- [10.8 Vue 2和Vue 3的主要区别?](#10.8 Vue 2和Vue 3的主要区别?)
- [10.9 什么是单向数据流?为什么不能直接修改props?](#10.9 什么是单向数据流?为什么不能直接修改props?)
- [10.10 原生Vue和工程化Vue项目的区别?](#10.10 原生Vue和工程化Vue项目的区别?)
- 十一、总结
一、痛点场景:原生JS开发的噩梦

1.1 真实场景还原
假设你正在开发一个商品列表页面,需求很简单:从后端获取商品数据,渲染成列表,用户可以点击"加入购物车",购物车数量实时更新,还能按价格排序。
用原生JavaScript+jQuery实现,你的代码大概是这样的:
javascript
// 原生JS实现商品列表
let products = [];
let cartCount = 0;
// 获取数据
function fetchProducts() {
$.ajax({
url: '/api/products',
success: function(data) {
products = data;
renderProducts();
}
});
}
// 渲染列表
function renderProducts() {
var html = '';
for (var i = 0; i < products.length; i++) {
html += '<div class="product-item">';
html += '<h3>' + products[i].name + '</h3>';
html += '<p>价格:' + products[i].price + '</p>';
html += '<button onclick="addToCart(' + i + ')">加入购物车</button>';
html += '</div>';
}
$('#product-list').html(html);
}
// 加入购物车
function addToCart(index) {
cartCount++;
$('#cart-count').text(cartCount);
// 还要更新购物车图标、弹出提示、同步到后端...
}
// 排序
function sortByPrice() {
products.sort(function(a, b) {
return a.price - b.price;
});
renderProducts(); // 整个列表重新渲染
}
看着好像还行?但随着需求增加,问题就来了:
- 数据变了,视图不会自动变 :你修改了
products数组,必须手动调用renderProducts(),万一忘了调用,页面就不更新。 - 全量重新渲染:每次排序都把整个列表的HTML重新生成一遍,性能差,用户输入的状态也会丢失。
- DOM操作分散 :获取元素用
getElementById,更新内容用innerHTML,事件绑定用onclick,代码散落在各个函数里,维护起来像在解毛线团。 - 状态管理混乱 :
cartCount是全局变量,谁都能改,出了bug不知道是谁改的。
1.2 大白话理解
打个比方,原生JS开发就像你手动装修房子:数据是装修材料,DOM是房子本身。每次材料变了,你得自己跑去把旧的拆下来,再把新的装上去。材料种类少还行,一旦材料多了、变化频繁了,你就会疲于奔命,而且很容易装错地方。
Vue.js做的事情,就是给你请了一个智能装修队。你只需要告诉它"这个位置用这种材料",以后材料变了,装修队自动帮你把旧的换掉、新的装上,你再也不用关心具体怎么拆怎么装了。
1.3 生活案例
想象你在经营一家奶茶店,菜单是手写在黑板上的。每次价格变动或者新品上架,你都要拿粉笔擦掉重写。如果只有一块黑板还好,但如果你店里有五块黑板、三个电子屏、两个外卖平台都要同步更新菜单,你是不是会疯掉?
原生JS开发就是这样,数据(菜单)变了,你得手动去更新每一个展示的地方(黑板、电子屏、外卖平台)。而Vue.js就是一个菜单管理系统,你只需要在系统里改一次,所有展示的地方自动同步更新。
二、痛点的解决方案:Vue.js的数据驱动
Vue.js的核心思想只有八个字:数据驱动,声明式渲染。
2.1 什么是数据驱动
数据驱动就是:你只需要关心数据,数据变了,视图自动更新。你不需要手动操作DOM,Vue帮你搞定。
用Vue重写上面的商品列表:
html
<div id="app">
<div class="product-item" v-for="product in sortedProducts" :key="product.id">
<h3>{{ product.name }}</h3>
<p>价格:{{ product.price }}</p>
<button @click="addToCart(product)">加入购物车</button>
</div>
<p>购物车数量:{{ cartCount }}</p>
<button @click="sortByPrice">按价格排序</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const products = ref([]);
const cartCount = ref(0);
const sortAsc = ref(true);
const sortedProducts = computed(() => {
return [...products.value].sort((a, b) =>
sortAsc.value ? a.price - b.price : b.price - a.price
);
});
function addToCart(product) {
cartCount.value++;
// 视图自动更新,不需要手动操作DOM
}
function sortByPrice() {
sortAsc.value = !sortAsc.value;
// 计算属性自动重新计算,视图自动更新
}
// 初始化数据
fetch('/api/products')
.then(res => res.json())
.then(data => { products.value = data; });
return { products, cartCount, sortedProducts, addToCart, sortByPrice };
}
}).mount('#app');
</script>
发现区别了吗?
- 没有
getElementById,没有innerHTML,没有手动渲染函数。 - 模板里用
{``{ product.name }}声明"这里显示商品名称",数据变了自动更新。 - 用
v-for声明"遍历products数组生成列表",数组变了列表自动更新。 - 用
@click声明"点击按钮执行addToCart方法",不需要手动绑定事件。
2.2 声明式 vs 命令式
原生JS是命令式编程:你要告诉浏览器"第一步做什么,第二步做什么",像在写操作手册。
Vue是声明式编程:你只需要告诉浏览器"我想要什么结果",具体怎么实现由Vue来搞定,像在点外卖,你只需要说"我要一份宫保鸡丁",不需要告诉厨师怎么切菜怎么炒菜。
三、什么是原生的Vuejs

3.1 专业解释
所谓原生的Vuejs,就是独立的Vue.js框架,不与Webpack、Vite等构建工具结合使用,直接通过CDN链接或本地JS文件引入到HTML页面中使用的方式。
在这种模式下:
- 不需要安装Node.js
- 不需要配置构建工具
- 不需要学习模块化打包
- 一个HTML文件就能运行Vue应用
- 浏览器直接运行你写的源码
3.2 大白话理解
原生Vuejs就像方便面:打开包装,开水一泡就能吃,不需要锅碗瓢盆,不需要厨艺。
工程化的Vue项目(Vue CLI/Vite)就像自己在家做面条:你需要面粉、擀面杖、锅、调料,还要掌握和面、擀面、煮面的技巧,虽然麻烦,但做出来的面条更劲道、可以加各种浇头。
对于新手来说,先吃方便面(原生Vue)体会一下味道,再去学做手擀面(工程化Vue),会更容易理解其中的原理。
3.3 两种引入方式
方式一:全局构建版本(CDN)
html
<!-- 引入Vue 3全局构建版本 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 使用 -->
<div id="app">{{ message }}</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return { message: 'Hello Vue!' };
}
}).mount('#app');
</script>
方式二:ES模块构建版本(importmap)
html
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'vue';
createApp({
setup() {
const message = ref('Hello Vue!');
return { message };
}
}).mount('#app');
</script>
两种方式的区别:全局构建版本通过Vue全局对象访问API,ES模块版本通过import导入API。对于新手来说,推荐先用全局构建版本,更简单直观。
四、为什么要学习原生Vuejs
4.1 学习成本低,快速建立信心
不需要搭建环境,打开记事本写一个HTML文件,浏览器一打开就能看到效果。对于零基础的小白来说,这种即时反馈非常重要,能快速建立学习信心。
4.2 理解核心概念的最佳途径
Vue官方文档中的很多概念,都是用原生Vue的形式来说明的。脱离了构建工具的干扰,你能更专注地理解响应式、指令、组件等核心概念。
4.3 老旧项目改造的利器
企业里有大量维护了五六年甚至十年的老旧项目,这些项目可能是JSP、PHP、ASP.NET写的,前端就是jQuery。你不可能把整个项目推倒重写,但可以用原生Vue渐进式地增强:在某个页面引入Vue CDN,把复杂的交互模块用Vue重写,其他部分保持不变。
4.4 快速原型开发
有时候产品经理让你做个Demo验证想法,或者你需要快速做个活动页面,用原生Vue几分钟就能搞定,不需要初始化工程、配置路由、配置构建。
4.5 生活案例
学原生Vue就像学开车先学手动挡。虽然现在大部分人开自动挡,但学会了手动挡,你才能真正理解离合器、变速箱的工作原理,开自动挡的时候也能更从容。而且万一遇到特殊情况(比如老旧项目),你也能上手。
五、Vue.js是怎么演进过来的

5.1 前传:前端开发的黑暗时代
在Vue出现之前,前端开发是这样的:
- 2006年:jQuery发布,简化了DOM操作和AJAX,成为前端标配。
- 2009年:AngularJS发布,提出了双向数据绑定和MVVM模式,但学习曲线陡峭。
- 2013年:React发布,提出了虚拟DOM和组件化思想,但需要配合一堆工具链。
那时候的前端开发者,要么用jQuery手动操作DOM,要么用AngularJS忍受陡峭的学习曲线,要么用React折腾复杂的工具链。大家都在期待一个既简单又强大的框架。
5.2 Vue的诞生与成长
2014年2月:尤雨溪(Evan You)发布了Vue.js的第一个版本。当时他还在Google工作,做AngularJS的原型开发,觉得AngularJS太复杂了,于是提取了自己喜欢的部分,做了一个更轻量的框架。
Vue这个名字的由来:Vue的发音和"view"相同,因为它是一个视图层框架。logo是一个V形,代表"view"的首字母。
2016年10月:Vue 2.0发布,引入了虚拟DOM,性能大幅提升,支持服务端渲染。这是Vue成为主流框架的关键里程碑。
2020年9月:Vue 3.0 "One Piece"发布,这是一个重大升级:
- 用Proxy替代Object.defineProperty实现响应式,解决了Vue 2中数组索引和新增属性无法监听的问题
- 引入Composition API,解决了大型组件中逻辑分散、难以复用的问题
- 更好的TypeScript支持
- 更小的打包体积,更快的运行速度
2022年2月7日:Vue 3正式成为默认版本。
2023年-2024年:Vue 3.3、3.4、3.5陆续发布,持续优化性能和开发体验。Vue 3.5重新设计了响应式系统的内部结构,使用版本计数和双向链表,性能进一步提升。
2023年12月31日:Vue 2正式到达终止支持(EOL)时间,不再维护。
5.3 从Options API到Composition API
Vue 2时代,我们用Options API写组件:
javascript
// Options API 风格
export default {
data() {
return { count: 0, name: '' };
},
computed: {
doubleCount() { return this.count * 2; }
},
methods: {
increment() { this.count++; }
},
mounted() {
console.log('组件挂载了');
}
};
这种方式的问题是:同一个功能的逻辑被分散到data、computed、methods、mounted等不同选项中。当组件有几百行代码时,你要理解一个功能,得在各个选项之间跳来跳去。
Vue 3引入了Composition API:
javascript
// Composition API 风格
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
const name = ref('');
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
onMounted(() => {
console.log('组件挂载了');
});
return { count, name, doubleCount, increment };
}
};
Composition API把相关的逻辑组织在一起,代码更清晰,逻辑更容易复用。你可以把一组相关的状态和方法抽成一个自定义函数(composable),在多个组件中复用。
5.4 生活案例
Vue的演进就像手机的进化:
- Vue 1.0像功能机,能打电话发短信,但功能有限。
- Vue 2.0像早期智能手机,有了APP商店(虚拟DOM),能做很多事了。
- Vue 3.0像最新的旗舰手机,性能更强、系统更流畅(Proxy响应式),还有了分屏功能(Composition API),能同时处理多个任务。
六、怎么用原生Vuejs
这一部分是全文的核心,我会从最基础的开始,一步步带你上手原生Vuejs。所有代码都可以直接复制到HTML文件中运行。
6.1 第一个Vue应用
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>第一个Vue应用</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>{{ message }}</h1>
<p>当前计数:{{ count }}</p>
<button @click="count++">点我加1</button>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
message: 'Hello, 原生Vuejs!',
count: 0
};
}
}).mount('#app');
</script>
</body>
</html>
代码解释:
<script src="...vue.global.js">:引入Vue 3全局构建版本。<div id="app">:Vue应用的挂载点,Vue会管理这个元素内部的所有内容。{``{ message }}:插值语法,把数据渲染到页面上。@click="count++":事件绑定,点击按钮时count加1。createApp({...}).mount('#app'):创建Vue应用并挂载到#app元素上。data():返回组件的响应式数据,数据变了视图自动更新。
6.2 模板语法
插值
html
<div id="app">
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- 原始HTML(注意XSS风险) -->
<p v-html="rawHtml"></p>
<!-- 属性绑定 -->
<img v-bind:src="imageUrl" :alt="imageAlt">
<!-- 简写:v-bind: 可以简写为 : -->
<a :href="url">链接</a>
<!-- JavaScript 表达式 -->
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</div>
指令
Vue指令是带有v-前缀的特殊属性,用于在模板中声明式地实现各种功能。
html
<div id="app">
<!-- v-if:条件渲染 -->
<p v-if="show">我会显示</p>
<p v-else>我会隐藏</p>
<!-- v-show:切换display -->
<p v-show="show">我也会显示</p>
<!-- v-for:列表渲染 -->
<ul>
<li v-for="(item, index) in list" :key="item.id">
{{ index }} - {{ item.name }}
</li>
</ul>
<!-- v-on:事件绑定,简写为 @ -->
<button v-on:click="handleClick">点击</button>
<button @click="handleClick">点击(简写)</button>
<!-- v-model:双向绑定 -->
<input v-model="inputValue">
<p>你输入的是:{{ inputValue }}</p>
</div>
6.3 条件渲染
v-if和v-show都能控制元素的显示和隐藏,但原理不同:
v-if:真正的条件渲染,会销毁和重建DOM元素,切换开销大,适合不频繁切换的场景。v-show:只是切换CSS的display属性,初始渲染开销大,适合频繁切换的场景。
html
<div id="app">
<button @click="show = !show">切换显示</button>
<!-- v-if:不显示时元素不存在于DOM中 -->
<p v-if="show">v-if控制的内容</p>
<!-- v-show:不显示时元素还在DOM中,只是display:none -->
<p v-show="show">v-show控制的内容</p>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return { show: true };
}
}).mount('#app');
</script>
6.4 列表渲染
html
<div id="app">
<h3>商品列表</h3>
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }} - ¥{{ product.price }}
<button @click="removeProduct(product.id)">删除</button>
</li>
</ul>
<button @click="addProduct">添加商品</button>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
products: [
{ id: 1, name: 'iPhone 15', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 14999 },
{ id: 3, name: 'AirPods Pro', price: 1899 }
],
nextId: 4
};
},
methods: {
addProduct() {
this.products.push({
id: this.nextId++,
name: '新产品' + this.nextId,
price: 999
});
},
removeProduct(id) {
this.products = this.products.filter(p => p.id !== id);
}
}
}).mount('#app');
</script>
注意:key的重要性:key帮助Vue识别每个节点的身份,在列表更新时能更高效地复用和重排元素。永远用唯一且稳定的值作为key,不要用数组索引。
6.5 事件处理
html
<div id="app">
<!-- 基础事件 -->
<button @click="count++">计数:{{ count }}</button>
<!-- 事件修饰符 -->
<form @submit.prevent="onSubmit">
<input type="text" v-model="message">
<!-- .prevent 阻止默认行为(表单提交刷新页面) -->
<button type="submit">提交</button>
</form>
<!-- 按键修饰符 -->
<input @keyup.enter="onEnter" placeholder="按回车提交">
<!-- 传入事件对象 -->
<button @click="warn('不能点', $event)">点我</button>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return { count: 0, message: '' };
},
methods: {
onSubmit() {
alert('提交了:' + this.message);
},
onEnter() {
alert('你按了回车键');
},
warn(msg, event) {
alert(msg);
console.log(event);
}
}
}).mount('#app');
</script>
常用事件修饰符:
.stop:阻止事件冒泡.prevent:阻止默认行为.capture:使用捕获模式.self:只在事件目标是自身时触发.once:只触发一次
6.6 双向绑定v-model
v-model是Vue的一大特色,它能让表单输入和数据保持同步:
html
<div id="app">
<h3>用户注册</h3>
<!-- 文本输入 -->
<p>用户名:<input v-model="form.username" placeholder="请输入用户名"></p>
<!-- 多行文本 -->
<p>个人简介:<textarea v-model="form.bio"></textarea></p>
<!-- 复选框 -->
<p>
兴趣爱好:
<label><input type="checkbox" v-model="form.hobbies" value="reading"> 阅读</label>
<label><input type="checkbox" v-model="form.hobbies" value="music"> 音乐</label>
<label><input type="checkbox" v-model="form.hobbies" value="sports"> 运动</label>
</p>
<!-- 单选框 -->
<p>
性别:
<label><input type="radio" v-model="form.gender" value="male"> 男</label>
<label><input type="radio" v-model="form.gender" value="female"> 女</label>
</p>
<!-- 下拉选择 -->
<p>
城市:
<select v-model="form.city">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
</p>
<p>表单数据:{{ JSON.stringify(form, null, 2) }}</p>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
form: {
username: '',
bio: '',
hobbies: [],
gender: '',
city: ''
}
};
}
}).mount('#app');
</script>
v-model本质上是语法糖,它等价于:value绑定值加上@input监听输入事件。
6.7 计算属性computed
当你需要基于现有数据派生出新数据时,用计算属性:
html
<div id="app">
<p>姓:<input v-model="firstName"></p>
<p>名:<input v-model="lastName"></p>
<p>全名(计算属性):{{ fullName }}</p>
<p>全名(方法):{{ getFullName() }}</p>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return { firstName: '张', lastName: '三' };
},
computed: {
// 计算属性有缓存,依赖不变时不会重新计算
fullName() {
console.log('计算属性执行了');
return this.firstName + this.lastName;
}
},
methods: {
// 方法每次渲染都会执行
getFullName() {
console.log('方法执行了');
return this.firstName + this.lastName;
}
}
}).mount('#app');
</script>
计算属性和方法的区别:计算属性有缓存,只有依赖的数据变化时才会重新计算;方法每次渲染都会执行。对于复杂的计算逻辑,用计算属性性能更好。
6.8 侦听器watch
当你需要在数据变化时执行异步操作或开销较大的操作时,用侦听器:
html
<div id="app">
<p>搜索关键词:<input v-model="keyword"></p>
<p>搜索结果:{{ result }}</p>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return { keyword: '', result: '请输入关键词' };
},
watch: {
// keyword变化时执行
keyword(newVal, oldVal) {
console.log('关键词从 ' + oldVal + ' 变成了 ' + newVal);
this.search(newVal);
}
},
methods: {
search(kw) {
if (!kw) {
this.result = '请输入关键词';
return;
}
this.result = '正在搜索 "' + kw + '"...';
// 模拟异步请求
setTimeout(() => {
this.result = '搜索 "' + kw + '" 的结果:找到100条数据';
}, 500);
}
}
}).mount('#app');
</script>
computed和watch的区别:
- computed:同步计算,有缓存,适合从现有数据派生出新数据。
- watch:异步操作,无缓存,适合数据变化时执行副作用(如发请求、操作DOM)。
6.9 组件基础
组件是Vue最强大的功能之一,它可以把页面拆分成独立、可复用的模块。
html
<div id="app">
<h3>商品列表</h3>
<product-card
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="handleAddToCart"
></product-card>
<p>购物车数量:{{ cartCount }}</p>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
// 定义一个子组件
const ProductCard = {
props: ['product'], // 接收父组件传入的数据
emits: ['add-to-cart'], // 声明要触发的事件
template: `
<div class="product-card">
<h4>{{ product.name }}</h4>
<p>价格:¥{{ product.price }}</p>
<button @click="$emit('add-to-cart', product)">加入购物车</button>
</div>
`
};
createApp({
components: {
ProductCard // 注册子组件
},
data() {
return {
products: [
{ id: 1, name: 'iPhone 15', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 14999 }
],
cartCount: 0
};
},
methods: {
handleAddToCart(product) {
this.cartCount++;
alert('已添加 ' + product.name + ' 到购物车');
}
}
}).mount('#app');
</script>
组件通信的基本原则:
- 父传子:通过
props - 子传父:通过
$emit触发事件 - 兄弟组件:通过共同的父组件中转,或者用事件总线/Pinia
6.10 生命周期钩子
每个Vue组件从创建到销毁都有一个生命周期,Vue在关键节点提供了钩子函数:
javascript
const { createApp } = Vue;
createApp({
data() {
return { message: 'Hello' };
},
// 创建阶段:实例创建之前,data和methods还不可用
beforeCreate() {
console.log('beforeCreate');
},
// 创建完成:data和methods可用,但DOM还没渲染
created() {
console.log('created');
// 适合发请求获取初始数据
},
// 挂载之前:DOM还没渲染到页面
beforeMount() {
console.log('beforeMount');
},
// 挂载完成:DOM已经渲染,可以操作DOM了
mounted() {
console.log('mounted');
// 适合初始化第三方库、操作DOM
},
// 更新之前:数据变了,DOM还没更新
beforeUpdate() {
console.log('beforeUpdate');
},
// 更新完成:DOM已经更新
updated() {
console.log('updated');
},
// 卸载之前:组件还在
beforeUnmount() {
console.log('beforeUnmount');
// 适合清除定时器、取消事件监听
},
// 卸载完成:组件已销毁
unmounted() {
console.log('unmounted');
}
}).mount('#app');
最常用的三个钩子:
created:发请求获取数据mounted:操作DOM、初始化第三方库beforeUnmount:清理定时器、事件监听,防止内存泄漏
七、企业项目中如何使用原生Vuejs

很多人觉得原生Vue只能用来写Demo,其实在企业项目中,原生Vue有很多实际的应用场景。
7.1 场景一:渐进式增强老旧项目
这是原生Vue在企业中最常见的用法。很多公司有维护了多年的老旧系统,技术栈可能是SSH(Struts+Spring+Hibernate)+JSP+jQuery,整个系统有几百个页面,不可能推倒重写。
这时候你可以用原生Vue做渐进式增强:在需要复杂交互的页面引入Vue CDN,把那个页面的交互逻辑用Vue重写,其他页面保持不变。
实战示例:在JSP页面中引入Vue
jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>订单管理</title>
<!-- 项目原有的jQuery和样式 -->
<script src="/js/jquery.min.js"></script>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<!-- 引入Vue -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
</head>
<body>
<!-- 原有的页面头部、导航等保持不变 -->
<jsp:include page="header.jsp" />
<!-- 用Vue管理的区域 -->
<div id="order-app" class="container">
<h3>订单列表</h3>
<div class="form-inline">
<input v-model="searchKeyword" placeholder="搜索订单号" class="form-control">
<select v-model="status" class="form-control">
<option value="">全部状态</option>
<option value="pending">待付款</option>
<option value="paid">已付款</option>
<option value="shipped">已发货</option>
</select>
<button @click="loadOrders" class="btn btn-primary">搜索</button>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>订单号</th>
<th>客户</th>
<th>金额</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="order in orders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>{{ order.customerName }}</td>
<td>¥{{ order.amount }}</td>
<td>
<span :class="getStatusClass(order.status)">
{{ getStatusText(order.status) }}
</span>
</td>
<td>
<button @click="viewDetail(order)" class="btn btn-sm btn-info">详情</button>
</td>
</tr>
<tr v-if="orders.length === 0">
<td colspan="5" class="text-center">暂无数据</td>
</tr>
</tbody>
</table>
<!-- 分页 -->
<nav>
<ul class="pagination">
<li :class="{disabled: page <= 1}">
<a @click="page > 1 && (page--, loadOrders())">上一页</a>
</li>
<li class="active"><a>{{ page }} / {{ totalPages }}</a></li>
<li :class="{disabled: page >= totalPages}">
<a @click="page < totalPages && (page++, loadOrders())">下一页</a>
</li>
</ul>
</nav>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
searchKeyword: '',
status: '',
orders: [],
page: 1,
pageSize: 10,
total: 0
};
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize);
}
},
created() {
this.loadOrders();
},
methods: {
loadOrders() {
// 用jQuery发请求(项目原有方式)
$.ajax({
url: '/api/orders',
data: {
keyword: this.searchKeyword,
status: this.status,
page: this.page,
pageSize: this.pageSize
},
success: (res) => {
this.orders = res.data;
this.total = res.total;
}
});
},
getStatusText(status) {
const map = { pending: '待付款', paid: '已付款', shipped: '已发货' };
return map[status] || status;
},
getStatusClass(status) {
const map = { pending: 'label label-warning', paid: 'label label-success', shipped: 'label label-info' };
return map[status] || '';
},
viewDetail(order) {
// 跳转到详情页(项目原有路由方式)
window.location.href = '/order/detail?id=' + order.id;
}
}
}).mount('#order-app');
</script>
</body>
</html>
这种方式的好处:
- 不需要改动项目的构建流程
- 可以和原有的jQuery、Bootstrap共存
- 只改造需要复杂交互的页面,风险可控
- 团队不需要学习新的构建工具
7.2 场景二:管理后台快速开发
很多中小公司的管理后台,功能不复杂,但页面多、交互重复。用原生Vue+Element UI(通过CDN引入),可以快速开发,不需要搭建工程化项目。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户管理后台</title>
<!-- Element Plus 样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<!-- Vue -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Element Plus -->
<script src="https://unpkg.com/element-plus"></script>
<!-- Axios -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-container style="height: 100vh;">
<el-header>
<h2>用户管理系统</h2>
</el-header>
<el-main>
<!-- 搜索栏 -->
<el-form :inline="true">
<el-form-item label="用户名">
<el-input v-model="searchForm.username" placeholder="请输入"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择">
<el-option label="全部" value=""></el-option>
<el-option label="启用" value="1"></el-option>
<el-option label="禁用" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadUsers">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button type="success" @click="openAddDialog">新增用户</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="userList" border stripe>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="phone" label="手机号"></el-table-column>
<el-table-column label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteUser(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
style="margin-top: 20px;"
background
layout="prev, pager, next, total"
:total="total"
:current-page="page"
:page-size="pageSize"
@current-change="handlePageChange"
></el-pagination>
</el-main>
</el-container>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="500px">
<el-form :model="userForm" label-width="80px">
<el-form-item label="用户名">
<el-input v-model="userForm.username"></el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="userForm.email"></el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="userForm.phone"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="userForm.status" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveUser">确定</el-button>
</template>
</el-dialog>
</div>
<script>
const { createApp, ref, reactive } = Vue;
const app = createApp({
setup() {
const searchForm = reactive({ username: '', status: '' });
const userList = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const dialogVisible = ref(false);
const isEdit = ref(false);
const userForm = reactive({ id: null, username: '', email: '', phone: '', status: 1 });
function loadUsers() {
axios.get('/api/users', {
params: { ...searchForm, page: page.value, pageSize: pageSize.value }
}).then(res => {
userList.value = res.data.list;
total.value = res.data.total;
});
}
function resetSearch() {
searchForm.username = '';
searchForm.status = '';
page.value = 1;
loadUsers();
}
function handlePageChange(p) {
page.value = p;
loadUsers();
}
function openAddDialog() {
isEdit.value = false;
Object.assign(userForm, { id: null, username: '', email: '', phone: '', status: 1 });
dialogVisible.value = true;
}
function openEditDialog(row) {
isEdit.value = true;
Object.assign(userForm, row);
dialogVisible.value = true;
}
function saveUser() {
const url = isEdit.value ? '/api/users/' + userForm.id : '/api/users';
const method = isEdit.value ? 'put' : 'post';
axios[method](url, userForm).then(() => {
ElementPlus.ElMessage.success('保存成功');
dialogVisible.value = false;
loadUsers();
});
}
function deleteUser(row) {
ElementPlus.ElMessageBox.confirm('确定删除用户 ' + row.username + ' 吗?', '提示', {
type: 'warning'
}).then(() => {
axios.delete('/api/users/' + row.id).then(() => {
ElementPlus.ElMessage.success('删除成功');
loadUsers();
});
}).catch(() => {});
}
loadUsers();
return {
searchForm, userList, total, page, pageSize,
dialogVisible, isEdit, userForm,
loadUsers, resetSearch, handlePageChange,
openAddDialog, openEditDialog, saveUser, deleteUser
};
}
});
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>
这种方式适合:
- 内部管理系统,对首屏加载速度要求不高
- 快速验证业务想法
- 小团队没有前端工程化能力
- 临时活动页面、运营页面
7.3 场景三:WordPress等CMS中的交互组件
很多企业官网用WordPress搭建,但有些页面需要复杂的交互(如产品筛选器、价格计算器、在线配置器)。这时候可以用原生Vue写一个交互组件,嵌入到WordPress页面中。
html
<!-- 在WordPress文章/页面中插入Vue组件 -->
<div id="price-calculator">
<h3>产品价格计算器</h3>
<div class="form-group">
<label>产品类型:</label>
<select v-model="productType">
<option value="basic">基础版 - ¥99/月</option>
<option value="pro">专业版 - ¥299/月</option>
<option value="enterprise">企业版 - ¥999/月</option>
</select>
</div>
<div class="form-group">
<label>用户数量:</label>
<input type="number" v-model.number="userCount" min="1" max="1000">
</div>
<div class="form-group">
<label>
<input type="checkbox" v-model="needSupport"> 购买技术支持(+20%)
</label>
</div>
<div class="result">
<h4>预估费用:¥{{ totalPrice }} / 月</h4>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
productType: 'basic',
userCount: 5,
needSupport: false
};
},
computed: {
basePrice() {
const prices = { basic: 99, pro: 299, enterprise: 999 };
return prices[this.productType];
},
totalPrice() {
let price = this.basePrice * this.userCount;
if (this.needSupport) {
price = price * 1.2;
}
return price.toFixed(2);
}
}
}).mount('#price-calculator');
</script>
7.4 企业使用注意事项
- 生产环境用生产版本 :开发时用
vue.global.js,生产环境用vue.global.prod.js,体积更小、性能更好。
html
<!-- 开发环境 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 生产环境 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
- CDN稳定性:不要直接用unpkg的最新版本,指定具体版本号,避免意外升级导致兼容性问题。
html
<!-- 推荐:指定版本号 -->
<script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script>
<!-- 不推荐:用latest,可能意外升级 -->
<script src="https://unpkg.com/vue@latest/dist/vue.global.prod.js"></script>
- 国内CDN加速:如果用户主要在国内,可以用jsdelivr或字节跳动的CDN,加载更快。
html
<!-- jsdelivr -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.21/dist/vue.global.prod.js"></script>
- SEO考虑:原生Vue是客户端渲染,对SEO不友好。如果是需要SEO的页面(如商品详情页),考虑用Nuxt.js做服务端渲染,或者用后端模板渲染主要内容、Vue只做交互增强。
八、竞品对比:Vue vs React vs Angular

选择前端框架是每个团队都会面临的决策。下面从多个维度对比三大主流框架。
8.1 核心对比表
| 对比维度 | Vue.js | React | Angular |
|---|---|---|---|
| 开发者 | 尤雨溪(前Google工程师)+ 社区 | Meta(Facebook) | |
| 首次发布 | 2014年 | 2013年 | 2016年(Angular 2+) |
| 学习曲线 | 平缓,HTML+JS基础即可上手 | 中等,需要理解JSX和函数式编程 | 陡峭,需要掌握TypeScript、依赖注入、RxJS等 |
| 模板语法 | 基于HTML的模板语法,接近原生HTML | JSX(JavaScript XML),在JS中写HTML | TypeScript + 模板语法,装饰器风格 |
| 响应式方式 | 数据劫持(Vue2用Object.defineProperty,Vue3用Proxy) | 不可变数据 + 重新渲染 | 脏检查 + Zone.js |
| 状态管理 | Pinia(官方推荐)/ Vuex | Redux / Zustand / Jotai | 内置状态管理 + RxJS |
| 生态系统 | 快速增长,官方维护路由、状态管理 | 最庞大,第三方库最丰富 | 完整,官方提供全套解决方案 |
| 适用场景 | 中小型项目、快速原型、渐进式增强 | 大型应用、跨平台(React Native) | 大型企业级应用、复杂业务系统 |
| 国内采用度 | 非常高,中小公司首选 | 高,大厂和创业公司常用 | 中等,外企和大型企业较多 |
| 原生开发支持 | uni-app、Weex | React Native(成熟) | NativeScript |
| TypeScript支持 | Vue 3良好支持 | 良好支持 | 原生支持(强制使用) |
8.2 各框架的优势
Vue.js的优势:
- 学习成本最低,文档友好,中文文档完善
- 渐进式框架,可以从一个页面逐步引入
- 模板语法直观,写起来像写HTML
- 官方维护路由和状态管理,选型统一
- 国内社区活跃,问题容易找到答案
React的优势:
- 生态系统最丰富,几乎任何需求都有现成的库
- 函数式编程思想,组件复用性强
- React Native可以一套代码跑iOS和Android
- 大厂背书,就业机会多
- 不可变数据模式,大型项目更可控
Angular的优势:
- 全家桶方案,路由、状态管理、HTTP、测试都内置
- TypeScript原生支持,代码可维护性强
- 依赖注入机制,适合大型团队协作
- Google背书,长期维护有保障
- 适合复杂的企业级应用,有严格的编码规范
8.3 各框架的劣势
Vue.js的劣势:
- 生态相比React还是小一些,某些小众场景缺少成熟方案
- 灵活性高可能导致大型项目架构不统一
- 移动端跨平台方案(uni-app)不如React Native成熟
- 高端岗位相对React少一些
React的劣势:
- 学习曲线中等偏上,JSX和函数式编程需要适应
- 选型自由度过高,路由、状态管理都要自己选,团队容易混乱
- 版本迭代快,API变化较多
- 需要配合大量工具链,配置复杂
Angular的劣势:
- 学习曲线最陡峭,概念多(装饰器、依赖注入、RxJS、模块系统)
- 框架体积大,小型项目显得笨重
- 版本升级有时有破坏性变更
- 国内社区相对小,中文资料不如Vue和React多
8.4 大白话理解
- Vue像自动挡汽车:容易上手,开起来省心,适合大多数人日常使用。
- React像手动挡性能车:操控性强,改装空间大,但需要一定的驾驶技术。
- Angular像重型卡车:功能强大,能拉重载,但车身笨重,需要专业驾照才能开。
8.5 选型建议
- 如果你是个人开发者或小团队,做中小型项目,选Vue,开发效率最高。
- 如果你在大厂或创业公司,需要跨平台移动应用,选React,生态和人才储备最好。
- 如果你在大型企业,做复杂的企业级系统,团队有TypeScript基础,选Angular,规范和可维护性最好。
- 如果你要改造老旧项目,原生Vue是最佳选择,渐进式引入风险最小。
九、常用场景教学
9.1 场景一:TodoList待办事项
这是学习任何框架的经典案例,涵盖了列表渲染、事件处理、双向绑定、计算属性等核心知识点。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>TodoList</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
.done { text-decoration: line-through; color: #999; }
.todo-item { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
</style>
</head>
<body>
<div id="app">
<h2>我的待办事项</h2>
<!-- 添加任务 -->
<div>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入新任务,按回车添加">
<button @click="addTodo">添加</button>
</div>
<!-- 筛选 -->
<div style="margin: 15px 0;">
<button @click="filter = 'all'" :class="{active: filter === 'all'}">全部</button>
<button @click="filter = 'active'" :class="{active: filter === 'active'}">未完成</button>
<button @click="filter = 'done'" :class="{active: filter === 'done'}">已完成</button>
</div>
<!-- 任务列表 -->
<ul>
<li v-for="todo in filteredTodos" :key="todo.id" class="todo-item">
<input type="checkbox" v-model="todo.done">
<span :class="{done: todo.done}">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 统计 -->
<p>共 {{ todos.length }} 项,已完成 {{ doneCount }} 项,未完成 {{ activeCount }} 项</p>
</div>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const newTodo = ref('');
const filter = ref('all');
const todos = ref([
{ id: 1, text: '学习Vue.js', done: true },
{ id: 2, text: '写一篇技术博客', done: false },
{ id: 3, text: '健身30分钟', done: false }
]);
let nextId = 4;
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.value.filter(t => !t.done);
}
if (filter.value === 'done') {
return todos.value.filter(t => t.done);
}
return todos.value;
});
const doneCount = computed(() => todos.value.filter(t => t.done).length);
const activeCount = computed(() => todos.value.filter(t => !t.done).length);
function addTodo() {
const text = newTodo.value.trim();
if (!text) return;
todos.value.push({ id: nextId++, text, done: false });
newTodo.value = '';
}
function removeTodo(id) {
todos.value = todos.value.filter(t => t.id !== id);
}
return { newTodo, filter, todos, filteredTodos, doneCount, activeCount, addTodo, removeTodo };
}
}).mount('#app');
</script>
</body>
</html>
9.2 场景二:实时搜索过滤
html
<div id="app">
<input v-model="keyword" placeholder="搜索商品...">
<ul>
<li v-for="item in filteredList" :key="item.id">
{{ item.name }} - ¥{{ item.price }}
</li>
<li v-if="filteredList.length === 0">没有找到匹配的商品</li>
</ul>
</div>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const keyword = ref('');
const products = ref([
{ id: 1, name: 'iPhone 15', price: 5999 },
{ id: 2, name: 'iPad Air', price: 4799 },
{ id: 3, name: 'MacBook Pro', price: 14999 },
{ id: 4, name: 'AirPods Pro', price: 1899 },
{ id: 5, name: 'Apple Watch', price: 2999 }
]);
const filteredList = computed(() => {
const kw = keyword.value.toLowerCase();
if (!kw) return products.value;
return products.value.filter(p => p.name.toLowerCase().includes(kw));
});
return { keyword, filteredList };
}
}).mount('#app');
</script>
9.3 场景三:选项卡切换
html
<div id="app">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="{active: activeTab === tab.id}"
>
{{ tab.title }}
</button>
</div>
<div class="tab-content">
<component :is="currentTab.component"></component>
</div>
</div>
<script>
const { createApp, ref, computed } = Vue;
// 定义三个选项卡组件
const TabHome = {
template: '<div><h3>首页</h3><p>欢迎来到首页!</p></div>'
};
const TabProfile = {
template: '<div><h3>个人中心</h3><p>这是个人中心页面。</p></div>'
};
const TabSettings = {
template: '<div><h3>设置</h3><p>在这里修改系统设置。</p></div>'
};
createApp({
components: { TabHome, TabProfile, TabSettings },
setup() {
const activeTab = ref('home');
const tabs = ref([
{ id: 'home', title: '首页', component: 'TabHome' },
{ id: 'profile', title: '个人中心', component: 'TabProfile' },
{ id: 'settings', title: '设置', component: 'TabSettings' }
]);
const currentTab = computed(() =>
tabs.value.find(t => t.id === activeTab.value)
);
return { activeTab, tabs, currentTab };
}
}).mount('#app');
</script>
9.4 场景四:表单验证
html
<div id="app">
<form @submit.prevent="handleSubmit">
<div>
<label>用户名:</label>
<input v-model="form.username" @blur="validateUsername">
<span v-if="errors.username" style="color:red">{{ errors.username }}</span>
</div>
<div>
<label>邮箱:</label>
<input v-model="form.email" @blur="validateEmail">
<span v-if="errors.email" style="color:red">{{ errors.email }}</span>
</div>
<div>
<label>密码:</label>
<input type="password" v-model="form.password" @blur="validatePassword">
<span v-if="errors.password" style="color:red">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="!isFormValid">注册</button>
</form>
</div>
<script>
const { createApp, reactive, computed } = Vue;
createApp({
setup() {
const form = reactive({ username: '', email: '', password: '' });
const errors = reactive({ username: '', email: '', password: '' });
function validateUsername() {
if (!form.username) {
errors.username = '用户名不能为空';
} else if (form.username.length < 3) {
errors.username = '用户名至少3个字符';
} else {
errors.username = '';
}
}
function validateEmail() {
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!form.email) {
errors.email = '邮箱不能为空';
} else if (!reg.test(form.email)) {
errors.email = '邮箱格式不正确';
} else {
errors.email = '';
}
}
function validatePassword() {
if (!form.password) {
errors.password = '密码不能为空';
} else if (form.password.length < 6) {
errors.password = '密码至少6个字符';
} else {
errors.password = '';
}
}
const isFormValid = computed(() =>
!errors.username && !errors.email && !errors.password &&
form.username && form.email && form.password
);
function handleSubmit() {
validateUsername();
validateEmail();
validatePassword();
if (isFormValid.value) {
alert('注册成功!' + JSON.stringify(form));
}
}
return { form, errors, isFormValid, validateUsername, validateEmail, validatePassword, handleSubmit };
}
}).mount('#app');
</script>
十、面试官高频面试题

10.1 Vue的响应式原理是什么?Vue2和Vue3有什么区别?
参考答案:
Vue的响应式系统基于数据劫持 和发布-订阅模式。
Vue 2 使用Object.defineProperty来劫持对象属性的getter和setter:
- 在初始化时,遍历data中的所有属性,用
Object.defineProperty把它们转成getter/setter。 - 当属性被读取时(getter),进行依赖收集,把当前的Watcher记录下来。
- 当属性被修改时(setter),通知所有依赖这个属性的Watcher,触发视图更新。
Vue 2的缺点:
- 无法监听对象新增属性和删除属性(需要用
Vue.set和Vue.delete)。 - 无法监听数组索引的变化和长度变化(需要用数组变异方法或
Vue.set)。 - 深层嵌套对象需要递归遍历,性能开销大。
Vue 3 使用Proxy来代理整个对象:
- Proxy可以代理整个对象,不需要遍历每个属性。
- 可以监听对象属性的新增和删除。
- 可以监听数组索引和长度的变化。
- 支持Map、Set等数据结构。
- 懒代理:只有访问到的属性才会被代理,性能更好。
10.2 computed和watch的区别?
参考答案:
| 对比项 | computed(计算属性) | watch(侦听器) |
|---|---|---|
| 用途 | 从现有数据派生出新数据 | 数据变化时执行副作用操作 |
| 缓存 | 有缓存,依赖不变不重新计算 | 无缓存,数据变化就执行 |
| 异步 | 不支持异步 | 支持异步操作 |
| 使用场景 | 格式化数据、过滤列表、计算总价 | 发请求、操作DOM、防抖节流 |
| 返回值 | 必须有返回值 | 不需要返回值 |
简单记:需要计算出新数据用computed,需要在数据变化时"做事情"用watch。
10.3 v-if和v-show的区别?
参考答案:
v-if:真正的条件渲染,不满足条件时元素不会被渲染到DOM中,切换时会销毁和重建组件。适合条件很少改变的场景。v-show:元素始终会被渲染,只是通过CSS的display属性控制显示和隐藏。适合频繁切换的场景。v-if有更高的切换开销,v-show有更高的初始渲染开销。
10.4 为什么v-for必须加key?
参考答案:
key是Vue在更新列表时识别每个节点身份的标识。Vue的虚拟DOM diff算法在对比新旧列表时,会根据key来判断哪些节点可以复用、哪些需要移动、哪些需要销毁重建。
如果不加key,Vue会默认用数组索引作为key,这在列表顺序变化时会导致问题:
- 节点复用错误,输入框等有状态的元素会出现数据错乱。
- diff效率降低,因为Vue无法准确识别节点身份。
key应该使用唯一且稳定的值(如数据的id),不要用数组索引,因为索引会随着列表的增删而变化。
10.5 Vue组件之间的通信方式有哪些?
参考答案:
- 父传子 :通过
props,父组件在子组件标签上绑定属性,子组件用props接收。 - 子传父 :通过
$emit触发事件,子组件触发事件并传递数据,父组件监听事件。 - 兄弟组件:通过共同的父组件中转(子1传父,父传子2),或者用事件总线(mitt)。
- 跨层级通信 :用
provide/inject,祖先组件提供数据,后代组件注入使用。 - 全局状态管理:用Pinia或Vuex,适合复杂的跨组件状态共享。
- $refs :父组件通过
ref获取子组件实例,直接调用子组件的方法或访问数据。
10.6 Vue的生命周期有哪些?每个阶段做什么?
参考答案:
Vue 3的生命周期钩子(Options API形式):
beforeCreate:实例创建之前,data和methods还不可用。created:实例创建完成,data和methods可用,但DOM还没渲染。适合发请求获取初始数据。beforeMount:挂载之前,模板编译完成,但还没渲染到页面。mounted:挂载完成,DOM已经渲染。适合操作DOM、初始化第三方库。beforeUpdate:数据变化后,DOM更新之前。updated:DOM更新完成。注意不要在这里修改数据,可能导致无限循环。beforeUnmount(Vue 2是beforeDestroy):组件卸载之前。适合清除定时器、取消事件监听、取消请求。unmounted(Vue 2是destroyed):组件卸载完成。
最常用的三个:created(发请求)、mounted(操作DOM)、beforeUnmount(清理资源)。
10.7 虚拟DOM是什么?有什么优势?
参考答案:
虚拟DOM是用JavaScript对象来描述真实DOM结构的一种技术。Vue在渲染时,先根据模板生成虚拟DOM树,然后通过diff算法对比新旧虚拟DOM树的差异,最后把差异部分更新到真实DOM上。
优势:
- 性能优化:避免频繁操作真实DOM,把多次DOM修改合并成一次批量更新。
- 跨平台:虚拟DOM是JS对象,可以渲染到不同平台(浏览器、移动端、SSR)。
- 声明式编程:开发者不需要关心具体的DOM操作,只需要关心数据和视图的映射关系。
注意:虚拟DOM不是比直接操作DOM更快,而是在复杂页面、频繁更新的场景下,通过批量更新和diff优化,减少不必要的DOM操作,从而提升整体性能。
10.8 Vue 2和Vue 3的主要区别?
参考答案:
- 响应式原理 :Vue 2用
Object.defineProperty,Vue 3用Proxy。 - API风格:Vue 2主要用Options API,Vue 3新增了Composition API。
- TypeScript支持:Vue 3用TypeScript重写,对TS的支持更好。
- 打包体积:Vue 3通过Tree-shaking,体积更小。
- 性能:Vue 3的虚拟DOM重写了,编译时优化更多,运行时更快。
- 新特性:Vue 3新增了Teleport(传送门)、Suspense(异步组件)、多根节点模板等。
- 状态管理:Vue 2用Vuex,Vue 3推荐用Pinia。
- 生命周期 :Vue 3的
beforeDestroy改名为beforeUnmount,destroyed改名为unmounted。
10.9 什么是单向数据流?为什么不能直接修改props?
参考答案:
单向数据流是指:数据只能从父组件流向子组件,子组件不能直接修改父组件传过来的props。
原因:
- 如果子组件能直接修改props,那么数据的变化来源就不明确了,出了bug很难追踪。
- 当多个子组件依赖同一个父组件数据时,一个子组件修改了数据会影响其他子组件,导致不可预测的行为。
- 单向数据流让数据变化可追溯、可预测,符合"单一数据源"原则。
如果子组件需要修改props的值,应该:
- 通过
$emit触发事件,让父组件修改。 - 或者把props赋值给本地data,在本地修改。
10.10 原生Vue和工程化Vue项目的区别?
参考答案:
| 对比项 | 原生Vue(CDN引入) | 工程化Vue(Vue CLI/Vite) |
|---|---|---|
| 构建工具 | 不需要 | 需要Webpack或Vite |
| 模块化 | 全局变量或ES Module | ES Module,支持组件拆分 |
| 单文件组件 | 不支持(用template字符串) | 支持.vue文件 |
| 热更新 | 不支持 | 支持,开发体验好 |
| 代码压缩 | 需要手动处理 | 自动压缩、Tree-shaking |
| CSS预处理 | 需要手动引入 | 支持Less/Sass/Stylus |
| 路由/状态管理 | CDN引入 | npm安装,模块化使用 |
| 适用场景 | 小型页面、老旧项目改造、快速原型 | 中大型项目、长期维护的项目 |
| 学习成本 | 低 | 较高,需要学习构建工具 |
十一、总结
回顾一下本文的核心内容:
- 原生Vuejs是什么:不依赖构建工具,直接通过CDN引入Vue.js到HTML页面中使用的方式。
- 为什么要学:学习成本低、能深入理解核心概念、是老旧项目改造的利器、适合快速原型开发。
- Vue的演进:从2014年初版到2016年Vue 2.0(虚拟DOM),再到2020年Vue 3.0(Proxy+Composition API),不断进化。
- 怎么用:掌握插值、指令、条件渲染、列表渲染、事件处理、双向绑定、计算属性、侦听器、组件、生命周期等核心概念。
- 企业应用:渐进式增强老旧项目、管理后台快速开发、CMS交互组件等场景都有广泛应用。
- 竞品对比:Vue上手快、React生态大、Angular最完整,根据项目规模和团队情况选择。
- 面试高频题:响应式原理、computed vs watch、v-if vs v-show、key的作用、组件通信、生命周期、虚拟DOM等。
原生Vuejs不是"过时"的技术,而是Vue最纯粹的形态。掌握了原生Vue,你就掌握了Vue的核心思想,再去学工程化的Vue项目就会事半功倍。而且在企业实际工作中,大量的老旧项目改造、快速原型开发、运营活动页面,原生Vue依然是最高效的选择。
希望这篇文章能帮你彻底搞懂原生的Vuejs。动手敲一遍文中的代码,你会发现Vue其实很简单。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。