ARTICLE · 1112932
AI编程必知必会(第二期)|菜鸟错题本
LAW × AI
法律、AI技术·研究、应用
RESEARCH · PRACTICE · TECH NOTES
Vibe Coding
底层知识补全 · 第二期
从「能跑就行」到「能读懂、能调试、能改」
杨显光 · 2026-10-2

第一期我们把操作系统层打通了:命令行、PATH、ENV、部署。这一期往代码层走——让你看到一段代码时不再发懵,遇到 bug 时不再瞎试,改别人的代码时不再手抖。
| 01 | PART ONE 一、从环境到代码,你还缺什么 |
第一期解决了「为什么跑不起来」,第二期解决「为什么它是这么写的」。

代码阅读能力差,通常卡在这四点
| 不知道怎么「看」代码 | ||
| 看不懂框架的「魔法」 | ||
| 不会调试,只能瞎试 | ||
| 协议和工具不熟 |
🔑 本期核心心法
「读代码」不是逐行看,而是「追踪数据流」。一个变量从哪来,经过哪些函数,变成什么样,最后去了哪——把这条线抓住,代码就不再是天书,而是一条可以跟着走的路径。本期所有章节,都是在帮你练这条线。
| 02 | PART TWO 二、调试:不会调试 = 不会编程 |
这是你最该补上的一课,没有之一。
vibe coding 的人有个通病:遇到 bug 就改一下、刷新、再改一下、再刷新。这种「盲改」方式,运气好能撞对,运气差就陷入死循环。真正的高手用的是调试——让程序自己告诉你哪里错了。
console.log 的正确姿势
① 给日志加标签,否则你分不清谁是谁
JAVASCRIPT
// ❌ 差
console.log(user)
console.log(data)
console.log(res)
// ✅ 好
console.log('用户信息:', user)
console.log('请求数据:', data)
console.log('接口返回:', res)
② 用 console.table 看数组
JAVASCRIPT
const users = [
{ name: '张三', age: 20 },
{ name: '李四', age: 25 },
]
console.log(users) // 展开一个个看,很累
console.table(users) // ✅ 直接变成表格
③ 用 console.time 测性能
JAVASCRIPT
console.time('请求耗时')
await fetchData()
console.timeEnd('请求耗时')
// 输出:请求耗时: 234.56ms
④ 用 debugger 打断点(比 log 更强)
JAVASCRIPT
function handleLogin(form) {
debugger // 浏览器执行到这里会自动停下
const res = validate(form)
// 此时你能看到所有变量的值,还能一步步往下走
}
✅ debugger 相比 console.log 的优势
• 能看所有变量的当前值,不用一个个打印
• 能一步步执行,看代码怎么走的
• 能修改变量的值再继续跑
• 不污染代码(用完记得删)
浏览器 DevTools 五大面板
F12 打开它,看懂这五个面板,你的调试能力立刻翻倍。
| Elements | ||
| Console | ||
| Network | 看所有网络请求 | |
| Application | ||
| Sources |
Network 面板必会技巧
前后端联调,90% 的问题都靠它定位。
• 打开 Network,勾选 Preserve log页面跳转后请求记录不会消失,能看到完整链路。
• 按 Fetch/XHR 筛选,只看接口请求不然图片、CSS、JS 一大堆,看不清。
• 点开一个请求,看四个地方- Headers — 请求地址对不对?带了 token 吗?- Payload — 传的参数是什么?- Response — 后端返回了什么?- Status — 状态码是多少?
💡 一个真实场景
前端报「登录失败」,你不知道哪错了。打开 Network → 点开 /api/login:
• Status 是 404 → 后端没这个路由(路径写错)
• 401 → 密码错或 token 无效
• 500 → 后端崩了,去看后端日志
• Status 200 但 Response 里 code: -1 → 业务逻辑错误,看返回的 message
Python 断点调试
Python 也有类似 debugger 的利器:breakpoint()(Python 3.7+ 内置)。
PYTHON
def handle_order(order_id):
order = db.get(order_id)
breakpoint() # 执行到这里会进入交互式调试器
total = calc_total(order)
return total
运行后程序会停在 (Pdb) 提示符,常用命令:
| n | |
| s | |
| c | |
| p 变量名 | |
| pp 变量名 | |
| l | |
| q |
✅ VS Code 里更爽
在 VS Code 里直接点行号左边打个红点(断点),按 F5 启动调试。鼠标悬停就能看变量值,不用敲命令。
🔑 调试的核心思维
不要猜,要让程序告诉你。每当你想「改一下试试」的时候,停下来问自己:「我能不能先打印/断点,确认问题到底出在哪?」这个问题问多了,你就从「盲改」升级成「精准定位」了。
| 03 | PART THREE 三、读懂异步 JavaScript |
这是 JS 代码阅读的第一大障碍。

你读 JS 时最常卡住的地方是不是这种:
JAVASCRIPT
fetchUser().then(user => {
fetchOrders(user.id).then(orders => {
fetchDetail(orders[0].id).then(detail => {
console.log(detail)
})
})
})
一层套一层,读着读着就不知道自己在哪了。
为什么需要异步
JS 是单线程的——一次只能做一件事。但网络请求可能要等几百毫秒,如果傻等,页面就卡死了。
类比
你在餐厅点了一份牛排。同步:厨师做完牛排才去接下一单,你等 10 分钟,别人也跟着等。异步:厨师把牛排下锅后,先去做别的单子,牛排好了再端给你。异步的核心就是:不要堵住主线程。等结果的时候,去做别的事。
回调 → Promise → async/await 的进化
同样是「先拿用户,再拿订单」,三种写法对比:
阶段一:回调函数(callback)
JAVASCRIPT
fetchUser(function(err, user) {
if (err) { return console.error(err) }
fetchOrders(user.id, function(err, orders) {
if (err) { return console.error(err) }
console.log(orders)
})
})
问题:嵌套深、错误处理重复、难维护。俗称「回调地狱」。
阶段二:Promise
JAVASCRIPT
fetchUser()
.then(user => fetchOrders(user.id))
.then(orders => console.log(orders))
.catch(err => console.error(err)) // 统一错误处理
好处:链式调用、扁平化、一个 catch 捕获所有错误。
阶段三:async/await(现代写法,最推荐)
JAVASCRIPT
async function loadData() {
try {
const user = await fetchUser()
const orders = await fetchOrders(user.id)
console.log(orders)
} catch (err) {
console.error(err)
}
}
看起来像同步代码一样从上到下,但实际上是异步的。这就是为什么现代项目里你看到的多半是 async/await。
✅ 阅读 async/await 的秘诀
你就把 await 理解成「等这个操作完成,把结果赋给左边」。遇到 await 就停下来想:这个函数返回什么?赋值给了谁?其他的阅读方式和普通同步代码一模一样。
事件循环:理解异步的关键
为什么下面这段代码输出顺序不是 1、2、3?
JAVASCRIPT
console.log('1')
setTimeout(() => console.log('2'), 0)
console.log('3')
// 实际输出:1 → 3 → 2
哪怕 setTimeout 的延迟是 0,它也不会立刻执行。原因就是事件循环。
类比
想象一个服务员(JS 主线程)和一块后厨订单板(任务队列)。所以 setTimeout 里的回调一定会等到当前所有同步代码跑完才执行。
• 服务员先按顺序做前台的活(同步代码)
• 遇到「需要等」的事(setTimeout、网络请求),就写个订单贴到后厨板上,然后继续做前台
• 前台活全部做完后,服务员才回头看后厨板上有没有完成的订单
简化版的执行顺序:
执行同步代码 → 清空微任务队列(Promise.then) → 执行宏任务(setTimeout) → 循环
看一个更完整的例子:
JAVASCRIPT
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')
// 输出:A → D → C → B
// 因为 Promise.then 是微任务,优先级高于 setTimeout 宏任务
⚠️ 一个超容易踩的坑
在循环里用 var + setTimeout,会输出一堆相同的值:原因:var 没有块级作用域,三个回调共享同一个 i,等它们执行时循环已结束,i 已经是 3。解决:把 var 换成 let,或者用闭包包起来。
JAVASCRIPT
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出:3 3 3(不是 0 1 2!)
🔑 读异步代码的三个问题
每遇到一段异步代码,问自己:① 这段代码是同步还是异步?(看有没有 await / .then / 回调)② 异步的部分什么时候执行?(微任务还是宏任务?)③ 它执行时,依赖的变量还是当时的值吗?回答完这三个问题,90% 的异步代码你都能读懂。
| 04 | PART FOUR 四、读懂框架的「魔法」 |
一个装饰器凭什么就绑定了路由?
你写 FastAPI 时,是不是觉得这行代码像魔法:
PYTHON
@app.get("/users")
def get_users():
return [{"name": "张三"}]
一个 @app.get 加在函数上,它就变成了一个 HTTP 接口。
Python 装饰器:@ 的真面目
🔑 装饰器的本质
@decorator 加在函数上,等价于 func = decorator(func)。就是这么简单——它把函数当成参数传进去,返回一个新的函数替换原来的。
看一个最小的例子:
PYTHON
def log_time(func):
def wrapper(*args, **kwargs):
print(f"开始执行 {func.__name__}")
result = func(*args, **kwargs)
print(f"结束执行")
return result
return wrapper
# 下面两种写法完全等价
# 写法一:手动包装
def say_hi():
print("你好")
say_hi = log_time(say_hi)
say_hi()
# 写法二:用 @ 语法糖
@log_time
def say_hi2():
print("你好")
say_hi2()
现在回头看 @app.get
FastAPI 的 @app.get("/users") 做了三件事:
所以真正的逻辑是:框架维护了一个路由表,装饰器帮你往表里加了一条记录。当请求进来时,框架查表找到对应的函数去执行。
✅ 以后看到装饰器,这样读
不要被 @ 迷惑。就想:「这个函数被包装了,可能加了额外行为,也可能被注册到某个地方了」。
Vue 组合式 API:为什么要有 ref 和 .value
你写 Vue 3 时,是不是经常忘了 .value 导致数据不更新?
JAVASCRIPT
import { ref } from 'vue'
const count = ref(0)
count.value++ // ✅ 正确:必须 .value
console.log(count.value)
// 在 template 里却不用写 .value
// <template>{{ count }}</template>
原因:JS 无法监听一个普通变量的变化
JAVASCRIPT
let count = 0
count = 1 // Vue 完全不知道这个变量变了
JS 里对一个普通变量赋值,没有任何机制能通知 Vue「该更新视图了」。所以 Vue 换了个思路:不让你直接改变量,而是让你改一个对象。
JAVASCRIPT
// ref(0) 实际上返回的是类似这样:
{
get value() {
追踪() // 记录:有人用了这个值
return 内部的值
},
set value(新值) {
内部的值 = 新值
触发更新() // 通知:值变了,重新渲染
}
}
当你写 count.value = 1 时,实际调用的是 set value,Vue 就能拦截到这次赋值。
类比
普通变量就像你家的电灯开关,拉一下灯亮,但没人知道。ref 就像装了个智能开关,你拉一下,手机 App 会收到通知。
为什么 template 里不用 .value?因为 Vue 的模板编译器会自动帮你加。
| <script setup> | count.value | |
| <template> | count | |
| reactive() | obj.count |
💡 reactive vs ref
• 基本类型(数字、字符串、布尔)→ 用 ref
• 对象/数组,希望直接改属性 → 用 reactive
• 团队约定:都用 ref 更统一
🔑 读框架代码的通用心法
所有「魔法」背后都是普通代码:遇到看不懂的语法,就去查它的等价普通写法。
• 装饰器 = 函数包装
• 响应式 = getter/setter 拦截
• 路由 = 一张映射表
• 依赖注入 = 一个全局容器
| 05 | PART FIVE 五、HTTP 协议实战 |
前后端之间的「语言」,不学真的没法调试。
状态码:一眼看出问题出在哪边
| 2xx | 200 | |
| 3xx | 301 | |
| 4xx | 客户端错 | 400 |
| 5xx | 服务端错 | 500 |
✅ 快速定位法
看到 4xx → 先查前端:参数对不对、路径对不对、有没有带 token看到 5xx → 立刻看后端日志:一般是有异常没被捕获看到 200 但业务失败 → 是业务层错误,看返回的 message
Header 与 Cookie
| Content-Type | ||
| Authorization | ||
| Cookie | ||
| Set-Cookie | ||
| User-Agent | ||
| Origin | ||
| Access-Control-Allow-Origin |
Cookie / Session / Token 的区别
| Cookie | |||
| Session | |||
| Token / JWT |
类比
• Cookie = 你在咖啡店办的纸质卡,每次去都带着
• Session = 店里有个本子记录你,你只带一个会员号
• JWT = 一张加密的电子凭证,谁都能验证真伪
JWT:现代前后端最常用的方案
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNzE5fQ.abc123
├── Header ──┤ ├─────── Payload ──────┤ ├─ Signature ─┤
完整流程
前端登录 → 后端验证密码 → 生成 JWT 返回 → 前端存 localStorage
前端请求 → Header 带 Token → 后端验证签名 → 返回数据
前端统一加 token:
JAVASCRIPT
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
⚠️ Token 存哪里?
localStorage:简单,但 XSS 攻击能偷走。Cookie + HttpOnly:JS 读不到,更安全,但有 CSRF 风险。简单项目:localStorage 够用。涉及支付的:必须用 HttpOnly Cookie + CSRF Token。
CORS 跨域:为什么本地一直报错
Access to XMLHttpRequest at 'http://localhost:8000/api/users'
from origin 'http://localhost:5173' has been blocked by CORS policy
浏览器为什么要拦截
想象你在 bank.com 登录了,然后打开了一个恶意网站 evil.com。如果没有同源策略,evil.com 就能用你的登录态偷偷请求 bank.com/api/transfer。
所以浏览器规定:一个源的页面,不能随便请求另一个源的接口。这是浏览器的行为,不是服务器拒绝你。
同源的定义
协议 + 域名 + 端口,三者完全一样才算同源。
| http://a.com | http://a.com/api | ||
| http://a.com | https://a.com/api | ||
| http://a.com | http://b.com/api | ||
| http://a.com | http://a.com:8000/api |
解决方案:三种,按场景选
开发环境:用 Vite proxy
JAVASCRIPT
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
}
生产环境:用 Nginx 反向代理
NGINX
location /api/ {
proxy_pass http://127.0.0.1:8000;
}
真正需要跨域时:后端设置 CORS 头
PYTHON
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
❗ 别用 allow_origins=["*"] + allow_credentials=True
浏览器不允许这种组合。生产环境必须明确列出允许的域名。
🔑 记住这个本质
CORS 是浏览器的安全机制,不是服务器的问题。报跨域错时,不是「后端拒绝了你」,而是「浏览器不让浏览器发这个请求」。解决思路就是:让请求看起来同源——用 proxy 或 Nginx 反代。
| 06 | PART SIX 六、Git:代码的时光机 |
不会 Git,等于没有版本控制的编程。

四个核心概念
工作区 → git add → 暂存区 → git commit → 本地仓库 → git push → 远程仓库
类比
Git 像一个「拍照存档系统」:
• 工作区 = 你正在改的稿子
• git add = 把想存档的页面挑出来
• git commit = 按下快门,拍一张照片存档
• git push = 把照片上传到云端
日常工作流(90% 时间只用这几条)
BASH
git status # 查看状态(最常用!)
git diff # 看改了什么
git add . # 添加全部
git add src/main.js # 添加单个
git commit -m "feat: 添加登录功能"
git push
git pull
读懂 git diff
diff --git a/src/main.js b/src/main.js
index 1234567..89abcde 100644
--- a/src/main.js
+++ b/src/main.js
@@ -10,7 +10,9 @@ function login() {
const user = getCurrentUser()
- if (user) {
- return user.name
+ if (user && user.isActive) {
+ console.log('登录成功')
+ return user.name
}
return null
}
| --- | |
| @@ ... @@ | |
| - | |
| + | |
分支:并行开发的利器
BASH
git branch # 看所有分支
git switch -c feature-login # 创建并切换
git switch main # 切回主分支
git merge feature-login # 合并分支到当前
git branch -d feature-login # 删除已合并分支
出错了怎么回滚(救命操作)
| git checkout -- 文件名 | |
| git reset HEAD 文件名 | |
| git commit --amend -m "新消息" | |
| git reset HEAD~1 | |
| git reset --hard HEAD~1 | |
| git revert <commit-hash> |
❗ --hard 很危险
git reset --hard 会彻底丢掉未提交的修改,找都找不回来。敲之前一定确认:git status 看看有没有未提交的重要内容。
冲突怎么办
<<<<<<< HEAD
// 你的版本
const API_URL = 'http://localhost:8000'
=======
// 别人的版本
const API_URL = 'https://api.example.com'
>>>>>>> feature-branch
处理方法:
✅ 提交信息规范
feat: 新功能 · fix: 修复 bug · docs: 文档style: 格式 · refactor: 重构 · chore: 杂项例子:fix: 修复登录页手机号校验错误
| 07 | PART SEVEN 七、数据库与 SQL 基础 |
做网站一定会碰,绕不过去。
为什么需要数据库
你写的 Python 后端,重启一次数据就没了——因为内存不持久。数据库就是「把数据存到磁盘上」的工具。
| 关系型(SQL) | ||
| 文档型(NoSQL) | ||
| 键值型 |
Web 项目里,90% 的场景用关系型数据库。
SQL 核心语法(记住这 5 条就够用)
1. 查询:SELECT
SQL
SELECT * FROM users
SELECT id, name, email FROM users WHERE age > 18
SELECT * FROM users WHERE name LIKE '张%' ORDER BY created_at DESC LIMIT 10
2. 插入:INSERT
SQL
INSERT INTO users (name, email, age)
VALUES ('张三', 'zhang@example.com', 25)
3. 更新:UPDATE
SQL
UPDATE users SET age = 26 WHERE id = 1
❗ UPDATE / DELETE 一定要带 WHERE
UPDATE users SET age = 26(不带 WHERE)会把所有用户的年龄都改成 26。养成习惯:先写 WHERE,再补前面的部分。
4. 删除:DELETE
SQL
DELETE FROM users WHERE id = 1
5. 关联查询:JOIN
SQL
SELECT u.name, o.order_no, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.id = 1
ORM:为什么后端代码里看不到 SQL
PYTHON
# SQLAlchemy(Python 最常见的 ORM)
users = db.query(User).filter(User.age > 18).all()
# 或新版本
from sqlalchemy import select
result = await db.execute(select(User).where(User.age > 18))
users = result.scalars().all()
类比
直接写 SQL 像用键盘敲中文拼音;用 ORM 像用输入法——你敲拼音它也翻译成中文,方便但不改变本质。
ORM 基本操作对照
| SELECT * FROM users | db.query(User).all() | |
| SELECT * FROM users WHERE age > 18 | db.query(User).filter(User.age > 18).all() | |
| SELECT * FROM users WHERE id = 1 | db.query(User).get(1) | |
| INSERT INTO users ... | db.add(User(name="张三")); db.commit() | |
| DELETE FROM users WHERE id = 1 | db.delete(user); db.commit() |
✅ 读懂 ORM 代码的关键
看到 db.query(User) 就翻译成 SELECT * FROM users,看到 .filter(...) 就翻译成 WHERE ...,看到 .all() 就是执行查询。ORM 只是语法糖,背后还是 SQL。
连接池
建立数据库连接很慢(几十毫秒)。所以后端启动时会预先创建一批连接放在池子里,用完不关,还回去。
这也解释了为什么 .env 里的 DATABASE_URL 那么重要——它决定后端连的是哪个数据库。
| 08 | PART EIGHT 八、读懂报错信息 |
90% 的答案就在报错里,只是你不会读。

Python 报错(traceback):从下往上读
Traceback (most recent call last):
File "app.py", line 25, in <module>
main()
File "app.py", line 18, in main
result = calculate(data)
File "app.py", line 8, in calculate
return a / b
ZeroDivisionError: division by zero
| 最后一行 | 错误类型 + 描述 |
读 traceback 的正确姿势是从下往上:
JS 报错:从第一行读
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
at login (main.js:25)
at handleClick (main.js:42)
at HTMLButtonElement.onclick (index.html:10)
| at ... |
常见错误类型速查
Python
| SyntaxError | ||
| NameError | ||
| TypeError | ||
| KeyError | ||
| IndexError | ||
| AttributeError | ||
| ImportError | ||
| ValueError | int("abc") |
JavaScript
| TypeError | undefined.name | |
| ReferenceError | ||
| SyntaxError | ||
| RangeError | ||
| Uncaught (in promise) |
排查报错的通用四步
🔑 一个思维转变
把报错信息当成「程序在向你求救」,而不是「失败的证据」。它告诉你:「我在这里,遇到了这个问题」。看懂它,你就多了一个永远在线的老师。
| 09 | PART NINE 九、项目分层与数据流 |
为什么代码要分目录,怎么快速找到某段逻辑。
典型后端分层(以 FastAPI 为例)
backend/
├── main.py ← 入口:创建 app,注册路由中间件
├── routers/ ← 路由层:定义 URL 和请求参数
│ └── user.py @router.get("/users")
├── schemas/ ← 数据校验层:定义请求/响应的数据结构
│ └── user.py class UserCreate(BaseModel)
├── services/ ← 业务逻辑层:真正干活的地方
│ └── user_service.py def create_user(data): ...
├── models/ ← 数据模型层:对应数据库表
│ └── user.py class User(Base)
├── db/ ← 数据库连接
└── utils/ ← 工具函数
一次「创建用户」的请求怎么走
HTTP 请求 → routers/user.py → services/user_service.py → models/user.py → 数据库
每层的职责
| routers | ||
| services | ||
| models | ||
| schemas |
✅ 找代码的快速方法
• 想找「这个接口怎么处理请求」→ 看 routers
• 想找「这个业务逻辑在哪」→ 看 services
• 想找「这个字段在数据库是什么类型」→ 看 models
• 想找「这个参数怎么校验」→ 看 schemas
前端典型分层(Vue)
src/
├── main.js ← 入口:挂载 app,注册路由和全局状态
├── App.vue
├── router/ ← 路由配置
├── stores/ ← 全局状态(Pinia)
├── api/ ← 接口封装:所有 HTTP 请求集中在这
│ └── user.js export const login = (data) => ...
├── views/ ← 页面组件
├── components/ ← 可复用的 UI 组件
└── utils/ ← 工具函数
└── request.js axios 实例、拦截器
一次「登录」的前端数据流
Login.vue(用户点击)
→ api/user.js(调用 login)
→ utils/request.js(axios 发请求)
→ 后端接口
→ 返回 token
→ stores/user.js(存 token)
→ router.push(跳转首页)
🔑 读项目代码的正确顺序
自上而下(Top-down):不要从下往上读,会迷失在细节里。
• 先看入口(main.py / main.js),知道项目怎么启动
• 再看路由(routers / router),知道有哪些功能
• 找到你要改的功能对应的路由
• 顺着它调用的 service / api 往下读
• 最后看它怎么操作数据(models / stores)
| 10 | PART TEN 十、Vue 响应式的坑 |
数据改了页面没更新?问题都在这。

坑一:忘了 .value
JAVASCRIPT
const count = ref(0)
// ❌ 忘了 .value,等于给 count 重新赋值
count = 1
// ✅ 正确
count.value = 1
坑二:解构 reactive 对象
JAVASCRIPT
const state = reactive({ count: 0, name: '张三' })
// ❌ 解构后,count 变成普通数字,丢失响应式
const { count } = state
count++ // 页面不会更新
// ✅ 方案 1:不解构
state.count++
// ✅ 方案 2:用 toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)
count.value++ // 现在可以了
坑三:给数组直接赋值索引或改长度
Vue 3 用了 Proxy,比 Vue 2 的 Object.defineProperty 覆盖更全面,所以 Vue 2 的老坑在 Vue 3 里基本不存在:
JAVASCRIPT
const list = ref([1, 2, 3])
list.value[0] = 99 // ✅ Vue 3 可以
list.value.length = 1 // ✅ 可以
list.value = [4, 5, 6] // ✅ 也可以
坑四:异步更新,读的是旧值
JAVASCRIPT
const count = ref(0)
function increment() {
count.value++
console.log(count.value) // 1,这个是对的
console.log(document.querySelector('.count').textContent)
// ❌ 还是旧值!因为 DOM 更新是异步的
}
// ✅ 需要等 DOM 更新后
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
console.log(document.querySelector('.count').textContent) // ✅ 新值
}
💡 为什么 DOM 更新是异步的
如果你在同一个事件里改了 10 次数据,Vue 不想更新 DOM 10 次,而是攒起来只更新一次。所以 DOM 更新会被推迟到下一个微任务。
坑五:watch 的深层监听
JAVASCRIPT
const user = reactive({ profile: { name: '张三' } })
// ❌ 只监听 user.profile 这个引用
watch(() => user.profile, (newVal) => {
console.log(newVal)
})
// ✅ 加 deep 监听内部
watch(() => user.profile, (newVal) => {
console.log(newVal)
}, { deep: true })
// ✅ 或直接监听具体字段
watch(() => user.profile.name, (newName) => {
console.log(newName)
})
坑六:v-if 和 v-for 一起用
HTML
<!-- ❌ 不推荐:同一元素上用 v-if 和 v-for -->
<li v-for="item in list" v-if="item.visible">{{ item.name }}</li>
<!-- ✅ 用 computed 过滤 -->
<script setup>
const visibleList = computed(() =>
list.value.filter(item => item.visible)
)
</script>
<template>
<li v-for="item in visibleList">{{ item.name }}</li>
</template>
原因:Vue 3 里 v-if 优先级高于 v-for,会导致 v-if 里访问不到 item。
🔑 响应式的核心思维
• 数据是「响应式对象」时,改它的属性/值会自动触发更新
• 「响应式」是对象/引用的性质,一旦被解构成普通值就丢失了
• DOM 更新是异步的,需要 nextTick 等它
• 遇到不更新,先问自己:我改的是不是响应式对象?
| 11 | PART ELEVEN 十一、学习路线与速查 |
循序渐进的练习路线
第 1-2 周:调试能力
每次遇到 bug,强制自己先用 console.log 或断点定位,不准盲改。一周后你会发现调试速度翻倍。
第 3-4 周:读懂异步代码
找你自己项目里的 5 处 async/await 或 .then,画一张图:数据从哪来、经过哪些异步操作、最后去了哪。你会发现之前觉得「玄学」的代码,其实都是普通流程。
第 5-6 周:吃透 HTTP
打开一个你项目的 Network 面板,点开每一个请求:
• 状态码是什么
• 请求头有哪些字段
• 响应体长什么样
• 用了 token 吗
每看懂一个字段,你对 HTTP 的理解就深一分。
第 7-8 周:Git 实战给自己定规矩:
• 每个功能一个分支
• 每次提交写清楚 commit message
• 遇到问题学会用 git status / git diff / git log 分析
第 9-10 周:跑一个 SQL 环境本地装个 PostgreSQL 或 MySQL,手动建表、插入、查询。然后再用 ORM 做一遍,对照着理解 db.query(User) 背后是什么 SQL。
第 11-12 周:完整读一个项目挑一个你写过的 Vue + Python 项目,按照第⑨章的方法,完整画出:路由 → 业务 → 模型 的分层图和一次请求的完整链路。画完那一刻,你对项目的掌控感会完全不一样。
核心概念一句话速查
两期合起来的知识地图
第一层:操作系统与命令行(第一期)
├── 命令行基础 / cwd / 管道
├── PATH / which / node_modules/.bin
├── 环境变量 / .env
├── 路径 / 权限 / chmod
└── 进程 / 端口 / Nginx / 部署
第二层:代码阅读与调试(第二期)
├── console.log / debugger / DevTools
├── 异步 JS / Promise / 事件循环
├── 装饰器 / 响应式原理
├── HTTP / 状态码 / Token / CORS
├── Git / SQL / ORM
└── 项目分层 / 数据流 / 报错阅读
第三层:架构与设计(下一期)
├── 系统设计 / 高并发
├── 微服务 / 消息队列
├── 性能优化 / 缓存策略
└── 自动化测试 / CI/CD
🎯 一句话总结第二期
所有框架的「魔法」都能拆成普通代码;所有 bug 都能靠调试定位;所有协议都有它的设计理由。把这三个认知记在心里,你就从「用工具的人」变成了「理解工具的人」。下一期我们往更高层走:架构设计和系统思维。但别急,先把这一期动手练熟。
Vibe Coding 底层知识补全计划 · 第二期 · 从能跑就行到真正读懂代码保存到本地随时翻阅 · 每章动手做一遍胜过读十遍
法律AI技术研究应用
LAW × AI · RESEARCH & PRACTICE