简介:一份面向Web开发初学者的Vue.js入门笔记,系统梳理前端框架中的核心概念与落地路径。内容从Vue.js的历史和设计哲学入手,逐步讲解组件化、虚拟DOM、双向数据绑定等关键特性,并配合安装配置、使用CDN与npm引入、Vue CLI创建项目等步骤,以及Vue实例中data、methods、computed、watch选项的代码示例,帮助读者理解数据驱动视图的实现方式,适合零基础或希望快速切入Vue的前端新人及在校学生。资源为单个docx文档,大小约39KB,内容精炼、结构完整,便于按章节阅读与随手查阅。目前已有90人浏览学习。文档采用渐进式讲解,知识点前后衔接,既有框架原理说明,也有可直接上手的示例代码,读者可按步骤动手验证,学完后能独立搭建基础Vue应用,为后续深入组件通信、路由与状态管理打下清晰基础。
1. 从第一行 JavaScript 到框架思维
很多人在学 Vue 之前都会卡在一个问题上:我已经会写 JavaScript 了,为什么还要学一个框架?这个问题的答案可以用一句话概括——框架不是在帮你写代码,而是在帮你约束代码的写法。Vue 的核心价值不是让你少写几行代码,而是让「数据和页面」之间的关系变得可预测。你不再需要手动去 DOM 里翻找某个节点然后改它的内容,你只需要告诉 Vue「数据变了」,页面自动跟着变。这个思维转变,才是 Vue 入门真正的门槛。
这篇文章不打算从安装开始一步步讲,而是把 Vue 里最基础的几个概念拆开揉碎:响应式数据、模板语法、条件与列表渲染、组件通信。这些是 Vue 的骨架,也是面试里被问得最频繁的部分。文章里的代码全部是 Vue 3 的组合式 API 写法,因为这是当前的主流用法。如果你用的是 Vue 2 的选项式,先把思路看懂,再对照官方文档迁移也不迟。前端框架入门这件事,学的是思维方式,不是背 API。
2. Vue 响应式系统与单项数据流:先搞懂 v-model 到底做了什么
2.1 从原生 JavaScript 到 Vue 的响应式模型
在原生 JavaScript 里,如果页面上有一个输入框,你要实现「输入内容同步显示在另一个元素上」,通常是这样写的:
const input = document.querySelector('#input'); const output = document.querySelector('#output'); input.addEventListener('input', (e) => { output.textContent = e.target.value; });这段代码本身没有错,但问题在于:数据、监听器、DOM 操作全都散落在不同的位置。如果页面里有十个这样的关联关系,代码就会开始变得难以维护。Vue 对这个问题的解法是引入「响应式数据」的概念——声明一个变量,让页面上的某些部分和它绑定,当变量变化时,绑定的部分自动更新。
Vue 3 里实现响应式的核心是reactive和ref这两个函数。reactive接收一个对象,返回这个对象的响应式代理;ref则更灵活,它可以包裹任意类型的值,包括原始类型。
import { ref } from 'vue'; const count = ref(0); count.value = 1;注意这里访问ref创建的值必须通过.value。这是因为 JavaScript 没有办法对原始类型做代理,只能用一个对象包一层。这个设计经常让新手困惑,但理解了原理之后就顺理成章了:ref的返回值是一个Ref对象,它的value属性才是真正的值。
2.2 v-model 是语法糖:拆开看 v-bind 和 v-on
v-model是 Vue 入门第一个让人「哇」的功能,但也是一直被人误解的功能。它看起来像是在做「双向绑定」,实际上 Vue 的响应式数据流是单向的:数据变化驱动视图更新,视图变化通过事件通知数据。
先看一个最简单的例子:
<script setup> import { ref } from 'vue'; const message = ref(''); </script> <template> <input v-model="message" /> <p>{{ message }}</p> </template>这段代码等价于下面这种写法:
<template> <input :value="message" @input="message = $event.target.value" /> <p>{{ message }}</p> </template>v-model的本质是v-bind:value加v-on:input的语法糖。v-bind把数据传给组件的value属性,v-on监听输入事件,在回调里更新 JavaScript 侧的数据。说它是「双向绑定」是因为你观察到的现象是双向的,但数据流向仍然是「数据 → 视图」和「视图 → 数据」,两个方向都有明确的触发时机。
2.3 v-model 的修饰符与参数细节
v-model还支持修饰符,常用的是.trim、.number和.lazy。
<template> <!-- 自动去除首尾空格 --> <input v-model.trim="username" /> <!-- 自动转数字,注意:如果输入无法解析为数字,会保留原字符串 --> <input v-model.number="age" /> <!-- 从 input 事件改为 change 事件触发,适合开销较大的输入场景 --> <input v-model.lazy="description" /> </template>.lazy修饰符值得单独说一句。默认v-model在input事件触发时同步更新数据,改为.lazy后只在change事件(输入框失焦或回车)时同步。如果你需要做输入过程中的实时搜索,不要用.lazy;如果你只是提交表单时取值,.lazy可以减少更新次数。
2.4 v-model 在自定义组件上的使用
很多初学者第一次在自定义组件上用v-model会踩坑,因为默认的value属性和input事件约定在自定义组件上并不成立。Vue 3 的默认约定是modelValue属性和update:modelValue事件。
<!-- 子组件 Child.vue --> <script setup> const props = defineProps({ modelValue: { type: String, default: '' } }); const emit = defineEmits(['update:modelValue']); function handleInput(event) { emit('update:modelValue', event.target.value); } </script> <template> <input :value="modelValue" @input="handleInput" /> </template>父组件这样使用:
<template> <Child v-model="searchText" /> </template>关键点在于:子组件不能直接修改props.modelValue,必须通过emit通知父组件更新。这是 Vue 单向数据流的核心约束——props 是只读的。违反这个约束的代码也许能跑,但会让数据的来源和修改路径变得混乱,项目一复杂就变成「找 bug 地狱」。
3. 条件渲染与列表渲染:v-if、v-show、v-for 的真实差异与常见坑
3.1 v-if 和 v-show:渲染 vs 切换,不只是性能问题
前端框架入门离不开「页面上这块内容什么时候显示」的问题。Vue 提供了两个指令:v-if和v-show。表面上看它们都能控制元素的显示与隐藏,但实现机制完全不同。
v-if是「真正的条件渲染」。条件为假时,Vue 根本不会把元素渲染到 DOM 里。条件从假变为真时,Vue 会重新创建这个元素及其所有子组件。v-show则是单纯地切换 CSS 的display: none属性,元素始终在 DOM 里存在。
这个差异直接决定了适用的场景:
- 切换频率低:用
v-if,因为它的开销是一次性的懒渲染,而且可以配合else分支 - 切换频率高:用
v-show,因为display切换的性能开销远小于 DOM 的创建和销毁
<template> <div v-if="isAdmin">管理员菜单</div> <div v-else>普通用户菜单</div> <div v-show="isVisible">这个元素始终在 DOM 中</div> </template>另外一个容易忽略的点:v-if和v-for同时出现在同一个元素上时,Vue 3 中v-if的优先级高于v-for。这意味着v-if无法访问v-for循环中的变量,如果写了这种代码,eslint 也会给出vue/no-use-v-if-with-v-for警告。规范的写法是把v-if放到循环体的内部:
<template> <div v-for="item in items" :key="item.id"> <span v-if="item.active">{{ item.name }}</span> </div> </template>3.2 v-for 的 :key 为什么重要,为什么不要用 index 当 key
v-for用来渲染列表,:key是它的必填伴侣。key 的作用是让 Vue 在更新列表时能识别哪些元素被改动、哪些被新增、哪些被删除。没有 key,Vue 只能靠「位置」来对比新旧节点,这会导致状态错乱。
看一个具体场景:列表项本身带有输入状态,比如一个待办事项列表,每项前面有复选框。
<template> <div v-for="(item, index) in todos" :key="item.id"> <input type="checkbox" v-model="item.done" /> <span :class="{ completed: item.done }">{{ item.text }}</span> </div> </template>如果这里用:key="index",当列表中间插入一条数据时,Vue 从中间开始的所有节点都会复用旧数据对应的 DOM,复选框的勾选状态就会整个错位。用item.id作 key,Vue 才能精确地知道哪一项是新增的。规则只有一条:key 必须是列表项中「稳定且唯一」的值,通常来自后端返回的 id。
3.3 列表渲染的过滤器:遍历对象与范围数字
除了遍历数组,v-for还可以遍历对象和数字范围。遍历对象时,第二个参数是键名,第三个参数是索引:
<template> <div v-for="(value, key, index) in userInfo" :key="key"> {{ index }}. {{ key }}: {{ value }} </div> </template>遍历数字范围时,注意 Vue 从 1 开始:
<template> <li v-for="page in totalPages" :key="page">{{ page }}</li> </template>这个写法常见于分页器的实现。totalPages指向一个数字,Vue 会循环 1 到 totalPages 次,从 1 开始计数。如果你习惯了 JavaScript 中数组下标从 0 开始的约定,这里要特别留意。
3.4 过滤列表的正确姿势:computed 而不是方法
「过滤后的列表」是列表渲染里最常见的需求。新手容易直接在模板里写一个方法调用:
<template> <div v-for="item in filterList(searchText)" :key="item.id"> {{ item.name }} </div> </template>这个写法能跑,但每次组件重新渲染时,filterList都会被重新执行一次,哪怕searchText根本没变。更好的做法是用computed缓存计算结果:
<script setup> import { ref, computed } from 'vue'; const searchText = ref(''); const allItems = ref([ { id: 1, name: 'JavaScript 基础' }, { id: 2, name: 'Vue 响应式原理' }, { id: 3, name: '组件通信思路' } ]); const filteredItems = computed(() => { if (!searchText.value) return allItems.value; return allItems.value.filter((item) => item.name.includes(searchText.value) ); }); </script> <template> <input v-model="searchText" /> <ul> <li v-for="item in filteredItems" :key="item.id">{{ item.name }}</li> </ul> </template>computed会基于它内部读取的响应式数据做依赖追踪,只有依赖变化时才会重新计算。这是 Vue 响应式系统设计中最优雅的部分之一,也是区分「会写 Vue」和「还在用 Vue 写 jQuery」的分水岭。
4. 组件通信:props、emits、provide/inject 和插槽的适用边界
4.1 props 单向数据流与命名规范
组件是 Vue 应用的构建单位。父组件向子组件传递数据,最常见的方式是 props。props 是只读的,这个约束前面已经提过,但这里要强调的是为什么要这么做:双向绑定的组件通信会让数据变更的来源变得难以追踪,调试时不知道是哪个组件改了数据。单向数据流强制数据变更必须经过明确的出口,这也就是子组件里的emit。
props 的命名是一个容易踩的小坑。在模板里,多单词 props 用 kebab-case 写法;在传递时,你需要保证传给组件的属性名和子组件声明的 props 名一致:
<!-- 父组件中 --> <Child :post-title="title" :post-content="content" /> <!-- 子组件中 --> <script setup> const props = defineProps({ postTitle: String, postContent: { type: String, default: '' } }); </script>props 的类型声明不仅仅是为了文档化,它还承担运行时校验的功能。在开发模式下,如果传入的类型不匹配,Vue 会在控制台抛出警告。这能帮你提前发现很多隐性问题。
4.2 emits:把事件「申明」出来,不只是调用
子组件要修改数据,或者通知父组件做某件事,通过emit发送事件。defineEmits不只是声明这个组件能触发哪些事件,更重要的是明确了组件的对外接口。
<script setup> const emit = defineEmits(['delete', 'update']); function handleDelete() { emit('delete', itemId.value); } </script>父组件这样监听:
<template> <TodoItem @delete="handleDeleteItem" /> </template>注意事件名的大小写约定。在模板中监听自定义事件时推荐使用 kebab-case,而在子组件定义 emit 时可以使用 camelCase。Vue 会自动做转换,但这个转换有时候会让人困惑,建议团队内统一写法。
4.3 隔层传值:provide/inject 的使用边界
如果组件层级很深,比如 5 层嵌套,中间的层级又不关心数据本身,逐层传 props 会非常繁琐。Vue 提供了provide和inject来处理这种情况。
<!-- 根组件 --> <script setup> import { provide, ref } from 'vue'; const theme = ref('dark'); provide('theme', theme); </script> <!-- 深层子组件 --> <script setup> import { inject } from 'vue'; const theme = inject('theme', 'light'); </script>inject的第二个参数是默认值。如果祖先组件没有提供对应的数据,默认值就会生效。这个特性看起来很美好,但有代价:数据来源变得隐式,组件不再能直观地从 props 中看出来它依赖什么。建议只在「跨多层且不需要中间层感知」的场景使用,比如主题、用户登录态这类全局共享的状态。
4.4 插槽:结构复用的利器
插槽处理的是「组件内部有部分内容需要父组件塞进来」的场景。最典型的就是弹窗组件:弹窗的骨架、遮罩、关闭逻辑都是通用的,但弹窗内容每个页面都不一样。
<!-- BaseDialog.vue --> <script setup> const visible = defineModel('visible', { type: Boolean, default: false }); </script> <template> <div v-if="visible" class="dialog-mask"> <div class="dialog-body"> <slot name="header">默认标题</slot> <slot>默认内容</slot> <slot name="footer"> <button @click="visible = false">关闭</button> </slot> </div> </div> </template>父组件这样填充:
<template> <BaseDialog v-model:visible="showDialog"> <template #header>删除确认</template> <p>删除后无法恢复,确定继续吗?</p> <template #footer> <button @click="showDialog = false">取消</button> <button @click="confirmDelete">确认删除</button> </template> </BaseDialog> </template>具名插槽用#name语法,默认插槽不需要指定名称。一段内容可能同时出现在两个插槽里的情况很罕见,但需要时可以给插槽绑定 props,实现「作用域插槽」,让父组件能拿到子组件内部的数据。这属于进阶用法,入门阶段掌握具名插槽就足够应付大多数弹窗类需求了。
4.5 组件通信方式的选择参考
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 父子组件直接传值 | props 向下,emits 向上 | 最常见,数据流清晰 |
| 兄弟组件共享状态 | 提升到共同父组件 | Vue 官方推荐的层级管理 |
| 深层嵌套组件传值 | provide / inject | 避免逐层透传 |
| 子组件插入内容 | 插槽 slot | 结构与逻辑分离 |
| 跨页面共享状态 | Pinia 等状态库 | 页面级状态,后续进阶 |
组件通信没有银弹,选择的标准只有一个:数据在组件树中传的层级越少,代码越好维护。表格里「提升到共同父组件」是很多初学者容易忽略的思路,多考虑这一层,往往就不需要引入状态管理库了。
5. 模板调试与三个必会的性能优化技巧
调试 Vue 应用的第一件事是安装 Vue Devtools 浏览器扩展。它会自动识别页面上的 Vue 实例,显示组件树和每个组件的数据。配合ref的展开检查,定位「数据对不对、数据在哪个组件里变了」会非常高效。
5.1 性能优化一:能用 computed 就不用方法
前面已经讲过computed的缓存机制,这里补充一个对比数据。模板里每调用一次方法,不管依赖的数据变没变,方法都会执行。十个列表项就是十次执行;如果这个方法是document.querySelectorAll查询 DOM,那开销就更大了。:key原则保持不变,但computed的价值被很多人低估。
5.2 性能优化二:v-once 标记静态内容
如果某个节点渲染一次后就不再变化,给它加上v-once指令。Vue 会跳过这个节点的后续更新检查:
<template> <div v-once> <h1>{{ title }}</h1> <p>这段内容在数据变化时不做更新检查</p> </div> </template>新闻标题、固定文案这类内容适合用v-once,但如果是频繁受数据驱动的区域,加v-once可能反而造成数据不更新,属于双刃剑。
5.3 性能优化三:v-memo 缓存长列表子元素
Vue 3.2 之后提供了v-memo,它接收一个数组,数组里的值不变时,跳过对应子树的更新。长列表场景下这个指令的价值很大:
<template> <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" v-memo="[todo.done, todo.title]" /> </template>用todo.done和todo.title两个字段做 memo 键,只有这两项变化时才会更新这个子元素。注意:v-memo只有在列表渲染时才有明显收益,且它使用的变量必须来自所渲染的元素自身,否则缓存命中会出错。
最后说一个调试技巧:在模板里打断点不方便时,可以在 script 里用watchEffect观察数据变化:
import { watchEffect } from 'vue'; watchEffect(() => { console.log('当前标题是:', props.title); });watchEffect会在依赖变化时自动执行回调,打印出最新的值。定位「哪个数据先变、哪个组件后变」这类问题时,比逐级 console.log 高效得多。
本文还有配套的精品资源,点击获取