Skip to content

Jiess 响应式观察 ​

Jiess 内置了强大的响应式系统,让你能够轻松创建响应式数据并监听其变化。本节详细介绍 $ref、$reactive、$watch、$watchEffect、$computed 的完整用法。

目录 ​

响应式数据创建 ​

Jiess 提供了两种创建响应式数据的方式:$ref 和 $reactive。

核心规则 ​

  • ✅ 对于对象或数组,Jiess 自动递归创建响应式对象,确保深层属性变化也能被捕获
  • ⚠️ 禁止循环引用,Jiess 会检测并返回原对象
  • ⚠️ 仅初次创建时递归,后续赋值的子对象不会自动转为响应式,需手动再次调用

$ref - 通用响应式数据 ​

$ref 是最通用的响应式创建函数,适用于任何类型。

API 签名 ​

typescript
function $ref<T>(value?: T, mark?: string, options?: { shallow?: boolean }): TypeRef<T>

参数:

参数说明
value初始值,任意类型,不传默认为 undefined
mark标记名,用于缓存相同标记的 ref 实例,实现单例效果
options.shallow是否浅响应式,默认 false(深度响应式)

返回值: { value: T } 形式的响应式引用,通过 .value 读写。

基本用法 ​

javascript
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 ​

浅响应式只对顶层属性做响应式处理,嵌套属性不自动转换——适合大型对象:

javascript
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 实例,适合全局状态管理:

javascript
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 签名 ​

typescript
function $reactive<T extends object>(object: T, options?: { shallow?: boolean }): T

基本用法 ​

javascript
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 })  // 触发 ✅

浅响应式对象 ​

只对顶层属性响应,嵌套不改:

javascript
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)

边界情况处理 ​

javascript
// 循环引用
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)  // ✅ 正常工作

检查响应式类型 ​

javascript
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. 表单状态管理 ​

javascript
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. 用户信息 + 计算属性 ​

javascript
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. 购物车 ​

javascript
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. 主题切换 ​

javascript
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 允许你精确指定要监听的数据和字段。

基本语法 ​

javascript
const stop = $watch(proxy, param?, callback)
参数说明
proxy响应式数据($ref 返回的代理 / $reactive 返回的代理)
param可选,指定监听目标:字符串(字段/路径)、数字(数组索引)、{ field, immediate } 配置对象
callback(newVal, oldVal) => void
返回值() => void —— 调用后停止监听

如果 param 为函数类型,$watch 自动判定为二参数模式 $watch(proxy, callback),监听整个对象。

监听 ref 数据 ​

javascript
const count = $ref(0)

$watch(count, 'value', (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

count.value++  // count: 0 → 1

监听 reactive 数据 ​

监听单个字段 ​

javascript
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

监听嵌套路径 ​

使用点号路径监听嵌套字段:

javascript
const user = $reactive({
  name: '张三',
  address: { city: '北京', district: '海淀区' }
})

$watch(user, 'address.city', (newVal, oldVal) => {
  console.log(`城市: ${oldVal} → ${newVal}`)
})

user.address.city = '上海'  // 城市: 北京 → 上海

监听整个对象 ​

param 为 undefined 或 callback 本身时,监听对象的所有变化:

javascript
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 和直接索引赋值:

javascript
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

监听数组特定索引 ​

javascript
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)

监听数组元素的属性 ​

javascript
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 可以是一个配置对象:

配置项类型默认值说明
fieldstring—要监听的字段路径,如 'name' 或 'address.city'
immediatebooleanfalse是否在创建监听器时立即执行一次回调
isSyncbooleanfalse是否同步触发回调(跳过防抖合并),默认异步防抖
javascript
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 = '上海'  // 城市: 北京 → 上海

立即执行示例 ​

javascript
const count = $ref(0)

$watch(count, {
  field: 'value',
  immediate: true
}, (newVal, oldVal) => {
  console.log(`count: ${newVal}(旧值: ${oldVal})`)
})
// 立即输出:count: 0(旧值: undefined)

实际应用:表单验证 ​

javascript
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 有两种传参模式:

javascript
// 模式 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异步触发(防抖合并)

基本示例 ​

javascript
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                     // ✅ 同上(防抖合并为一次)

详细示例:嵌套属性观察 ​

javascript
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 配置:

javascript
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({...})(数组变异)✅ 触发(数组方法重写)✅ 触发

如果需要在嵌套属性变更时更新监听该字段的视图,有两种方案:

  1. 使用 $watch(reactive, {}, cb) 无字段观察(异步防抖,覆盖所有变更)
  2. 在修改嵌套属性后显式替换引用:state.users = [...state.users]

停止整体观察 ​

javascript
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 返回一个清理函数,调用它即可停止监听:

javascript
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) 也返回清理函数:

javascript
const count = $ref(0)

const stop = $watchEffect(() => {
  console.log(`count: ${count.value}`)
})

count.value++  // count: 1
stop()
count.value++  // 无输出

清理函数的应用场景 ​

组件卸载时清理 ​

javascript
function setup() {
  const state = $reactive({ count: 0 })
  const unwatch = $watch(state, 'count', (n, o) => {
    console.log(`count: ${o} → ${n}`)
  })

  this.onBeforeUnmount(() => {
    unwatch()  // 防止内存泄漏
  })
}

条件监听 ​

javascript
const count = $ref(0)
let unwatch = null

function startWatch() {
  unwatch = $watch(count, 'value', v => console.log(v))
}
function stopWatch() {
  unwatch?.()
  unwatch = null
}

动态切换监听目标 ​

javascript
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 停止函数

基本用法 ​

javascript
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同步立即执行每个修改都触发一次
javascript
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

实际应用:自动保存 ​

javascript
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 的回调可以返回一个清理函数——在下次执行前调用,适合清理定时器、取消请求:

javascript
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

清理事件监听 ​

javascript
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)

取消网络请求 ​

javascript
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  // 取消前一个请求,发起新请求

高级用法:与异步操作结合 ​

javascript
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  // 自动重新获取

常见问题与解决方案 ​

问题:条件语句中的依赖未被收集 ​

javascript
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)
})

问题:异步函数中依赖未被收集 ​

javascript
// ❌ setTimeout 中的访问不会被收集
$watchEffect(() => { setTimeout(() => console.log(count.value), 0) })

// ✅ 同步代码中访问
$watchEffect(() => {
  const c = count.value
  setTimeout(() => console.log(c), 0)
})

问题:无限循环 ​

javascript
// ❌ 回调中修改依赖 → 无限循环
$watchEffect(() => count.value++)

// ✅ 引入外部触发器
$watchEffect(() => { console.log(count.value) })
setInterval(() => count.value++, 1000)

$computed - 智能计算属性 ​

$computed 根据依赖的响应式数据自动计算结果,并在依赖变化时惰性重新计算。

基本用法 ​

javascript
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:

javascript
$computed(() => a.value + b.value, 'pre')
$computed(() => a.value + b.value, 'post')
$computed(() => a.value + b.value, 'sync')

带 setter 的计算属性 ​

javascript
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)   // 四

复杂计算示例 ​

购物车总价 ​

javascript
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

过滤和排序 ​

javascript
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)

计算属性的缓存机制 ​

只依赖变化时重新计算,其余情况直接返回缓存:

javascript
let computeCount = 0
const doubled = $computed(() => { computeCount++; return count.value * 2 })

doubled.value  // 计算 1 次
doubled.value  // 从缓存读 ← 不计算
count.value++   // 依赖变了
doubled.value  // 重新计算
doubled.value  // 从缓存读

监听计算属性 ​

计算属性本身也是响应式的——用 $watch 监听它:

javascript
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) 逐一比对查找,父对象键越多开销越大。

javascript
// ❌ 旧方案: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) 查表。

javascript
// ✅ 新方案: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 链向上冒泡。

javascript
// deleteProperty 不传 receiver!
deleteProperty: (target, key) => {
  // 无法直接获取 proxy → 无法从 target 读取 JIESS_PARENT
}

解决方案:在 Observer.create 中记录 _targetToProxy.set(target, proxy),delete 时通过 target 反查 proxy。

javascript
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)所有元素都移位
javascript
// 验证示例: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 + 比对 10000O(20000)O(1)
pop()拷贝 10000 + 比对 10000O(20000)O(1)
splice(5000, 1)拷贝 10000 + 比对 10000O(20000)O(5000)
sort()拷贝 10000 + 比对 10000O(20000)O(20000)(无变化)

设计要点:P1 必须先于 P2/P3 执行(先更新映射,再触发 setter),确保冒泡链路中的键始终正确。