前端框架Vue-07_原生的Vue.js

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
      • [6.1 第一个Vue应用](#6.1 第一个Vue应用)
      • [6.2 模板语法](#6.2 模板语法)
      • [6.3 条件渲染](#6.3 条件渲染)
      • [6.4 列表渲染](#6.4 列表渲染)
      • [6.5 事件处理](#6.5 事件处理)
      • [6.6 双向绑定v-model](#6.6 双向绑定v-model)
      • [6.7 计算属性computed](#6.7 计算属性computed)
      • [6.8 侦听器watch](#6.8 侦听器watch)
      • [6.9 组件基础](#6.9 组件基础)
      • [6.10 生命周期钩子](#6.10 生命周期钩子)
    • 七、企业项目中如何使用原生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 企业使用注意事项

  1. 生产环境用生产版本 :开发时用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>
  1. 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>
  1. 国内CDN加速:如果用户主要在国内,可以用jsdelivr或字节跳动的CDN,加载更快。
html 复制代码
<!-- jsdelivr -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.21/dist/vue.global.prod.js"></script>
  1. SEO考虑:原生Vue是客户端渲染,对SEO不友好。如果是需要SEO的页面(如商品详情页),考虑用Nuxt.js做服务端渲染,或者用后端模板渲染主要内容、Vue只做交互增强。

八、竞品对比:Vue vs React vs Angular

选择前端框架是每个团队都会面临的决策。下面从多个维度对比三大主流框架。

8.1 核心对比表

对比维度 Vue.js React Angular
开发者 尤雨溪(前Google工程师)+ 社区 Meta(Facebook) Google
首次发布 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组件之间的通信方式有哪些?

参考答案:

  1. 父传子 :通过props,父组件在子组件标签上绑定属性,子组件用props接收。
  2. 子传父 :通过$emit触发事件,子组件触发事件并传递数据,父组件监听事件。
  3. 兄弟组件:通过共同的父组件中转(子1传父,父传子2),或者用事件总线(mitt)。
  4. 跨层级通信 :用provide/inject,祖先组件提供数据,后代组件注入使用。
  5. 全局状态管理:用Pinia或Vuex,适合复杂的跨组件状态共享。
  6. $refs :父组件通过ref获取子组件实例,直接调用子组件的方法或访问数据。

10.6 Vue的生命周期有哪些?每个阶段做什么?

参考答案:

Vue 3的生命周期钩子(Options API形式):

  1. beforeCreate:实例创建之前,data和methods还不可用。
  2. created:实例创建完成,data和methods可用,但DOM还没渲染。适合发请求获取初始数据。
  3. beforeMount:挂载之前,模板编译完成,但还没渲染到页面。
  4. mounted:挂载完成,DOM已经渲染。适合操作DOM、初始化第三方库。
  5. beforeUpdate:数据变化后,DOM更新之前。
  6. updated:DOM更新完成。注意不要在这里修改数据,可能导致无限循环。
  7. beforeUnmount(Vue 2是beforeDestroy):组件卸载之前。适合清除定时器、取消事件监听、取消请求。
  8. unmounted(Vue 2是destroyed):组件卸载完成。

最常用的三个:created(发请求)、mounted(操作DOM)、beforeUnmount(清理资源)。

10.7 虚拟DOM是什么?有什么优势?

参考答案:

虚拟DOM是用JavaScript对象来描述真实DOM结构的一种技术。Vue在渲染时,先根据模板生成虚拟DOM树,然后通过diff算法对比新旧虚拟DOM树的差异,最后把差异部分更新到真实DOM上。

优势:

  1. 性能优化:避免频繁操作真实DOM,把多次DOM修改合并成一次批量更新。
  2. 跨平台:虚拟DOM是JS对象,可以渲染到不同平台(浏览器、移动端、SSR)。
  3. 声明式编程:开发者不需要关心具体的DOM操作,只需要关心数据和视图的映射关系。

注意:虚拟DOM不是比直接操作DOM更快,而是在复杂页面、频繁更新的场景下,通过批量更新和diff优化,减少不必要的DOM操作,从而提升整体性能。

10.8 Vue 2和Vue 3的主要区别?

参考答案:

  1. 响应式原理 :Vue 2用Object.defineProperty,Vue 3用Proxy。
  2. API风格:Vue 2主要用Options API,Vue 3新增了Composition API。
  3. TypeScript支持:Vue 3用TypeScript重写,对TS的支持更好。
  4. 打包体积:Vue 3通过Tree-shaking,体积更小。
  5. 性能:Vue 3的虚拟DOM重写了,编译时优化更多,运行时更快。
  6. 新特性:Vue 3新增了Teleport(传送门)、Suspense(异步组件)、多根节点模板等。
  7. 状态管理:Vue 2用Vuex,Vue 3推荐用Pinia。
  8. 生命周期 :Vue 3的beforeDestroy改名为beforeUnmount,destroyed改名为unmounted。

10.9 什么是单向数据流?为什么不能直接修改props?

参考答案:

单向数据流是指:数据只能从父组件流向子组件,子组件不能直接修改父组件传过来的props。

原因:

  1. 如果子组件能直接修改props,那么数据的变化来源就不明确了,出了bug很难追踪。
  2. 当多个子组件依赖同一个父组件数据时,一个子组件修改了数据会影响其他子组件,导致不可预测的行为。
  3. 单向数据流让数据变化可追溯、可预测,符合"单一数据源"原则。

如果子组件需要修改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安装,模块化使用
适用场景 小型页面、老旧项目改造、快速原型 中大型项目、长期维护的项目
学习成本 低 较高,需要学习构建工具

十一、总结

回顾一下本文的核心内容:

  1. 原生Vuejs是什么:不依赖构建工具,直接通过CDN引入Vue.js到HTML页面中使用的方式。
  2. 为什么要学:学习成本低、能深入理解核心概念、是老旧项目改造的利器、适合快速原型开发。
  3. Vue的演进:从2014年初版到2016年Vue 2.0(虚拟DOM),再到2020年Vue 3.0(Proxy+Composition API),不断进化。
  4. 怎么用:掌握插值、指令、条件渲染、列表渲染、事件处理、双向绑定、计算属性、侦听器、组件、生命周期等核心概念。
  5. 企业应用:渐进式增强老旧项目、管理后台快速开发、CMS交互组件等场景都有广泛应用。
  6. 竞品对比:Vue上手快、React生态大、Angular最完整,根据项目规模和团队情况选择。
  7. 面试高频题:响应式原理、computed vs watch、v-if vs v-show、key的作用、组件通信、生命周期、虚拟DOM等。

原生Vuejs不是"过时"的技术,而是Vue最纯粹的形态。掌握了原生Vue,你就掌握了Vue的核心思想,再去学工程化的Vue项目就会事半功倍。而且在企业实际工作中,大量的老旧项目改造、快速原型开发、运营活动页面,原生Vue依然是最高效的选择。

希望这篇文章能帮你彻底搞懂原生的Vuejs。动手敲一遍文中的代码,你会发现Vue其实很简单。


转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
帆张芳显1 小时前
前端表格公式能力新选择|智表 ZCell 公式引擎深度剖析
前端·excel·canva可画·excel插件
清水迎朝阳1 小时前
Edge 在线语音 403 问题修复
前端·edge
一碗白开水一1 小时前
Day 5 · 弹性 | 微观
java·前端·数据库
Lsx_1 小时前
微信支付接入全流程:JSAPI支付、小程序支付与H5支付
前端·javascript·全栈
OpenTiny社区1 小时前
React 正式登场!GenUI SDK v1.4.0 三框架渲染、出码与 Agent 生态重磅升级~
前端·ai编程
试图让你心动2 小时前
【无标题】
前端·javascript·vue.js
Hilaku2 小时前
为什么每个前端都应该拥有一个自己的独立博客?
前端·javascript·程序员
燕落南枝2 小时前
CSS 后代选择器与子代选择器:从入门到实战
前端·css
Amos_Web2 小时前
Rspack 源码解析(二十八):HtmlRspackPlugin 如何把模板变成带资源标签的最终产物
前端