Linux 三剑客
可以,你可以把前端基础分成三层:HTML 负责结构,CSS 负责样式,JavaScript 负责逻辑,Vue3 是在 JS 基础上封装出来的组件化开发框架。你现在真正要补的是 JS,因为 Vue3 的 ref、reactive、computed、watch、事件、接口请求、组件通信、路由守卫 本质都依赖 JS。
一、HTML / CSS 快速回忆:够看项目就行
1. HTML:页面结构
HTML 是页面骨架,负责描述“页面有什么”。常见标签不用背全,重点看懂表单、列表、布局、语义标签。
div是独占一行的块级布局容器,span是随文字同行、仅修饰局部文本的行内元素。
<div>普通容器</div>
<span>行内容器</span>
<h1>标题</h1>
<p>段落</p>
<a href="/home">链接</a>
<img src="/logo.png" alt="logo" />
<ul>
<li>列表项</li>
</ul>
<input> 填东西用的(填文本、勾选项、选文件等)
<button>点东西用的(提交、取消、弹窗等)
<form>表单:用来"装"输入框的容器,里面放各种输入框、按钮
input输入框:让用户填东西
type="text":普通文本输入
placeholder`:输入框灰色提示文字。
<input type="text" placeholder="用户名" />
<input type="password" placeholder="密码" />
button 的 type="submit":提交表单(默认行为,会刷新页面)
<button type="submit">提交</button>
</form>
大型 Vue 项目里,HTML 通常写在 .vue 的 <template> 中,但不是纯 HTML,而是 Vue 模板语法增强版:
<template>Vue 页面模板区域,所有页面 HTML 写在这里。
<div class="user-page">普通容器,给页面整体加样式用。
v-model="keyword":双向绑定,输入框里写的文字自动存到变量keyword,页面 / 代码都能读取这个关键词;
<input v-model="keyword" placeholder="请输入关键词" />
<button @click="search">搜索</button>@click="search":点击按钮,自动执行 js 里写好的`search`搜索方法。
<ul>
v-for="user in userList":循环数组userList,数组有多少用户就生成多少个 li;
:key="user.id":固定写法,提升列表渲染性能,必须绑定唯一 id;
<li v-for="user in userList" :key="user.id">
{{ }}:插值语法,把变量 / 数据展示到页面上。
{{ user.name }} user:循环里单个用户对象,user.name取用户名
</li>
</ul>
</div>
</template>
这里 HTML 结构还是 HTML,但多了 Vue 指令:v-model 双向绑定,@click 绑定点击事件,v-for 列表渲染,:key 动态绑定属性。
2. CSS:页面样式
CSS 负责“页面长什么样”。核心就是选择器、盒模型、布局、颜色、字体、间距。
.user-card {
width: 300px;
padding: 16px;
margin-bottom: 12px;
border: 1px solid #ddd;
border-radius: 8px;
}
常见选择器: box、app 都是示例占位词,随便改名,不用固定写
.box {} /* class 选择器,最常用 */ .box {}` class 选择器:给标签加 class="box",多个标签可共用一套样式,日常开发最常用。
#app {} /* id 选择器 */ id 选择器:给标签加 id="app",一个页面 id 只能写一次,极少用来写样式。
button {} /* 标签选择器 */ 标签选择器:直接选中页面所有 button 按钮统一改样式。
.box .title {} /* 后代选择器 */ 后代选择器:只选中.box 内部带.title 的元素,精准缩小范围。
.box:hover {} /* 鼠标悬停状态 */ 伪类悬停:鼠标移到.box 元素上时,自动切换这套样式。
盒模型要记住:width/height 是内容大小,padding 是内边距,border 是边框,margin 是外边距。
.box {
width: 200px;
padding: 20px;
border: 1px solid #ccc;
margin: 10px;
box-sizing: border-box;
}
box-sizing: border-box 很常用,表示 width 包含内容、padding、border,布局更好算。
3. 常见布局:Flex 最重要
现代前端项目里,Flex 用得最多。你先把 Flex 学会,就能看懂大部分后台管理页面布局。
.container {
display: flex;
align-items: center; /* 交叉轴居中,常见是垂直居中 */
justify-content: space-between; /* 主轴两端对齐 */
gap: 12px;
}
常见写法:
.page {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
height: 60px;
}
.main {
flex: 1;
overflow: auto;
}
后台系统常见布局:
<div class="layout">
<aside class="sidebar">菜单</aside>
<main class="content">内容区</main>
</div>
.layout {
display: flex;
height: 100vh;
}
.sidebar {
width: 220px;
}
.content {
flex: 1;
overflow: auto;
}
4. Vue 里的 CSS
.vue 文件中常见写法:
<style scoped> scoped:当前 CSS只作用本 Vue 页面,不会污染其他页面的同名 class,Vue 单文件必备。
.user-page {
padding: 20px;
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
</style>
scoped 表示样式只作用于当前组件,避免污染其他页面。大型项目里也经常用 Less / Sass:
<style scoped lang="scss"> lang="scss":开启 SCSS 语法,支持嵌套写样式,原生 CSS 不能嵌套
.user-page {
padding: 20px;
.search-bar { 嵌套含义:.search-bar 等价于原生 .user-page .search-bar,省去重复写外层类名,写起来更省事
display: flex;
gap: 12px;
}
}
</style>
你现在 HTML/CSS 不用重新完整学一遍,能看懂结构、表单、Flex、盒模型、class 样式就够上手 Vue 项目。
二、JavaScript 核心:Vue3 必须补完整
JavaScript 是浏览器里的编程语言,负责页面逻辑。Vue3 的响应式、事件、接口请求、路由跳转、状态管理,全都是 JS 写出来的。你不需要先学到特别底层,但必须掌握变量、函数、对象、数组、模块、异步、DOM、this、解构、展开、Promise、async/await。
三、变量:let、const、var
现代 JS 基本不用 var,主要用 let 和 const。
let count = 1
count = 2
const name = 'Tom' //字符串属于基本数据类型,不存在内部属性,const 定义后整个值被锁死,完全不能修改:
// name = 'Jerry' // 报错,const 不能重新赋值
区别:
let:变量,可以重新赋值
const:常量,不能重新赋值
var:老语法,有作用域问题,项目里尽量别用
注意:const 不是对象完全不能改,而是变量不能重新指向新对象。
const user = {
name: 'Tom',
age: 18
}
user.name = 'Jerry' // 可以 只修改对象内部属性,变量 user 本身引用没变,允许
// user = {} // 不可以 让 user 指向全新空对象,改变变量的引用地址,const 禁止
Vue3 里经常这样写:
普通 const count = 0 改数字,页面不会变;
ref / reactive 是 Vue3 给数据套一层「监控壳子」,一改数据,页面立刻同步更新。
ref()专门放数字、文字、数组
reactive()专门放表单、多层对象,不用写 .value
- 数字、字符串、数组 → ref,改的时候加 `.value`
- 表单对象、复杂结构 → reactive,直接点属性修改
const count = ref(0)
const userList = ref([])
const form = reactive({})
这里用 const 是因为变量本身不重新指向别的东西,但对象里面的数据可以变。
四、数据类型
JS 常见类型:
const name = 'Tom' // string 字符串
const age = 18 // number 数字
const isLogin = true // boolean 布尔值
const user = null // null 空值
let data // undefined 未定义
const list = [1, 2, 3] // array 数组
const obj = { name: 'Tom' } // object 对象
判断类型:
typeof 'abc' // 'string'
typeof 123 // 'number'
typeof true // 'boolean'
Array.isArray([]) // true
要特别注意 null 和 undefined:
let a
console.log(a) // undefined,声明了但没赋值
const b = null // 主动表示这里是空
接口请求里很常见:
if (res.data === null) {
// 后端返回空
}
五、字符串
字符串可以用单引号、双引号、反引号。项目里最常见的是反引号模板字符串。
const name = 'Tom'
const age = 18
const text = `我叫${name},今年${age}岁`
常用方法:
const str = 'hello world'
str.length // 长度
str.includes('hello') // 是否包含
str.startsWith('he') // 是否以 he 开头
str.endsWith('ld') // 是否以 ld 结尾
str.replace('hello', 'hi')
str.split(' ') // ['hello', 'world']
str.trim() // 去掉首尾空格
表单校验常用:
if (!username.trim()) {
console.log('用户名不能为空')
}
接口拼 URL 常用:
const id = 10
const url = `/api/users/${id}`
六、数组:前端最常用数据结构
后端返回列表,前端表格、菜单、树、下拉框,本质都是数组。
const users = [
{ id: 1, name: 'Tom', age: 18 },
{ id: 2, name: 'Jerry', age: 20 }
]
1. 遍历 forEach
`user => {}`:箭头函数,每循环一次,当前这一项叫 `user`
`console.log(user.name)`:每次循环执行的代码,打印当前对象的 name
users.forEach(user => {
console.log(user.name)
})
forEach 只遍历,不产生新数组。
2. map:把数组转换成新数组
map 映射:数组批量提取 / 转换,返回新数组
遍历 users,只取出每个对象的 name,拼成纯名字数组。
const names = users.map(user => user.name)
// ['Tom', 'Jerry']
常用于把后端数据转换成组件需要的格式:
const options = users.map(user => ({
label: user.name,
value: user.id
}))
3. filter:过滤数组
filter 过滤:筛选符合条件的数据,返回新数组
const adults = users.filter(user => user.age >= 18)
Vue 页面搜索也常见:
const result = list.filter(item => item.name.includes(keyword))
4. find:找到第一个符合条件的元素
const user = users.find(user => user.id === 1)
找不到返回 undefined。
5. some / every
const hasAdmin = users.some(user => user.role === 'admin')
const allActive = users.every(user => user.status === 1)
some 是只要有一个满足就 true;every 是所有都满足才 true。
6. push / splice
const list = []
list.push({ id: 1, name: 'Tom' }) // 添加
list.splice(0, 1) // 从下标 0 开始删除 1 个
Vue 里数组修改会触发视图更新:
userList.value.push(newUser)
7. reduce:聚合计算
const nums = [1, 2, 3]
const sum = nums.reduce((total, item) => total + item, 0)
// 6
统计订单总价:
const totalPrice = orders.reduce((sum, order) => {
return sum + order.price * order.count
}, 0)
七、对象:接口数据、表单数据都靠它
对象是键值对结构。
const user = {
id: 1,
name: 'Tom',
age: 18
}
console.log(user.name)
console.log(user['name'])
修改属性:
user.name = 'Jerry'
user.email = '[已隐藏邮箱]'
delete user.age
对象嵌套:
const order = {
id: 1,
user: {
name: 'Tom',
phone: '123'
},
goods: [
{ id: 1, name: '电脑' }
]
}
访问时注意空值:
console.log(order.user.name)
如果 user 可能为空,直接 order.user.name 会报错。可以用可选链:
order.user ? order.user.name : undefined
console.log(order.user?.name)
Vue 项目里非常常见:
<template>
<div>{{ userInfo?.name }}</div>
</template>
八、函数:逻辑封装的核心
函数就是把一段逻辑封装起来。
function add(a, b) {
return a + b
}
const result = add(1, 2)
现代项目更常见箭头函数:
const add = (a, b) => {
return a + b
}
简写:
const add = (a, b) => a + b
无参数:
const sayHello = () => {
console.log('hello')
}
一个参数可以省略括号,但项目里建议保留括号:
const 定义常量变量 double,这个变量存的是一个函数,后面不能 double = 别的东西 重新赋值。
const double = (num) => num * 2
Vue3 里常见:
const getList = async () => {
const res = await getUserListApi()
list.value = res.data
}
const handleDelete = (id) => {
console.log('删除', id)
}
函数参数可以有默认值:
function getList(page = 1, size = 10) {
console.log(page, size)
}
九、条件判断
if (score >= 90) {
console.log('优秀')
} else if (score >= 60) {
console.log('及格')
} else {
console.log('不及格')
}
三元表达式:
const text = isLogin ? '已登录' : '未登录'
Vue 模板中常见:
<span>{{ status === 1 ? '启用' : '禁用' }}</span>
switch:
switch (status) {
case 0:
console.log('禁用')
break
case 1:
console.log('启用')
break
default:
console.log('未知')
}
常见真假判断:
if (token) {
console.log('有 token')
}
if (!list.length) {
console.log('列表为空')
}
JS 中 ''、0、null、undefined、false、NaN 都会被当成 false。
十、循环
普通 for:
for (let i = 0; i < list.length; i++) {
console.log(list[i])
}
for…of:
for (const item of list) {
console.log(item)
}
对象遍历:
const user = { name: 'Tom', age: 18 }
for (const key in user) {
console.log(key, user[key])
}
实际 Vue 项目里,数组处理更常用 map/filter/find/forEach,模板里用 v-for。
十一、解构赋值:项目里非常常见
数组解构:数组解构(按顺序取值)
const arr = [1, 2, 3]
const [a, b] = arr
console.log(a) // 1
console.log(b) // 2
// 基础
const [变量1, 变量2] = 数组
// 跳过元素
const [第一个, , 第三个] = 数组
// 剩余全部 ...rest
const [a, b, ...rest] = 数组
// 默认值
const [x = 0] = 数组
对象解构:(最常用,拿对象里的属性)
const user = {
name: 'Tom',
age: 18
}
const { name, age } = user
// 基础
const { 属性1, 属性2 } = 对象
// 重命名
const { 属性1: 新变量名, 属性2 } = 对象
// 默认值(属性不存在时生效)
const { 属性1 = 默认值 } = 对象
// 重命名+默认值
const { 属性1: 新名 = 默认值 } = 对象
函数参数解构:
//不解构: 调用时传给函数一个完整对象 `{name,age}`,函数内部得用变量 `.属性` 获取
function printUser(userObj) {
// 要手动点两次才能拿到数据
const name = userObj.name
const age = userObj.age
console.log(name, age)
}
printUser({ name: 'Tom', age: 18 })
//解构: const { name, age } = 传进来的那个对象
function printUser({ name, age }) {
console.log(name, age)
}
printUser({ name: 'Tom', age: 18 })
// 直接解构入参
function fn({ prop1, prop2 }) {}
// 带默认兜底,防止传null/undefined报错 如果没传参数,默认给一个空对象 `{}` 用来解构,不会报错。
function fn({ prop1 } = {}) {}
接口返回值常见:
const res = await getUserInfo()
const { id, username, roles } = res.data
Vue Router 常见:
const route = useRoute()
const { id } = route.params
注意:Vue 的 reactive 对象不要随便直接解构,否则可能丢响应式。需要解构响应式对象时用 toRefs。
十二、展开语法:复制、合并、传参
数组展开:
const a = [1, 2]
const b = [3, 4]
...a 把数组a全部元素摊开
const c = [...a, ...b]
// [1, 2, 3, 4]
对象展开:
const user = {
name: 'Tom',
age: 18
}
const newUser = {
...user,
age: 20
}
合并查询参数常见:
const query = {
page: 1,
size: 10,
keyword: 'test'
}
const params = {
...query,
status: 1
}
表单重置 / 编辑回填常见:
Object.assign(form, row) 直接修改原对象 form,把 row 里所有属性复制到 form 身上,
同名属性直接覆盖。原地修改原对象,旧数据直接消失,无法保留原始表单默认值
或者:
form.value = { ...row展开 row 所有属性,生成全新对象,再整体赋值给 form.value
...row
}
十三、模块化:import / export
大型项目一定是模块化的,不会所有代码写一个文件里。
命名导出 export function: export function 函数名(参数) { 逻辑 } 一个文件可以导出多个接口方法,供别处按需导入。
// api/user.js
export function getUserList() {
return request.get('/users')
}
export function deleteUser(id) {
return request.delete(`/users/${id}`)
}
命名导入(对应 export 函数 / 变量): import { 导出名1, 导出名2 } from ‘文件路径’ 文件导出多个方法,用花括号解构导入,名字必须和导出完全一致。
import { getUserList, deleteUser } from '@/api/user'
默认导出: export default 变量/对象/函数 一个文件只能写一次,导出该文件最核心的东西(这里是 axios 请求实例)。
// request.js
const request = axios.create()
export default request
默认导入:
文件里用 export default 导出一个核心值,导入时名字随便取。
import request from '@/utils/request'
区别:
export:可以导出多个,导入时要用 {}
export default:一个文件通常一个默认导出,导入时名字可以自定义
十四、异步:Promise、async、await
前端调接口是异步的。意思是:请求发出去后,不会卡死页面,等后端返回再继续处理。 前端页面运行在浏览器里,JS 负责处理点击、输入、渲染、接口请求等逻辑。问题是:接口请求、定时器、文件读取这些操作都不是马上完成的。
比如你点击“查询用户列表”:
点击按钮 → 发送请求给后端 → 后端查数据库 → 返回 JSON → 前端渲染表格
这个过程可能需要几十毫秒到几秒。如果 JS 一直傻等后端返回,页面就会卡死,按钮不能点,输入框不能输。所以浏览器采用异步机制:请求先发出去,JS 不阻塞页面;等结果回来,再执行回调逻辑。
所以异步的核心不是“代码乱跳”,而是:
现在不能立刻得到结果 → 先注册后续处理逻辑 → 结果回来后再执行
1. Promise
Promise 是 JS 用来表示“未来才会完成的任务”的对象。它可以理解成一个承诺:
现在我还没有结果,但未来会有两种结果:
1. 成功:resolve
2. 失败:reject
Promise 有三种状态:
pending 等待中
fulfilled 成功了
rejected 失败了
状态一旦从 pending 变成成功或失败,就不能再改。
创建一个 Promise 对象,表示一个未来完成的任务。
(resolve, reject) => { ... } 这是传给 Promise 的执行函数。`resolve` 表示任务成功时调用,`reject` 表示任务失败时调用。
1 秒后调用 `resolve('成功')`,表示这个异步任务成功完成,并且成功结果是字符串 `'成功'`。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
}, 1000)
})
.then() 是 Promise 用来接收异步成功结果的方法。
.then() 专门接收 resolve 传递的成功结果
resolve传入的'成功',会作为参数res传入回调函数
promise.then(res => {
console.log(res)
})
创建 Promise → 设置 1 秒定时器 → 继续执行后面的代码 → 1 秒后 resolve(‘成功’) → 执行 then → 打印 成功
接口请求本质上就是 Promise:
axios.get('/users') 不是马上得到用户列表,而是返回一个 Promise。
axios.get('/users').then(res => {
console.log(res.data)
})
它的意思是:
axios.get('/users'):发起请求,返回一个未来会完成的 Promise
.then(...):请求成功后执行
.catch(...):请求失败后执行
完整一点:
axios
.get('/users')
.then(res => {
console.log('后端返回:', res.data)
})
.catch(error => {
console.log('请求失败:', error)
})
这里的 res 通常是 Axios 封装后的响应对象,常见结构是:
{
data: 后端真正返回的数据,
status: 200,
headers: {},
config: {}
}
reject 和 catch
失败时用 reject:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('请求失败')
}, 1000)
})
// `.then`:只接收 `resolve` 传来的成功数据,**本次不会执行**
//`.catch`:专门捕获 `reject` 抛出的错误,`error` 就是 `'请求失败'`
promise
.then(res => {
console.log('成功:', res)
})
.catch(error => {
console.log('失败:', error)
})
执行逻辑:
resolve(...) → 进入 then
reject(...) → 进入 catch
接口请求失败、网络错误、后端报错,一般都会进入 catch。
2. async / await
现代项目更常用 async/await,写起来像同步代码。
async 是一个函数修饰关键字,专门用来标记「异步函数」。表示 getList 是一个异步函数。只要函数前面加了 async,这个函数一定返回 Promise
await 只能在 async 函数里用
await 不是让整个浏览器卡住,而是暂停当前 async 函数后面的代码,浏览器页面仍然可以响应点击、输入、渲染。
async function getList() {
const res = await axios.get('/users') //`await` 的意思是:等待右边这个 Promise 完成,然后把成功结果取出来。
console.log(res.data)
}
执行流程:
调用 getList()
→ 执行 axios.get('/users') 发请求
→ 当前 getList 暂停在 await
→ JS 可以继续处理其他事情
→ 请求成功返回
→ await 拿到结果赋值给 res
→ 继续执行 console.log(res.data)
箭头函数写法:
const getList = async () => {
const res = await axios.get('/users')
list.value = res.data
}
3. try / catch 处理错误
const getList = async () => {
//请求开始,页面显示加载中。
loading.value = true
try {
//发请求,等结果。如果成功,继续往下执行。
const res = await getUserListApi()
//把后端数据放到页面列表里。
list.value = res.data
} catch (error) {
console.error('请求失败', error)
} finally {
loading.value = false
}
}
大型项目里,接口错误一般会在 Axios 响应拦截器统一处理,但页面自己的 loading、局部错误提示仍然经常用 try/finally。
4. Vue 项目里的真实接口写法
页面里一般不会直接写 Axios,而是先封装 API。
// api/user.js
export function getUserListApi(params) {
return request.get('/users', { params })
}
这里 getUserListApi 返回的是 Promise,因为 request.get 本质就是 Axios 请求。
页面里调用:
const list = ref([])
const getList = async () => {
const res = await getUserListApi({
page: 1,
size: 10
})
list.value = res.data.records
}
这段的意思是:
定义响应式数组 list
调用 getUserListApi 发请求
await 等后端返回
把后端返回的 records 赋值给 list
list 变化后 Vue 自动更新页面
十五、JSON:前后端传输格式
前后端分离里,前端和后端通常通过 JSON 交换数据。 JSON 是一种数据格式,全称是 JavaScript Object Notation。它长得像 JS 对象,但它本质是字符串格式的数据,用于前后端传输。前端拿到后,把它变成 JS 对象,然后读取里面的数据。
JS 对象:可以用单引号,key 可以不加引号,可以有函数、undefined JSON:key 必须双引号,字符串必须双引号,不能写函数,不能写 undefined
JSON 长这样:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "Tom"
}
}
JS 对象转 JSON 字符串:
const user = { name: 'Tom', age: 18 }
const str = JSON.stringify(user)
结果是:
'{"name":"Tom","age":18}'
注意,str 是字符串,不再是对象。
JSON 字符串转对象:
const str = '{"name":"Tom","age":18}'
const user = JSON.parse(str)
console.log(user.name)
结果:
Tom
JSON.parse 的作用是把 JSON 字符串重新变成 JS 对象。
localStorage 只能存字符串。
localStorage 是浏览器自带的本地持久化存储工具,属于 Web Storage API,专门在用户电脑浏览器里存数据。
- 存储位置:当前浏览器、当前域名下
- 持久化:关闭页面、关闭浏览器、重启电脑,数据不会消失
- 容量:约 5MB,只能存字符串
普通变量:只存在当前页面内存,刷新 / 关页面直接消失 localStorage:存在浏览器本地磁盘,永久保留
和它配套的还有 sessionStorage(关闭页面就清空)
localStorage 存对象时要转字符串:
// 存数据 setItem :键名, 内容(只能字符串)
localStorage.setItem('user', JSON.stringify(user))
取出来:
const str = localStorage.getItem('user')
const userInfo = JSON.parse(str)
console.log(userInfo.name)
完整流程:
JS 对象 → JSON.stringify → 字符串 → 存 localStorage
localStorage 字符串 → JSON.parse → JS 对象 → 使用属性
十六、DOM 和事件:Vue 封装了,但要知道原理
浏览器拿到 HTML 后,不是直接把 HTML 字符串拿来操作,而是会把它解析成一棵对象树,这棵树叫 DOM。 比如 HTML:
<div id="app">
<button>点击</button>
</div>
浏览器会理解成类似:
document //代表整个网页文档
└── div#app
└── button
└── 文本:点击
JS 可以通过 DOM API 找到这些节点,然后读取或修改它们。
DOM 是浏览器把 HTML 解析后生成的对象树。原生 JS 可以直接操作 DOM:
document代表整个网页文档
querySelector('#btn') 按照 CSS 选择器找第一个匹配元素。`#btn` 表示找 `id="btn"` 的元素。
const btn = document.querySelector('#btn')
addEventListener 给 DOM 元素添加事件监听
() => { ... } 点击发生后执行的回调函数
btn.addEventListener('click', () => {
console.log('点击了按钮')
})
Vue 里一般不直接操作 DOM,而是写:
<button @click="handleClick">点击</button>
<button>普通按钮标签@click@是 Vue 简写,等价于v-on:v-on:click= 绑定点击事件
handleClick绑定的事件处理函数名,对应 js 里写的同名方法,不要加()(不加代表点击时执行)
function handleClick() {
console.log('点击了按钮')
}
- 这是事件触发后执行的回调函数
- 点击按钮时,Vue 内部自动调用这个函数
- 里面只写业务逻辑:打印日志、修改变量、请求接口等,完全不用碰 DOM
输入框原生写法:
const input = document.querySelector('#username')
console.log(input.value)
input.value 表示输入框当前的值
Vue 写法:
<input v-model="username" />
const username = ref('')
5. Vue 为什么不推荐频繁手动操作 DOM?
原生写法是:
找 DOM → 读 DOM → 改 DOM
Vue 的写法是:
定义数据 → 模板绑定数据 → 修改数据 → Vue 自动更新 DOM
比如原生写法:
<!-- 页面两个元素:显示数字的盒子、加1按钮 -->
<div id="count">0</div>
<button id="btn">+1</button>
<script>
// 1. 定义数据:数字计数器,存在JS变量里
let count = 0
// 2. 通过选择器手动获取DOM元素(拿到页面标签对象)
const countDom = document.querySelector('#count')
const btn = document.querySelector('#btn')
// 3. 给按钮绑定点击监听事件
btn.addEventListener('click', () => {
// 4. 先修改JS里的数据
count++
// 5. 手动把最新数据赋值给DOM,更新页面文字
countDom.innerText = count
})
</script>
Vue 写法:
<script setup>:JS 逻辑(数据、方法、接口)<template>:页面结构(HTML)<style>:页面样式(CSS,可选)
<script setup>
// 从vue中导入ref函数,用来创建响应式数据
import { ref } from 'vue'
// 创建响应式变量count,初始值0
//`ref(0)` 包装后,Vue 能监听它的变化
const count = ref(0)
// 点击触发的函数
function add() {
// 修改响应式数据,必须用 .value
count.value++
}
</script>
<template>
<!-- 模板中直接写count,不用 .value -->
插值语法,渲染 count 的值;一旦 count 变化,Vue 自动更新这个 div 的文字。
<div>{{ count }}</div>
<!-- @click绑定点击事件,触发add函数 -->
<button @click="add">+1</button>
</template>
Vue 帮你做了:
监听点击事件
修改响应式数据
找到需要更新的 DOM
更新页面
所以 Vue 的思想是:
不要手动操作 DOM,而是操作数据
6. Vue 里的 @click 是什么?
<button @click="handleClick">点击</button>
@click 是 v-on:click 的简写。
完整写法:
<button v-on:click="handleClick">点击</button>
意思是:
按钮点击时,执行 handleClick 函数
脚本:
function handleClick() {
console.log('点击了按钮')
}
如果要传参数:
<button @click="deleteUser(user.id)">删除</button>
对应:
function deleteUser(id) {
console.log('删除用户', id)
}
如果要拿事件对象:
<button @click="handleClick($event)">点击</button>
function handleClick(event) {
console.log(event.target)
}
event.target 表示触发事件的 DOM 元素。
7. Vue 里的 v-model 是什么?
你写:
<input v-model="username" />
脚本:
const username = ref('')
它的意思是:
输入框的值和 username 双向绑定
输入框变化 → username 变化
username 变化 → 输入框显示变化
大概等价于:
<input
<!-- :value 是 v-bind:value 的简写 -->
<!-- 单向绑定:JS 变量 username 的值渲染到输入框(JS → 视图) -->
:value="username"
<!-- @input 是 v-on:input 的简写 -->
<!-- 监听输入框输入事件,打字、粘贴、剪切都会触发该事件 -->
@input="username = $event.target.value"
<!-- $event:Vue 内置变量,代表原生事件对象 -->
<!-- $event.target:触发事件的 DOM 元素,也就是当前 input 输入框 -->
<!-- $event.target.value:输入框内最新输入的文本内容 -->
<!-- 赋值给 username,实现视图同步更新 JS 变量(视图 → JS) -->
/>
<!-- 上下两句组合,完整实现双向绑定,等价简写:<input v-model="username" /> -->
但在 Vue3 的 ref 里,真实脚本中是:
username.value = event.target.value
模板里自动解包,所以你不用写 .value。
完整例子:
<script setup>
// 从Vue中导入ref函数,用来创建响应式字符串变量
import { ref } from 'vue'
// 声明响应式变量username,初始为空字符串
// ref包裹后,变量变化页面会自动更新
const username = ref('')
</script>
<template>
<!-- v-model双向绑定输入框,等价下方完整底层写法 -->
<!-- :value="username" + @input="username = $event.target.value" -->
<input v-model="username" />
<!-- 插值表达式,实时展示username的值 -->
<p>你输入的是:{{ username }}</p>
</template>
当你在输入框里输入 admin:
输入框 value 变成 admin
→ v-model 把 admin 同步给 username
→ username 变化
→ 页面中的 {{ username }} 自动更新
8. Vue 里什么时候需要直接拿 DOM?
大部分时候不需要。但有些场景需要:
输入框自动聚焦
操作 canvas
操作第三方图表库 echarts
读取元素宽高
滚动到某个位置
这时可以用模板 ref:
<script setup>
// 导入ref(创建响应式变量)、onMounted(页面挂载完成钩子)
import { ref, onMounted } from 'vue'
// 创建一个ref变量,用来绑定DOM元素,初始值null
const inputRef = ref(null)
// onMounted:组件DOM渲染完成后自动执行
onMounted(() => {
// inputRef.value 就是真实的input DOM元素
// .focus() 原生DOM方法,让输入框自动获取光标、聚焦
inputRef.value.focus()
})
</script>
<template>
<!-- ref="inputRef":把当前input DOM关联到script里的inputRef变量 -->
<!-- `ref="xxx"`:标签上绑定 DOM 的标记(模板属性)-->
<input ref="inputRef" />
</template>
这里容易混淆:
const inputRef = ref(null)
这个 ref 是 Vue 的响应式 API。
<input ref="inputRef" />
这个 ref 是模板引用,用来把 DOM 元素绑定到 inputRef 上。
组件挂载后:
inputRef.value
就是那个真实 input DOM。
为什么要放在 onMounted 里?
因为组件没挂载前,DOM 还没创建出来,inputRef.value 还是 null。
十七、this:能少用就少用
this 表示“当前函数执行时的调用者”。它不是固定的,取决于函数怎么被调用。
this.xxx :通常表示访问当前组件的数据、方法、计算属性或插件实例。
传统 JS 里 this 指向容易变,Vue2 Options API 经常用 this.xxx:
// 导出当前组件配置对象(Vue2 老式写法,无<script setup>)
export default { //把这个配置对象导出,Vue 读取里面的 data、methods、生命周期等配置
// data:存放组件所有响应式数据
data() { //专门定义响应式数据;
// 必须 return 一个对象,里面写变量
return {
count: 0
}
},
// methods:存放所有事件/业务函数
methods: { //存放所有自定义方法(点击事件、工具函数等)
add() {
// 通过 this 访问 data 里的 count
this.count++
}
}
}
Vue3 <script setup> 基本不用 this:
<script setup>
// 从vue中引入ref函数,用来创建基础类型响应式数据(数字、字符串、布尔)
import { ref } from 'vue'
// 创建响应式变量count,初始值为0
// ref会把0包装成一个Ref对象,真实数值存在 .value 属性中
const count = ref(0)
// 自定义点击函数,模板@click直接调用
function add() {
// 修改ref响应式数据,必须加 .value
count.value++
}
</script>
<template>
<!-- 模板中使用ref变量,不需要写 .value,Vue自动解析 -->
<div>{{ count }}</div>
<button @click="add">+1</button>
</template>
你找工作如果是 Vue3 项目,先掌握 <script setup>,this 不用深入纠结;但看到老项目的 this.xxx 要知道它通常指当前组件实例。
十八、作用域和闭包:看懂即可
作用域就是变量能在哪里访问。
代码中的 {} 会形成块级作用域;使用 let / const 定义的变量仅在当前 {} 内部生效,作用域外部无法访问。
var 不存在块级作用域,在 {} 中用 var 声明的变量,外部依然可以读取,容易造成全局变量污染。
function test() {
const a = 1
console.log(a)
}
console.log(a) // 报错,外面访问不到函数内部变量
闭包 = 内层函数 + 它所在外层函数的变量环境
满足两个条件才算闭包:
- 内部函数引用了外层函数的局部变量;
- 内部函数脱离外层函数,在外部持续执行。
特点:内层函数会永久保留外层函数的变量,外层函数执行完毕,变量不会被销毁。
// 外层函数
function createCounter() {
// 外层局部变量 count
let count = 0
// 内层函数,引用外层count
return function () {
count++
return count
}
}
// 调用外层函数,拿到内层函数,存到全局变量counter
const counter = createCounter()
counter() // 1
counter() // 2
counter() // 3
createCounter()执行,创建局部变量count=0,返回内部函数;- 函数执行完毕,正常局部变量会销毁,但内部函数引用了
count,形成闭包; counter持有内层函数,每次调用都能访问、修改原本该销毁的count;- 多个独立变量互不干扰: 每个闭包拥有独立私有数据,不会互相污染
闭包作用
- 实现私有变量,外部无法直接修改 count,只能通过内部函数操作;
- 数据持久化,函数多次调用保留上次状态;
- 封装逻辑,隔离数据。
Vue 里的组合式函数其实经常用到类似思想:
import { ref } from 'vue'
// 自定义组合函数(hooks)
export function useCounter() {
// 私有响应式状态,只在当前函数作用域内
const count = ref(0)
// 操作状态的内部方法
function add() {
count.value++
}
// 将状态、方法暴露出去,供外部组件使用
return {
count,
add
}
}
useCounter 把状态和方法封装起来,其他组件可以复用。
闭包对应关系
useCounter= 外层函数count= 外层私有变量add= 内部函数,引用count- 组件接收返回值,在组件中持续使用,形成闭包 组件使用示例
<script setup>
import { useCounter } from './useCounter'
// 拿到闭包暴露的变量和方法
const { count, add } = useCounter()
</script>
<template>
<p>{{ count }}</p>
<button @click="add">+1</button>
</template>
十九、常用浏览器存储
前端刷新页面后,普通 JS 变量会消失。
let token = 'abc'
页面一刷新,这个变量没了。
但是登录 token、用户信息、菜单状态有时候需要刷新后还在,所以要存到浏览器存储里。
1. localStorage
持久存储,关闭浏览器也还在。 关闭浏览器后还在,刷新页面后还在,只能存字符串,同一个域名下共享。
localStorage.setItem('token', 'abc') //意思是保存一个键值对:key: token value: abc
const token = localStorage.getItem('token')
localStorage.removeItem('token')
登录 token 常放这里,但真实企业项目也可能用 cookie、sessionStorage 或更安全的方案。
2. sessionStorage
sessionStorage 和 localStorage 类似,但生命周期更短:
刷新页面还在 关闭当前标签页后消失 不同标签页之间通常不共享
sessionStorage.setItem('activeMenu', '/user')
# 二十、JS 和 Vue3 的对应关系
这部分不能只列对应关系,要知道 Vue3 其实就是把 JS 的常见操作封装得更适合写页面。
1. 变量 → ref / reactive
普通 JS:
let count = 0
function add() {
count++
}
问题是:count 变了,页面不会自动更新。
Vue:
const count = ref(0)
function add() {
count.value++
}
ref 把普通数据变成响应式数据。count.value 变化后,Vue 知道依赖它的页面需要更新。
模板:
<div>{{ count }}</div>
<button @click="add">+1</button>
模板里不用写 .value,Vue 会自动解包。
2. 对象 → 表单数据
普通 JS 对象:
const form = {
username: '',
password: ''
}
Vue 表单:
const form = reactive({
username: '',
password: ''
})
模板:
<input v-model="form.username" />
<input v-model="form.password" />
<button @click="submit">登录</button>
提交:
async function submit() {
await loginApi(form)
}
这里流程是:
用户输入 → v-model 修改 form
点击登录 → submit 调接口 → 把 form 传给后端
3. 数组 → 表格 / 列表 / 菜单
JS 数组:
const users = [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jerry' }
]
Vue:
const userList = ref([])
请求接口后赋值:
const getList = async () => {
const res = await getUserListApi()
userList.value = res.data.records
}
模板渲染:
<ul>
<li v-for="user in userList" :key="user.id">
{{ user.name }}
</li>
</ul>
v-for 的意思是:
遍历 userList,每个元素临时叫 user,为每个 user 渲染一个 li
:key="user.id" 是给每一项一个稳定身份,方便 Vue 高效更新列表。
4. 函数 → 事件处理
JS 函数:
function search() {
console.log('搜索')
}
Vue 事件:
<button @click="search">搜索</button>
点击按钮时执行 search。
带参数:
<button @click="deleteUser(user.id)">删除</button>
对应:
function deleteUser(id) {
console.log('删除', id)
}
5. async / await → 接口请求
JS 异步请求:
const res = await axios.get('/users')
Vue 页面常见:
const loading = ref(false)
const list = ref([])
const getList = async () => {
loading.value = true
try {
const res = await getUserListApi()
list.value = res.data.records
} finally {
loading.value = false
}
}
模板:
<div v-if="loading">加载中...</div>
<ul v-else>
<li v-for="user in list" :key="user.id">
{{ user.name }}
</li>
</ul>
流程:
getList 执行
→ loading = true,显示加载中
→ 请求后端
→ 后端返回用户列表
→ list 更新
→ Vue 重新渲染列表
→ loading = false
6. import / export → 模块拆分
大型项目不可能一个文件写完,所以要拆模块。
接口文件:
// api/user.js
export function getUserListApi(params) {
return request.get('/users', { params })
}
export function deleteUserApi(id) {
return request.delete(`/users/${id}`)
}
页面导入:
import { getUserListApi, deleteUserApi } from '@/api/user'
意思是:
从 api/user.js 这个模块里,引入 getUserListApi 和 deleteUserApi 两个函数
这样页面不用关心 Axios 细节,只关心业务函数。
7. localStorage → token 持久化
登录页:
const submit = async () => {
const res = await loginApi(form)
localStorage.setItem('token', res.data.token)
router.push('/')
}
请求拦截器:
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
路由守卫:
router.beforeEach((to) => {
const token = localStorage.getItem('token')
if (!token && to.path !== '/login') {
return '/login'
}
})
这三段连起来就是大型项目的登录核心:
登录保存 token
→ 请求自动携带 token
→ 路由根据 token 判断能不能进入页面
二十一、你现在应该怎么学 JS
不要先刷特别厚的 JS 教程。你是为了看 Vue3 项目和找工作,建议按这个顺序:
第一步,掌握基础语法:let/const、数据类型、条件、循环、函数、对象、数组。目标是能看懂页面里的变量和方法。
第二步,重点练数组和对象:map/filter/find/some/reduce、对象解构、展开语法。因为前端业务大部分就是处理后端返回的数组和对象。
第三步,学模块化和异步:import/export、Promise、async/await、try/catch/finally。这是看懂接口请求的关键。
第四步,结合 Vue 页面练:写一个列表页,包含查询、新增、编辑、删除、分页、loading、接口请求。不要只学语法,要马上放到 Vue 里用。
第五步,补一点浏览器知识:DOM、事件、localStorage、JSON、跨域、请求头、token。够理解前后端分离链路就行。
二十二、最小必背 JS 代码模板
1. 请求列表
const list = ref([])
const loading = ref(false)
const getList = async () => {
loading.value = true
try {
const res = await getListApi()
list.value = res.data
} finally {
loading.value = false
}
}
2. 搜索
const query = reactive({
keyword: '',
page: 1,
size: 10
})
const search = () => {
query.page = 1
getList()
}
3. 删除
const handleDelete = async (id) => {
await deleteApi(id)
getList()
}
4. 表单提交
const form = reactive({
username: '',
password: ''
})
const submit = async () => {
await loginApi(form)
}
5. 登录保存 token
const login = async () => {
const res = await loginApi(form)
localStorage.setItem('token', res.data.token)
}
6. 请求头带 token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
二十三、面试能说的话
HTML 负责结构,CSS 负责样式,JavaScript 负责页面逻辑;Vue3 在此基础上提供组件化、响应式和声明式渲染,让我们不用频繁手动操作 DOM,而是通过修改响应式数据驱动页面更新。
Vue3 项目里,JavaScript 的核心使用场景包括:用 ref/reactive 保存页面状态,用函数处理按钮事件,用 async/await 调接口,用数组方法处理后端返回的列表,用 import/export 拆分模块,用 localStorage 保存 token,用 Axios 拦截器统一加请求头和处理错误。
如果看一个前后端分离页面,我会按“页面事件 → 调 API 函数 → Axios 请求拦截器加 token → 后端返回 JSON → 响应拦截器处理结果 → 修改响应式状态 → 模板自动更新”的链路去理解。
这就是你现在最该补的前端基础。HTML/CSS 你只要恢复常见标签、盒模型、Flex、样式作用域;JS 要重点补,因为 Vue3 的项目代码本质上就是 JS + Vue API。