0
0

Linux 三剑客

2026-03-25
2026-07-23

可以,你可以把前端基础分成三层: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,主要用 letconst

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

要特别注意 nullundefined

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>

@clickv-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) // 报错,外面访问不到函数内部变量

闭包 = 内层函数 + 它所在外层函数的变量环境

满足两个条件才算闭包:

  1. 内部函数引用了外层函数的局部变量;
  2. 内部函数脱离外层函数,在外部持续执行。

特点:内层函数会永久保留外层函数的变量,外层函数执行完毕,变量不会被销毁。

// 外层函数
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
  • 多个独立变量互不干扰: 每个闭包拥有独立私有数据,不会互相污染

闭包作用

  1. 实现私有变量,外部无法直接修改 count,只能通过内部函数操作;
  2. 数据持久化,函数多次调用保留上次状态;
  3. 封装逻辑,隔离数据。

Vue 里的组合式函数其实经常用到类似思想:

import { ref } from 'vue'

// 自定义组合函数(hooks)
export function useCounter() {
  // 私有响应式状态,只在当前函数作用域内
  const count = ref(0)

  // 操作状态的内部方法
  function add() {
    count.value++
  }

  // 将状态、方法暴露出去,供外部组件使用
  return {
    count,
    add
  }
}

useCounter 把状态和方法封装起来,其他组件可以复用。

闭包对应关系

  1. useCounter = 外层函数
  2. count = 外层私有变量
  3. add = 内部函数,引用 count
  4. 组件接收返回值,在组件中持续使用,形成闭包 组件使用示例
<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/awaittry/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。