Jiess 响应式绑定
目录
- 响应式绑定对象
- $proxy - 指定响应式数据
- warn - 消除警告
- field - 指定绑定字段
- done - 数据处理函数
- update - 双向同步
- $computed - 计算属性
- $set
- defaultValue - 默认值
- 数组响应式绑定
- 响应式绑定的高级用法
- $clearReactiveCache - 清理缓存
- 性能优化
- 最佳实践
Jiess 通过 Proxy 实现响应式系统,核心流程:依赖收集 → 自动追踪 → 精确更新 → 缓存优化。
核心 API:
function $reactive<T extends object>(object: T, options?: { shallow?: boolean }): T
function $ref<T>(value?: T, mark?: string, options?: { shallow?: boolean }): { value: T }
function $computed<T>(val: Function | { get: Function, set?: Function }, flush?: 'pre' | 'post' | 'sync'): { value: T }
function $watch(proxy: object, param: string | object, callback: (newVal: any, oldVal: any) => void): void
function $watchEffect(effect: () => void | (() => void), flush?: 'pre' | 'post' | 'sync'): () => void
function $set(proxy: object, field: string | number, value: any, param?: any): any
function $isReactive(value: any): boolean
function $isRef(value: any): boolean
function $clearReactiveCache(obj: object): void响应式绑定对象
通过响应式绑定对象将响应式数据绑定到节点,数据变化时视图自动更新。
ObserverData 接口
export interface ObserverData {
$proxy: UnObject // 要绑定的响应式数据(必填)
warn?: boolean | Function // 非响应式数据警告控制
field?: string | number // 绑定字段名,默认 'value'
done?: Function // 数据处理函数,返回转换后的值或节点配置
defaultValue?: unknown // 数据为 undefined 时的默认值
update?: boolean | Function // 是否开启双向同步
}基本用法
const count = $ref(1)
this.add({ $proxy: count }) // 渲染:1
setInterval(() => { count.value++ }, 1000) // 视图自动更新验证响应式状态
console.log($isReactive($reactive({ name: 'test', value: 100 }))) // true
console.log($isReactive({})) // false
console.log($isRef($ref(0))) // true
console.log($isRef(0)) // false边界情况
循环引用不会导致无限递归:
const obj = { name: 'test' }
obj.self = obj
const proxy = $reactive(obj)
console.log(proxy.name) // test
console.log(proxy.self) // 指向自身,安全$proxy - 指定响应式数据
$proxy 是绑定对象的标识属性,指定被绑定的响应式数据。
基本用法
const count = $ref(1)
this.add({ $proxy: count })
const user = $reactive({ name: '张三', age: 28 })
this.add({ is: 'div', children: { $proxy: user, field: 'name' } })属性说明
- 必填属性,含
$proxy的对象会被自动识别为响应式绑定对象 - 原则上应绑定
$ref或$reactive创建的响应式数据
非响应式数据处理
绑定普通数据也能显示,但控制台会警告:
this.add({ $proxy: 123 }) // 渲染:123,控制台警告
this.add({ $proxy: 123, warn: false }) // 消除警告warn - 消除警告
当 $proxy 绑定非响应式数据时,控制台默认会警告。warn 用于控制此行为。
消除警告
this.add({ $proxy: 123, warn: false })自定义警告处理
this.add({
$proxy: 123,
warn: (warning) => {
console.log('自定义警告:', warning)
// 可对接日志系统
}
})浅响应式
对于大型对象可使用浅响应式,仅顶层属性是响应式的:
const obj = { nested: { value: 10 } }
const shallowProxy = $reactive(obj, { shallow: true })
shallowProxy.nested = { value: 20 } // 触发响应
shallowProxy.nested.value = 30 // 不触发响应
const shallowRef = $ref(obj, undefined, { shallow: true })
shallowRef.value = { nested: { value: 20 } } // 触发响应
shallowRef.value.nested.value = 30 // 不触发响应field - 指定绑定字段
field 指定从响应式数据中绑定的字段名,默认值为 'value'。
$ref 场景(默认 field = 'value')
const count = $ref(1)
this.add({ $proxy: count }) // 默认取 count.value
this.add({ $proxy: count, field: 'value' }) // 等价$reactive 场景
const user = $reactive({ name: '张三', age: 28, email: 'zhangsan@example.com' })
this.add({ is: 'div', children: '姓名:' })
this.add({ $proxy: user, field: 'name' })
this.add({ is: 'div', children: '年龄:' })
this.add({ $proxy: user, field: 'age' })数组索引绑定
const fruits = $reactive(['苹果', '香蕉', '橙子'])
this.add({ $proxy: fruits, field: 0 }) // 苹果
this.add({ $proxy: fruits, field: 1 }) // 香蕉done - 数据处理函数
done 在数据渲染前对值进行处理,可返回:转换后的值、节点配置、或另一个响应式绑定对象。
格式化数据
const count = $ref(1)
this.add({ $proxy: count, done: (v) => `${v}个` })
this.add({ $proxy: count, done: (v) => `当前数量:${v}` })条件样式(返回节点配置)
const status = $ref('active')
this.add({
$proxy: status,
done: (value) => {
const styles = {
active: { color: 'green', text: '在线' },
inactive: { color: 'gray', text: '离线' },
banned: { color: 'red', text: '已封禁' }
}
const config = styles[value] || styles.inactive
return {
is: 'span',
style: { color: config.color, fontWeight: 'bold' },
children: config.text
}
}
})嵌套响应式绑定(动态 children)
done 可返回另一个响应式绑定对象,实现多数据联动:
const width = $ref(200)
const height = $ref(100)
this.add({ is: 'div', children: '面积:' })
this.add({
$proxy: width,
done: (w) => ({
$proxy: height,
done: (h) => `${w} × ${h} = ${w * h} 平方像素`
})
})链式深度嵌套
const protocol = $ref('https://')
const domain = $ref('www.example.com')
const path = $ref('about')
this.add({
$proxy: protocol,
done: (proto) => ({
$proxy: domain,
done: (d) => ({
$proxy: path,
done: (p) => {
const url = `${proto}${d}/${p}`
return {
is: 'a', href: url, target: '_blank',
style: { color: 'blue', textDecoration: 'underline' },
children: url
}
}
})
})
})条件渲染 + 多层数据联动
const user = $reactive({ name: '张三', isVip: true, points: 1500 })
this.add({
$proxy: user, field: 'isVip',
done: (isVip) => {
if (isVip) {
return {
$proxy: user, field: 'points',
done: (points) => {
const level = points > 10000 ? '钻石VIP' : points > 5000 ? '黄金VIP' : '普通VIP'
return { is: 'div', className: 'vip-badge', children: `${user.name} - ${level}(${points}积分)` }
}
}
}
return { is: 'div', className: 'normal-user', children: `${user.name} - 普通用户` }
}
})错误处理
done 中抛出的错误会被框架捕获并输出警告,不会中断整个渲染流程:
this.add({
$proxy: someData,
done: (value) => {
try {
return JSON.parse(value)
} catch {
return { is: 'span', style: { color: 'red' }, children: '数据格式错误' }
}
}
})update - 双向同步
update 实现视图 → 数据的反向同步(数据 → 视图方向由响应式系统自动完成)。
React 环境
默认使用 value + onChange:
import { Input } from 'antd'
const text = $ref('初始值')
this.add({ is: Input, value: { $proxy: text, update: true } })Vue 3 环境
默认使用 modelValue + update:modelValue:
import { ElInput } from 'element-plus'
const text = $ref('初始值')
this.add({ is: ElInput, modelValue: { $proxy: text, update: true } })Web Components 环境
Web Components 组件使用原生事件名进行双向同步,默认约定使用 value 属性 + input 事件:
// Web Components 组件双向同步
const value = $ref('')
this.add({
is: 'my-input',
$proxy: { value, update: true },
onInput: (e) => { value.value = e.target.value }
})说明:Web Components 适配器中,
update机制通过监听原生事件(如input、change)实现双向同步,与 React/Vue 3 的行为保持一致。
自定义同步处理
update 为函数时可完全自定义更新逻辑:
const count = $ref(0)
this.add({
is: 'input',
value: { $proxy: count },
update: (newVal, oldVal) => {
count.value = Number(newVal) || 0
}
})指定事件名
当默认机制不符合组件要求时,可指定事件名:
import { ElCheckbox } from 'element-plus'
const isChecked = $ref(false)
this.add({
is: ElCheckbox,
checked: { $proxy: isChecked, update: 'onChange' },
children: '同意条款'
})表单完整示例
import { Input, Select, Switch } from 'antd'
const { Option } = Select
const formData = $reactive({ username: '', gender: 'male', isActive: true })
this.add({
is: 'div', className: 'form',
children: [
{
is: Input, placeholder: '请输入用户名',
value: { $proxy: formData, field: 'username', update: true }
},
{
is: Select,
value: { $proxy: formData, field: 'gender', update: true },
children: [
{ is: Option, value: 'male', children: '男' },
{ is: Option, value: 'female', children: '女' }
]
},
{
is: Switch,
checked: { $proxy: formData, field: 'isActive', update: 'onChange' }
},
{
is: 'div', className: 'form-preview',
children: { $proxy: formData, done: (data) => `当前数据:${JSON.stringify(data)}` }
}
]
})Web Components 提示:在 Web Components 环境中,可将上述 antd 组件替换为对应的 Web Components 自定义元素,
$proxy+update机制同样适用。例如使用<my-input>、<my-select>等自定义元素。
$computed - 计算属性
基于响应式依赖自动计算并缓存结果,仅在依赖变化时重新计算。
基本用法
const state = $reactive({ count: 0 })
const double = $computed(() => state.count * 2)
console.log(double.value) // 0
state.count++
console.log(double.value) // 2缓存机制
let computeCount = 0
const state = $reactive({ count: 0 })
const double = $computed(() => { computeCount++; return state.count * 2 })
double.value // computeCount=1
double.value // computeCount=1(缓存命中,不重算)
state.count++
double.value // computeCount=2与 $ref 结合
const count = $ref(0)
const double = $computed(() => count.value * 2)
const sum = $computed(() => {
const a = $ref(1)
const b = $ref(2)
return a.value + b.value + count.value
})实际场景
// 数据格式化
const user = $reactive({ firstName: '张', lastName: '三' })
const fullName = $computed(() => `${user.firstName}${user.lastName}`)
// 列表过滤
const todos = $reactive([{ text: '学习', done: false }, { text: '工作', done: true }])
const completedTodos = $computed(() => todos.filter(t => t.done))
// 购物车总价
const cart = $reactive({ items: [{ price: 10, quantity: 2 }, { price: 20, quantity: 1 }] })
const totalPrice = $computed(() => cart.items.reduce((s, i) => s + i.price * i.quantity, 0))$computed vs $watchEffect
| 特性 | $computed | $watchEffect |
|---|---|---|
| 返回值 | 有返回值 | 无返回值(执行副作用) |
| 缓存 | 有缓存 | 无缓存 |
| 执行时机 | 惰性(首次访问时计算) | 立即 + 依赖变化时执行 |
| 适用场景 | 派生状态、数据转换 | DOM 操作、网络请求 |
$set
$set - 添加响应式属性
向响应式对象添加新属性并确保其响应式。$set 与直接赋值的核心差异在于:会建立父子代理链(JIESS_PARENT),使父级观察者能收到子代理变更通知。
const state = $reactive({ name: '张三' })
$set(state, 'schoolInfo', { schoolName: '清华大学', schoolAddress: '北京' })
state.schoolInfo.schoolName = '北京大学' // 视图更新
// 设置数组
const hobbies = ['篮球', '足球', '游泳']
$set(state, 'hobbies', hobbies)
state.hobbies[0] = '乒乓球' // 视图更新为什么必须用 $set 而不是直接赋值?
| 场景 | state.schoolInfo = {...} | $set(state, 'schoolInfo', {...}) |
|---|---|---|
| 嵌套属性响应 | ❌ 不响应 | ✅ 响应 |
| 父级观察者收到变更 | ❌ 不通知 | ✅ 通知 |
| 子代理自动建立父子链 | ❌ 不建立 | ✅ 建立 |
// ❌ 反例:直接赋值不创建代理
const state = $reactive({})
state.user = { name: '张三' }
state.user.name = '李四' // 不会触发响应式更新
// ✅ 正例:$set 创建响应式代理 + 父子链
$set(state, 'user', { name: '张三' })
state.user.name = '李四' // 触发响应式更新defaultValue - 默认值
当绑定数据为 undefined 时生效,确保视图有合理的初始值。
基本用法
const count = $ref()
this.add({ $proxy: count, defaultValue: 0 }) // 渲染:0
const name = $ref()
this.add({ $proxy: name, defaultValue: '未知用户' })
const user = $ref()
this.add({ $proxy: user, field: 'name', defaultValue: '访客' })与 done 结合
const score = $ref()
this.add({
$proxy: score,
defaultValue: '暂无评分',
done: (value) => {
if (value === '暂无评分') return value
return `评分:${value.toFixed(1)} / 10`
}
})数组响应式绑定
使用 $reactive 创建响应式数组,所有变异方法和索引访问都能自动触发视图更新。
创建与变异方法
const arr = $reactive([1, 2, 3])
arr.push(4) // [1, 2, 3, 4]
arr.splice(1, 2, 'a', 'b') // [1, 'a', 'b', 4]
arr[1] = 10 // 响应式更新
arr.length = 2 // 响应式更新嵌套响应式
数组元素为对象或数组时,递归进行响应式转换:
const arr = $reactive([])
arr.push({ count: 1 })
arr[0].count = 2 // 响应式更新
arr.push([1, 2, 3])
arr[1][0] = 10 // 响应式更新
// 任意深度
const nestedArr = $reactive([[1, 2, [3, 4]], [{ a: 1 }, { b: 2 }]])
nestedArr[0][2][0] = 30 // 最内层也能响应待办事项列表示例
const todoList = $reactive([
{ id: 1, text: '学习 Jiess', done: false },
{ id: 2, text: '实现响应式数组', done: true }
])
this.add({
is: 'ul', className: 'todo-list',
children: {
$proxy: todoList,
done: (list) => list.map(item => ({
is: 'li',
className: { $proxy: item, field: 'done', done: (d) => d ? 'completed' : '' },
children: [
{ is: 'input', type: 'checkbox', checked: { $proxy: item, field: 'done', update: true } },
{ $proxy: item, field: 'text', done: (t) => item.done ? `<s>${t}</s>` : t }
]
}))
}
})
function addTodo(text) { todoList.push({ id: Date.now(), text, done: false }) }
function deleteTodo(id) { const i = todoList.findIndex(t => t.id === id); if (i > -1) todoList.splice(i, 1) }非变异方法
map、filter、concat 返回新数组,需手动替换:
const arr = $reactive([1, 2, 3, 4, 5])
const filtered = arr.filter(item => item > 2)
arr.splice(0, arr.length, ...filtered) // 推荐:splice 替换原数组注意事项
$reactive会修改原始数组使其也成为响应式- 非变异方法不触发更新,需手动替换原数组
- 超大数组(>10000元素)建议配合分页或虚拟滚动
响应式绑定的高级用法
直接绑定与嵌套展平
const count = $ref(1)
this.add({ $proxy: count }) // 渲染:1
// 嵌套数组自动展平
this.add([[[{ $proxy: count }]]]) // 渲染:1
// 多个绑定拼接
const firstName = $ref('张')
const lastName = $ref('三')
this.add({ $proxy: firstName }, { $proxy: lastName }) // 渲染:张三节点 children 绑定
const message = $ref('Hello Jiess!')
this.add({ is: 'div', className: 'message', children: { $proxy: message } })children 数组中的 $proxy 子节点绑定
当 children 为数组且在子节点配置对象中直接使用 $proxy + field + done 时,可实现子节点根据响应式数据动态重建。这是 Jiess 继承体系的核心功能,常用于父子组件级联场景。
const state = $reactive({ active: true, count: 0 })
this.add({
is: 'div',
children: [
{ is: 'p', children: { $proxy: state, field: 'count', done: (v) => `计数: ${v}` } },
// ★ 子节点配置对象中直接绑定 $proxy,done 返回新节点配置
{
is: 'span',
$proxy: state,
field: 'active',
done: (active) => ({
is: 'span',
class: active ? 'stat-ok' : 'stat-err',
children: active ? '运行中' : '已停止'
})
},
]
})
// state.active = false → 子节点 <span> 自动变为 "已停止" 状态原理:框架会自动将 $proxy + field + done 组合构造为 children 的代理绑定,确保响应式数据变化时正确重建子节点。
注意事项:
- 必须同时提供
$proxy、field、done三者 done必须返回新的节点配置对象(含is属性)- 子节点配置中不能同时包含静态
children,否则会被$proxy动态绑定覆盖
复杂节点结构
const title = $ref('欢迎使用 Jiess')
const content = $ref('这是一个强大的响应式框架')
const isVisible = $ref(true)
this.add({
is: 'div', className: 'card',
children: [
{ is: 'h2', children: { $proxy: title } },
{ $if: { $proxy: isVisible }, is: 'p', children: { $proxy: content } },
{ is: 'button', children: '切换显示', onClick: () => { isVisible.value = !isVisible.value } }
]
})动态类名与样式
const isActive = $ref(true)
this.add({
is: 'div',
className: { $proxy: isActive, done: (active) => active ? 'active' : 'inactive' },
children: '动态类名'
})
const fontSize = $ref(16)
this.add({
is: 'div',
style: { fontSize: { $proxy: fontSize, done: (s) => `${s}px` }, color: 'blue' },
children: '动态字体大小'
})动态属性
const isDisabled = $ref(false)
this.add({
is: 'button',
disabled: { $proxy: isDisabled },
children: '动态禁用按钮',
onClick: () => {
isDisabled.value = true
setTimeout(() => { isDisabled.value = false }, 2000)
}
})$if 结合响应式绑定
const userRole = $ref('guest')
this.add({
is: 'div',
children: [
{ $if: { $proxy: userRole, done: (r) => r === 'admin' }, is: 'button', children: '管理面板' },
{ $if: { $proxy: userRole, done: (r) => r === 'user' }, is: 'button', children: '用户中心' },
{ $if: { $proxy: userRole, done: (r) => r === 'guest' }, is: 'button', children: '登录' }
]
})$clearReactiveCache - 清理缓存
清理响应式系统缓存,主要用于测试场景确保测试隔离:
const state = $reactive({ count: 0 })
$clearReactiveCache(state)生产环境一般无需手动调用。在测试的
afterEach中使用以确保测试隔离。
性能优化
浅响应式处理大型对象
const largeObj = $reactive(hugeData, { shallow: true })避免不必要的响应式转换
常量或静态数据无需转为响应式:
const PI = 3.14159 // 直接使用常量
// 不要:const PI = $reactive({ value: 3.14159 })利用计算属性缓存
复杂计算用 $computed 而非每次在 done 中重算:
const total = $computed(() => largeArray.reduce((s, i) => s + i.value, 0))分离响应式与非响应式数据
将不常变的数据与响应式数据分离,缩小响应式范围:
const state = $reactive({ count: 0, userInfo: { name: '张三' } })
const staticConfig = { theme: 'dark', locale: 'zh-CN' } // 非响应式批量更新
连续修改多个响应式属性时,框架会自动合并更新,避免逐个触发视图刷新:
const state = $reactive({ a: 0, b: 0, c: 0 })
// 以下三次修改会被合并为一次视图更新
state.a = 1
state.b = 2
state.c = 3最佳实践
1. 选择合适的响应式工具
| 场景 | 推荐 API |
|---|---|
| 单个可变值 | $ref |
| 多个关联状态 | $reactive |
| 派生/计算值 | $computed |
| 副作用监听 | $watchEffect |
| 精确字段监听 | $watch |
2. 保持数据流向清晰
数据→视图由响应式系统自动处理,视图→数据使用 update,避免手动同步 DOM:
// ✅ 推荐
const name = $ref('')
this.add({ is: Input, value: { $proxy: name, update: true } })
// ❌ 不推荐:手动操作 DOM
document.querySelector('#name').value = name.value3. 合理使用 $watch
$watch 签名是 $watch(proxy, 'field', callback),精确监听指定字段:
const state = $reactive({ name: '张三', age: 28 })
$watch(state, 'name', (newVal, oldVal) => {
console.log(`name 从 "${oldVal}" 变为 "${newVal}"`)
})
state.name = '李四' // 触发回调
state.age = 30 // 不触发(未监听此字段)4. 控制响应式粒度
- 频繁变化的数据使用
$ref,避免整个对象重建 - 不常变的配置数据使用
$reactive - 超大对象使用浅响应式
{ shallow: true }
5. 避免深层嵌套的 done 链
超过 3 层嵌套建议抽取为 $computed 或独立函数:
// ❌ 深层嵌套
{ $proxy: a, done: (a) => ({ $proxy: b, done: (b) => ({ $proxy: c, done: (c) => ({ $proxy: d, done: ... }) }) }) }
// ✅ 抽取计算属性
const result = $computed(() => `${a.value}-${b.value}-${c.value}-${d.value}`)
this.add({ $proxy: result })