Jiess 响应式观察
Jiess 内置了强大的响应式系统,让你能够轻松创建响应式数据并监听其变化。本节详细介绍 $ref、$reactive、$watch、$watchEffect、$computed 的完整用法。
目录
响应式数据创建
Jiess 提供了两种创建响应式数据的方式:$ref 和 $reactive。
核心规则
- ✅ 对于对象或数组,Jiess 自动递归创建响应式对象,确保深层属性变化也能被捕获
- ⚠️ 禁止循环引用,Jiess 会检测并返回原对象
- ⚠️ 仅初次创建时递归,后续赋值的子对象不会自动转为响应式,需手动再次调用
$ref - 通用响应式数据
$ref 是最通用的响应式创建函数,适用于任何类型。
API 签名
function $ref<T>(value?: T, mark?: string, options?: { shallow?: boolean }): TypeRef<T>参数:
| 参数 | 说明 |
|---|---|
value | 初始值,任意类型,不传默认为 undefined |
mark | 标记名,用于缓存相同标记的 ref 实例,实现单例效果 |
options.shallow | 是否浅响应式,默认 false(深度响应式) |
返回值: { value: T } 形式的响应式引用,通过 .value 读写。
基本用法
import { $ref } from '@jiess/core'
const count = $ref(0)
const message = $ref('Hello')
const isVisible = $ref(true)
const user = $ref({ name: '张三' })
const list = $ref([1, 2, 3])
console.log(count.value) // 0
count.value++ // 触发响应式更新浅响应式 ref
浅响应式只对顶层属性做响应式处理,嵌套属性不自动转换——适合大型对象:
import { $ref, $watch } from '@jiess/core'
const obj = { nested: { value: 10 } }
const ref = $ref(obj, undefined, { shallow: true })
let callbackCount = 0
$watch(ref, 'value.nested.value', () => { callbackCount++ })
ref.value = { nested: { value: 20 } } // 替换整个 value:触发监听 ✅
console.log(ref.value.nested.value) // 20
ref.value.nested.value = 30 // 修改嵌套:不触发监听 ❌(浅响应式)
console.log(callbackCount) // 仍为 1标记缓存
相同标记名返回同一个 ref 实例,适合全局状态管理:
const ref1 = $ref(0, 'my-ref')
const ref2 = $ref(0, 'my-ref')
console.log(ref1 === ref2) // true
ref1.value = 10
console.log(ref2.value) // 10 —— 同步变化$reactive - 对象/数组响应式
$reactive 专门用于对象和数组。返回直接的代理对象,无需 .value。
API 签名
function $reactive<T extends object>(object: T, options?: { shallow?: boolean }): T基本用法
import { $reactive } from '@jiess/core'
const user = $reactive({
name: '张三', age: 28,
address: { city: '北京', district: '海淀区' }
})
user.name = '李四' // 触发更新 ✅
user.address.city = '上海' // 嵌套也触发 ✅
const todos = $reactive([
{ id: 1, text: '学习 Jiess', done: false },
{ id: 2, text: '编写组件', done: true }
])
todos.push({ id: 3, text: '测试响应式', done: false }) // 触发 ✅浅响应式对象
只对顶层属性响应,嵌套不改:
const obj = { nested: { value: 10 } }
const proxy = $reactive(obj, { shallow: true })
let count = 0
$watch(proxy, 'nested', () => { count++ })
proxy.nested = { value: 20 } // 替换 nested:触发 ✅(count: 1)
proxy.nested.value = 30 // 修改嵌套:不触发 ❌(count: 仍 1)边界情况处理
// 循环引用
const loopObj = { name: 'test' }
loopObj.self = loopObj
const loopProxy = $reactive(loopObj) // ✅ 自动检测
// 超大对象
const largeObj = {}
for (let i = 0; i < 10000; i++) largeObj[`prop${i}`] = i
const largeProxy = $reactive(largeObj) // ✅ 正常工作检查响应式类型
import { $ref, $isRef, $reactive, $isReactive } from '@jiess/core'
const count = $ref(0)
$isRef(count) // true
$isRef(0) // false
const user = $reactive({ name: '张三' })
$isReactive(user) // true
$isReactive({ name: '张三' }) // false$ref vs $reactive:如何选择?
| 特性 | $ref | $reactive |
|---|---|---|
| 适用类型 | 所有类型 | 仅对象/数组 |
| 访问方式 | 需 .value | 直接访问 |
| 嵌套处理 | 自动递归 | 自动递归 |
| 整体替换 | count.value = newObj ✅ | 直接 obj = xxx 会丢失响应式 ❌ |
| 缓存 | 支持标记缓存 | 不支持缓存 |
$watch(obj, {}, cb) | 自动补 field='value',同步触发 | field 保持 undefined,异步防抖触发 |
| 推荐场景 | 基本类型、需整体替换的对象 | 表单数据集合 |
实际应用示例
1. 表单状态管理
const formState = $ref({ username: '', password: '', remember: false })
$watch(formState, 'value.username', (newVal, oldVal) => {
console.log(`用户名:${oldVal} → ${newVal}`)
})
// 整体替换
formState.value = { username: 'admin', password: '123', remember: true }
function submitForm() {
console.log('提交:', formState.value)
}2. 用户信息 + 计算属性
const userInfo = $reactive({
firstName: '张', lastName: '三', age: 28,
settings: { theme: 'light', notifications: true }
})
const fullName = $computed(() => `${userInfo.firstName}${userInfo.lastName}`)
const isAdult = $computed(() => userInfo.age >= 18)
userInfo.settings.theme = 'dark'
console.log('全名:', fullName.value, '成年:', isAdult.value)3. 购物车
const cart = $reactive({
items: [
{ id: 1, name: '商品1', price: 100, quantity: 1 },
{ id: 2, name: '商品2', price: 200, quantity: 2 }
],
discount: 0.9
})
const totalQuantity = $computed(() =>
cart.items.reduce((s, i) => s + i.quantity, 0)
)
const totalPrice = $computed(() => {
const sub = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
return (sub * cart.discount).toFixed(2)
})
$watch(cart, 'items', () => console.log('购物车变了'))
function addItem(item) {
const existing = cart.items.find(i => i.id === item.id)
existing ? existing.quantity++ : cart.items.push({ ...item, quantity: 1 })
}
function removeItem(id) {
const idx = cart.items.findIndex(i => i.id === id)
if (idx > -1) cart.items.splice(idx, 1)
}4. 主题切换
const theme = $ref('light')
const fontSize = $ref('medium')
$watchEffect(() => {
document.documentElement.setAttribute('data-theme', theme.value)
document.documentElement.setAttribute('data-font-size', fontSize.value)
})
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
function changeFontSize(size) {
fontSize.value = size
}$watch - 精确监听数据变化
$watch 允许你精确指定要监听的数据和字段。
基本语法
const stop = $watch(proxy, param?, callback)| 参数 | 说明 |
|---|---|
proxy | 响应式数据($ref 返回的代理 / $reactive 返回的代理) |
param | 可选,指定监听目标:字符串(字段/路径)、数字(数组索引)、{ field, immediate } 配置对象 |
callback | (newVal, oldVal) => void |
| 返回值 | () => void —— 调用后停止监听 |
如果
param为函数类型,$watch自动判定为二参数模式$watch(proxy, callback),监听整个对象。
监听 ref 数据
const count = $ref(0)
$watch(count, 'value', (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
count.value++ // count: 0 → 1监听 reactive 数据
监听单个字段
const user = $reactive({ name: '张三', age: 28 })
$watch(user, 'name', (newVal, oldVal) => {
console.log(`姓名: ${oldVal} → ${newVal}`)
})
$watch(user, 'age', (newVal, oldVal) => {
console.log(`年龄: ${oldVal} → ${newVal}`)
})
user.name = '李四' // 姓名: 张三 → 李四
user.age = 29 // 年龄: 28 → 29监听嵌套路径
使用点号路径监听嵌套字段:
const user = $reactive({
name: '张三',
address: { city: '北京', district: '海淀区' }
})
$watch(user, 'address.city', (newVal, oldVal) => {
console.log(`城市: ${oldVal} → ${newVal}`)
})
user.address.city = '上海' // 城市: 北京 → 上海监听整个对象
param 为 undefined 或 callback 本身时,监听对象的所有变化:
const user = $reactive({ name: '张三', age: 28 })
// 方式一:二参数模式
const stop = $watch(user, (newVal, oldVal) => {
console.log('user 对象变了', newVal, oldVal)
})
// 方式二:三参数模式(param 不传)
$watch(user, undefined, (newVal, oldVal) => {
console.log('user 对象变了')
})
user.name = '李四' // 触发
user.age = 29 // 触发
stop() // 停止监听数组
Jiess 覆盖了数组的所有变异:push、pop、splice、sort、reverse 和直接索引赋值:
const todos = $reactive([
{ id: 1, text: '学习 Jiess', done: false },
{ id: 2, text: '编写组件', done: true }
])
let changes = 0
$watch(todos, undefined, () => { changes++ })
todos.push({ id: 3, text: '测试数组监听', done: false }) // changes: 1
todos.pop() // changes: 2
todos.splice(0, 1) // changes: 3
todos.sort() // changes: 4
todos.reverse() // changes: 5
todos[0].text = '更新任务' // changes: 6
todos[0].done = true // changes: 7监听数组特定索引
const arr = $reactive([1, 2, 3, 4, 5])
let count = 0
$watch(arr, '2', (newVal, oldVal) => {
count++
console.log(`arr[2]: ${oldVal} → ${newVal}`)
})
arr[2] = 10 // 触发(count: 1)
arr[3] = 20 // 不触发(count: 仍 1)
arr.splice(2, 1, 30) // 触发(count: 2)监听数组元素的属性
const users = $reactive([
{ id: 1, name: '张三', age: 28 },
{ id: 2, name: '李四', age: 30 }
])
let count = 0
$watch(users, '0.name', (newVal, oldVal) => {
count++
console.log(`第一个用户的姓名: ${oldVal} → ${newVal}`)
})
users[0].name = '张五' // 触发 ✅ count: 1
users[1].name = '李六' // 不触发 count: 1
users[0] = { id: 1, name: '张六', age: 29 } // 触发 ✅ count: 2配置选项
param 可以是一个配置对象:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
field | string | — | 要监听的字段路径,如 'name' 或 'address.city' |
immediate | boolean | false | 是否在创建监听器时立即执行一次回调 |
isSync | boolean | false | 是否同步触发回调(跳过防抖合并),默认异步防抖 |
const user = $reactive({
name: '张三',
address: { city: '北京', district: '海淀区' }
})
// 使用配置对象
$watch(user, {
field: 'address.city',
immediate: true // 立即执行一次
}, (newVal, oldVal) => {
console.log(`城市: ${oldVal} → ${newVal}`)
})
// immediate: true → 立即输出"城市: undefined → 北京"
// 修改时触发
user.address.city = '上海' // 城市: 北京 → 上海立即执行示例
const count = $ref(0)
$watch(count, {
field: 'value',
immediate: true
}, (newVal, oldVal) => {
console.log(`count: ${newVal}(旧值: ${oldVal})`)
})
// 立即输出:count: 0(旧值: undefined)实际应用:表单验证
const email = $ref('')
const emailError = $ref('')
$watch(email, 'value', (newVal) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!newVal) {
emailError.value = '邮箱不能为空'
} else if (!regex.test(newVal)) {
emailError.value = '请输入有效的邮箱地址'
} else {
emailError.value = ''
}
})
email.value = 'invalid' // → '请输入有效的邮箱地址'
email.value = 'user@example.com' // → ''$watch 无 field 观察 —— $ref 与 $reactive 差异
从前面的例子可以看到,$watch 有两种传参模式:
// 模式 A:三参数(指定 field)
$watch(proxy, 'fieldName', callback)
// 模式 B:二参数或空对象(不指定 field)
$watch(proxy, {}, callback) // 不指定 field
$watch(proxy, callback) // 等同于上面当不指定 field 时,$ref 和 $reactive 的处理方式完全不同:
| 场景 | 行为 | 内部机制 | 触发时机 |
|---|---|---|---|
$watch(ref, {}, cb) | 自动补 field='value',仅观察 .value 整体替换 | Watcher 检测 __JIESS_REF__ 标记后自动设 key='value' | 同步触发 |
$watch(reactive, {}, cb) | field 保持 undefined,整体观察,任意属性变更都触发 | observer 存入 _proxyToObserveNo | 异步触发(防抖合并) |
基本示例
import { $ref, $reactive, $watch } from '@jiess/core'
const ref = $ref({ data: 'initial' })
const state = $reactive({ name: '张三', age: 28 })
// $ref:自动补 field='value',只观察 .value 的直接赋值(同步触发)
$watch(ref, {}, (newVal, oldVal) => {
console.log('ref.value 整体被替换:', oldVal, '→', newVal)
})
// $reactive:整体观察,任意属性变更都触发(异步防抖)
$watch(state, {}, (newVal) => {
console.log('state 对象属性变更:', newVal)
})
ref.value = { data: 'updated' } // ✅ 同步触发 ref 回调
ref.value.data = 'changed' // ❌ 不触发(嵌套属性变更)
state.name = '李四' // ✅ 异步触发 state 回调
state.age = 30 // ✅ 同上(防抖合并为一次)详细示例:嵌套属性观察
const ref = $ref({ nested: { value: 10 } })
const state = $reactive({ inner: { value: 20 } })
let refCount = 0, stateCount = 0
$watch(ref, {}, () => { refCount++ })
$watch(state, {}, () => { stateCount++ })
// $ref:嵌套属性变更不触发
ref.value.nested.value = 99
console.log(refCount) // 0 ← 不触发!
// 原因:ref observer 在 _proxyToObserve(key='value')中,
// Observer.trigger 父级冒泡仅向上传播一层(到 ref 的 value 代理),
// 而 Watcher.update 只触发 _proxyToObserveNo(无字段观察者),
// 不触发 _proxyToObserve(有字段观察者,如 key='value')
// 只有直接替换 .value 才触发
ref.value = { nested: { value: 99 } }
console.log(refCount) // 1 ✅
// $reactive:嵌套属性变更通过父链传播触发(异步)
state.inner.value = 99
// 等待异步防抖完成
setTimeout(() => {
console.log(stateCount) // 1 ✅
}, 50)isSync 配置:同步触发
默认情况下,$watch(reactive, {}, cb) 的回调是异步防抖的(使用 debounce1(() => done(), 0) 并入微任务队列)。若需要立即同步触发,使用 isSync 配置:
const state = $reactive({ count: 0 })
let asyncCount = 0
$watch(state, {}, () => { asyncCount++ })
let syncCount = 0
$watch(state, { isSync: true }, () => { syncCount++ })
state.count = 1
console.log(asyncCount) // 0 ← 尚未触发
console.log(syncCount) // 1 ← 已同步触发
state.count = 2
console.log(asyncCount) // 0 ← 仍在等待
console.log(syncCount) // 2 ← 再次同步触发
// 等待异步回调完成
setTimeout(() => {
console.log(asyncCount) // 1 ← 被防抖合并为一次
}, 50)防抖机制详解
| 场景 | 触发机制 | 多次连续修改 |
|---|---|---|
$watch(ref, {}, cb) | 同步 | 每次赋值都触发 |
$watch(reactive, {}, cb) | debounce1(done, 0) 异步 | 合并为一次回调 |
$watch(reactive, { isSync: true }, cb) | 同步 | 每次变更都触发 |
$watch(reactive, 'field', cb) | 同步 | 每次变更都触发 |
为什么需要防抖? 当整体观察一个大对象时,短时间内可能有多个属性同时变更。防抖机制确保在所有变更完成后,只触发一次回调,避免不必要的高频更新。
重要设计语义:keyed 观察者与嵌套属性变更
关键理解:
$watch(reactive, 'field', cb)监听的是 field 引用变化,而非 field 内部属性变化。当修改reactive.field.subField时,field的引用未变,因此 keyed 观察者不会被触发。
这是 Jiess 响应式系统的核心设计语义之一:
| 操作 | $watch(state, 'users', cb) | $watch(state, {}, cb) |
|---|---|---|
state.users = [...](替换引用) | ✅ 同步触发 | ✅ 异步触发 |
state.users[0].role = 'admin'(嵌套属性) | ❌ 不触发 | ✅ 异步触发 |
state.users.push({...})(数组变异) | ✅ 触发(数组方法重写) | ✅ 触发 |
如果需要在嵌套属性变更时更新监听该字段的视图,有两种方案:
- 使用
$watch(reactive, {}, cb)无字段观察(异步防抖,覆盖所有变更) - 在修改嵌套属性后显式替换引用:
state.users = [...state.users]
停止整体观察
const state = $reactive({ count: 0 })
let callbackCount = 0
const stop = $watch(state, {}, () => { callbackCount++ })
state.count = 1
// 异步触发...
await new Promise(resolve => setTimeout(resolve, 50))
console.log(callbackCount) // 1
stop() // 主动停止
state.count = 2
await new Promise(resolve => setTimeout(resolve, 50))
console.log(callbackCount) // 仍然为 1,已停止监听器清理机制
$watch 清理
$watch 返回一个清理函数,调用它即可停止监听:
const count = $ref(0)
const stop = $watch(count, 'value', (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
count.value++ // count: 0 → 1
stop() // 停止监听
count.value++ // 无输出$watchEffect 清理
同上,$watchEffect(fn) 也返回清理函数:
const count = $ref(0)
const stop = $watchEffect(() => {
console.log(`count: ${count.value}`)
})
count.value++ // count: 1
stop()
count.value++ // 无输出清理函数的应用场景
组件卸载时清理
function setup() {
const state = $reactive({ count: 0 })
const unwatch = $watch(state, 'count', (n, o) => {
console.log(`count: ${o} → ${n}`)
})
this.onBeforeUnmount(() => {
unwatch() // 防止内存泄漏
})
}条件监听
const count = $ref(0)
let unwatch = null
function startWatch() {
unwatch = $watch(count, 'value', v => console.log(v))
}
function stopWatch() {
unwatch?.()
unwatch = null
}动态切换监听目标
let unwatch = null
function watchTarget(proxy, field) {
unwatch?.() // 先清理旧监听
unwatch = $watch(proxy, field, (n, o) => {
console.log(`${field}: ${o} → ${n}`)
})
}$watchEffect - 自动依赖收集
$watchEffect 自动收集回调中访问的所有响应式数据,任一变化都触发回调。
$watchEffect vs $watch
| 特性 | $watch | $watchEffect |
|---|---|---|
| 依赖声明 | 手动指定字段 | 自动收集 |
| 初始执行 | 仅监听,不执行(除非 immediate: true) | 立即执行一次,用于收集依赖 |
| 适合场景 | 精确监听某个字段 | 多个数据组合的副作用 |
| 返回值 | () => void 停止函数 | () => void 停止函数 |
基本用法
const count = $ref(0)
const message = $ref('Hello')
$watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`)
})
count.value++ // count: 1, message: Hello
message.value = 'World' // count: 1, message: World观察模式
$watchEffect(effect, flush?) 支持三种模式:
| 模式 | 执行时机 | 特点 |
|---|---|---|
pre(默认) | 微任务(Promise.resolve) | 批量合并,多个同步修改只触发一次 |
post | 宏任务(setTimeout) | 延迟更长 |
sync | 同步立即执行 | 每个修改都触发一次 |
const count = $ref(0)
// pre(默认):连续三次 +=1,只触发一次,输出最终值 3
$watchEffect(() => console.log(`count: ${count.value}`))
count.value++; count.value++; count.value++ // 输出: count: 3
// sync:每次 +=1 都触发
$watchEffect(() => console.log(count.value), 'sync')
count.value++; count.value++ // 输出: 1 2实际应用:自动保存
const formData = $ref({ title: '', content: '', tags: [] })
let saveTimer = null
$watchEffect(() => {
clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
console.log('自动保存:', formData.value)
}, 1000)
})
formData.value.title = '新文章'
formData.value.content = '内容...'
// 1 秒后自动保存清理函数
$watchEffect 的回调可以返回一个清理函数——在下次执行前调用,适合清理定时器、取消请求:
const interval = $ref(1000)
$watchEffect(() => {
console.log(`设置定时器,间隔: ${interval.value}ms`)
const timer = setInterval(() => console.log('执行'), interval.value)
return () => { clearInterval(timer); console.log('已清理定时器') }
})
interval.value = 2000
// 输出:已清理定时器
// 输出:设置定时器,间隔: 2000ms清理事件监听
const isListening = $ref(true)
$watchEffect(() => {
if (!isListening.value) return
const handler = () => console.log('点击')
window.addEventListener('click', handler)
return () => window.removeEventListener('click', handler)
})
setTimeout(() => { isListening.value = false }, 5000)取消网络请求
const userId = $ref(1)
$watchEffect(() => {
const id = userId.value
console.log(`请求用户 ${id}`)
const controller = new AbortController()
fetch(`/api/user/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(d => console.log('数据:', d))
.catch(e => { if (e.name !== 'AbortError') console.error(e) })
return () => controller.abort()
})
userId.value = 2 // 取消前一个请求,发起新请求高级用法:与异步操作结合
const userId = $ref(1)
const userData = $ref(null)
$watchEffect(async () => {
const id = userId.value // 同步访问,确保被收集
console.log(`获取用户 ${id}...`)
const data = await new Promise(r =>
setTimeout(() => r({ id, name: `用户${id}` }), 1000)
)
userData.value = data
})
userId.value = 2 // 自动重新获取常见问题与解决方案
问题:条件语句中的依赖未被收集
const count = $ref(1)
const show = $ref(false)
// ❌ 初始化时 show.value 为 false,count 未被访问 → 未收集
$watchEffect(() => { if (show.value) console.log(count.value) })
// ✅ 在条件外提前访问
$watchEffect(() => {
const c = count.value // ← 保证被收集
if (show.value) console.log(c)
})问题:异步函数中依赖未被收集
// ❌ setTimeout 中的访问不会被收集
$watchEffect(() => { setTimeout(() => console.log(count.value), 0) })
// ✅ 同步代码中访问
$watchEffect(() => {
const c = count.value
setTimeout(() => console.log(c), 0)
})问题:无限循环
// ❌ 回调中修改依赖 → 无限循环
$watchEffect(() => count.value++)
// ✅ 引入外部触发器
$watchEffect(() => { console.log(count.value) })
setInterval(() => count.value++, 1000)$computed - 智能计算属性
$computed 根据依赖的响应式数据自动计算结果,并在依赖变化时惰性重新计算。
基本用法
const firstName = $ref('张')
const lastName = $ref('三')
const fullName = $computed(() => `${firstName.value}${lastName.value}`)
console.log(fullName.value) // 张三
lastName.value = '四'
console.log(fullName.value) // 张四(自动更新)观察模式
与 $watchEffect 相同,$computed(fn, flush?) 支持 pre(默认)/ post / sync:
$computed(() => a.value + b.value, 'pre')
$computed(() => a.value + b.value, 'post')
$computed(() => a.value + b.value, 'sync')带 setter 的计算属性
const firstName = $ref('张')
const lastName = $ref('三')
const fullName = $computed({
get: () => `${firstName.value}${lastName.value}`,
set: (value) => {
const [a, b] = value.split('')
firstName.value = a
lastName.value = b
}
})
fullName.value = '李四'
console.log(firstName.value) // 李
console.log(lastName.value) // 四复杂计算示例
购物车总价
const cart = $reactive({
items: [
{ name: '商品1', price: 100, quantity: 2 },
{ name: '商品2', price: 200, quantity: 1 },
{ name: '商品3', price: 50, quantity: 3 }
],
discount: 0.9
})
const totalPrice = $computed(() => {
const sub = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
return (sub * cart.discount).toFixed(2)
})
console.log(totalPrice.value) // 405.00
cart.items[0].quantity = 3
console.log(totalPrice.value) // 495.00
cart.discount = 0.8
console.log(totalPrice.value) // 480.00过滤和排序
const todos = $reactive([
{ id: 1, text: '学习 Jiess', done: false },
{ id: 2, text: '编写组件', done: true },
{ id: 3, text: '测试响应式', done: false }
])
const filter = $ref('all') // all | active | completed
const sortBy = $ref('id') // id | text
const filteredTodos = $computed(() => {
let result = [...todos]
if (filter.value === 'active') result = result.filter(t => !t.done)
else if (filter.value === 'completed') result = result.filter(t => t.done)
result.sort((a, b) => a[sortBy.value] < b[sortBy.value] ? -1 : 1)
return result
})
const completedCount = $computed(() => todos.filter(t => t.done).length)
const remainingCount = $computed(() => todos.length - completedCount.value)计算属性的缓存机制
只依赖变化时重新计算,其余情况直接返回缓存:
let computeCount = 0
const doubled = $computed(() => { computeCount++; return count.value * 2 })
doubled.value // 计算 1 次
doubled.value // 从缓存读 ← 不计算
count.value++ // 依赖变了
doubled.value // 重新计算
doubled.value // 从缓存读监听计算属性
计算属性本身也是响应式的——用 $watch 监听它:
const count = $ref(0)
const doubled = $computed(() => count.value * 2)
$watch(doubled, 'value', (newVal, oldVal) => {
console.log(`doubled: ${oldVal} → ${newVal}`)
})
count.value++ // doubled: 0 → 2计算属性 vs 普通函数
| 特性 | 计算属性 | 普通函数 |
|---|---|---|
| 缓存 | ✅ 依赖不变返回缓存 | ❌ 每次调用都计算 |
| 响应式结果 | ✅ 结果可被 $watch 监听 | ❌ 结果不响应式 |
| 依赖收集 | ✅ 自动 | ❌ 无 |
| 适用场景 | 复杂计算、频繁读取 | 一次性计算 |
响应式系统总结
| API | 一句话 |
|---|---|
$ref(value) | 通用响应式,用 .value 读写 |
$reactive(obj) | 对象/数组响应式,直接操作属性 |
$computed(fn) | 带缓存的计算属性,惰性求值 |
$watch(proxy, 'field', cb) | 精确监听→返回停止函数 |
$watchEffect(fn) | 自动收集依赖→返回停止函数 |
$set(obj, key, val) | 动态添加响应式属性 |
$isRef / $isReactive | 类型判断 |
性能优化最佳实践
1. 父子链键名 O(1) 查找(_childKeyMap)
内部优化:Observer.trigger 中的
JIESS_PARENT冒泡查找从 O(n) 降至 O(1)。
当嵌套的子对象属性变更时,响应式系统需要向上冒泡通知父级观察者。冒泡时必须知道子对象在父级中的键名(例如 state.config.debug 变更时,需知道 config 是 state 的哪个键)。
旧方案的问题:每次冒泡都需要遍历 Object.keys(parentProxy) 逐一比对查找,父对象键越多开销越大。
// ❌ 旧方案:O(n) 遍历,父对象有 N 个键就比对 N 次
const parentKeys = Object.keys(parentProxy)
for (const parentKey of parentKeys) {
if (parentProxy[parentKey] === childProxy) {
// 找到了,触发父级
break
}
}优化方案:利用 Observer.create 递归创建子代理时已有的 key 信息,建立 WeakMap<代理对象, 键名> 映射,直接 O(1) 查表。
// ✅ 新方案:O(1) 查表,无需遍历
const childKey = childKeyMap.get(childProxy)
if (childKey !== undefined) {
trigger(parentProxy, childKey, ...)
}性能收益:
| 父对象键数 | 旧方案(O(n)) | 新方案(O(1)) | 提速 |
|---|---|---|---|
| 10 个键 | 0.003ms(10次比对) | 0.0003ms(1次查表) | 10x |
| 100 个键 | 0.03ms(100次比对) | 0.0003ms(1次查表) | 100x |
| 1000 个键 | 0.3ms(1000次比对) | 0.0003ms(1次查表) | 1000x |
设计要点:
- 使用
WeakMap→ 代理对象被 GC 时自动清理,无内存泄漏 - 所有代理创建路径都记录键名:
create递归 + 数组变异方法(push/unshift/splice) - 无需 fallback → 所有正常路径必然记录
2. 代理对象缓存(_targetToProxy)
功能优化:维护
target原始对象 → proxy代理对象的映射,解决deleteProperty协议缺失receiver的问题。
Proxy 的 deleteProperty 处理函数签名是 (target, key),不传递 receiver(代理对象)。这导致删除操作无法通过 JIESS_PARENT 链向上冒泡。
// deleteProperty 不传 receiver!
deleteProperty: (target, key) => {
// 无法直接获取 proxy → 无法从 target 读取 JIESS_PARENT
}解决方案:在 Observer.create 中记录 _targetToProxy.set(target, proxy),delete 时通过 target 反查 proxy。
deleteProperty: (target, key) => {
if (key in target) {
const bool = Reflect.deleteProperty(target, key)
const proxy = proxyCache.get(target) || target // 反查 proxy
trigger(target, key, undefined, proxy) // 传入 proxy 触发冒泡
return bool
}
return Reflect.deleteProperty(target, key)
}3. 避免不必要的高频触发
- 整体观察用防抖:
$watch(reactive, {}, cb)自动异步防抖,连续修改合并为一次 - 精确监听直接用 field:
$watch(reactive, 'name', cb)同步触发,值不变时被Object.is门拦截 - 格式化/编辑器场景:先
state.code = ''再state.code = code利用 debounce1 合并绕过 Object.is
4. 数组变异差量优化(P1-P3)
核心思路:利用不同数组方法的语义差异,按受影响范围精确处理,避免全量扫描。
P1:移位后 _childKeyMap 键重建
splice/sort/reverse/shift/unshift 导致元素位置变更后,必须更新 _childKeyMap 中移位元素的键,否则父级冒泡会使用过期索引。
| 操作 | 更新范围 | 原因 |
|---|---|---|
splice(start, ...) | [start, target.length) | start 之前的元素不移位 |
sort / reverse | [0, target.length) | 所有元素都可能改变位置 |
shift / unshift | [0, target.length) | 所有元素都移位 |
// 验证示例:splice 删除后,移位元素的键映射正确
const arr = $reactive([{v:0}, {v:1}, {v:2}, {v:3}]);
arr.splice(1, 1); // 移除 [1],原本 [2],[3] 向前移位
// childKeyMap: proxy2→2(新位置), proxy3→3(新位置)
arr[1].v = 999; // 通过 childKeyMap(2) 正确冒泡到父级 items[2]P2:按需浅拷贝
仅 sort/reverse/unshift/shift 需要 [...target] 全量拷贝。push/pop/splice 跳过拷贝,节省大量内存。
P3:差量遍历
push 只遍历新增索引 [oldLength, target.length),pop 只遍历删除索引 [target.length, oldLength),splice 从 start 起遍历至 max(oldLen, newLen)。
| 操作 | 10000 元素数组 | 旧方案 | 新方案 |
|---|---|---|---|
push(1 项) | 拷贝 10000 + 比对 10000 | O(20000) | O(1) |
pop() | 拷贝 10000 + 比对 10000 | O(20000) | O(1) |
splice(5000, 1) | 拷贝 10000 + 比对 10000 | O(20000) | O(5000) |
sort() | 拷贝 10000 + 比对 10000 | O(20000) | O(20000)(无变化) |
设计要点:P1 必须先于 P2/P3 执行(先更新映射,再触发 setter),确保冒泡链路中的键始终正确。