Skip to content

Jiess 响应式绑定 ​

目录 ​

Jiess 通过 Proxy 实现响应式系统,核心流程:依赖收集 → 自动追踪 → 精确更新 → 缓存优化。

核心 API:

typescript
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 接口 ​

typescript
export interface ObserverData {
  $proxy: UnObject           // 要绑定的响应式数据(必填)
  warn?: boolean | Function   // 非响应式数据警告控制
  field?: string | number     // 绑定字段名,默认 'value'
  done?: Function             // 数据处理函数,返回转换后的值或节点配置
  defaultValue?: unknown      // 数据为 undefined 时的默认值
  update?: boolean | Function // 是否开启双向同步
}

基本用法 ​

javascript
const count = $ref(1)
this.add({ $proxy: count })            // 渲染:1
setInterval(() => { count.value++ }, 1000) // 视图自动更新

验证响应式状态 ​

javascript
console.log($isReactive($reactive({ name: 'test', value: 100 }))) // true
console.log($isReactive({}))     // false
console.log($isRef($ref(0)))     // true
console.log($isRef(0))           // false

边界情况 ​

循环引用不会导致无限递归:

javascript
const obj = { name: 'test' }
obj.self = obj
const proxy = $reactive(obj)
console.log(proxy.name)  // test
console.log(proxy.self)  // 指向自身,安全

$proxy - 指定响应式数据 ​

$proxy 是绑定对象的标识属性,指定被绑定的响应式数据。

基本用法 ​

javascript
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 创建的响应式数据

非响应式数据处理 ​

绑定普通数据也能显示,但控制台会警告:

javascript
this.add({ $proxy: 123 })                // 渲染:123,控制台警告
this.add({ $proxy: 123, warn: false })   // 消除警告

warn - 消除警告 ​

当 $proxy 绑定非响应式数据时,控制台默认会警告。warn 用于控制此行为。

消除警告 ​

javascript
this.add({ $proxy: 123, warn: false })

自定义警告处理 ​

javascript
this.add({
  $proxy: 123,
  warn: (warning) => {
    console.log('自定义警告:', warning)
    // 可对接日志系统
  }
})

浅响应式 ​

对于大型对象可使用浅响应式,仅顶层属性是响应式的:

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

javascript
const count = $ref(1)
this.add({ $proxy: count })                  // 默认取 count.value
this.add({ $proxy: count, field: 'value' })  // 等价

$reactive 场景 ​

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

数组索引绑定 ​

javascript
const fruits = $reactive(['苹果', '香蕉', '橙子'])
this.add({ $proxy: fruits, field: 0 }) // 苹果
this.add({ $proxy: fruits, field: 1 }) // 香蕉

done - 数据处理函数 ​

done 在数据渲染前对值进行处理,可返回:转换后的值、节点配置、或另一个响应式绑定对象。

格式化数据 ​

javascript
const count = $ref(1)
this.add({ $proxy: count, done: (v) => `${v}个` })
this.add({ $proxy: count, done: (v) => `当前数量:${v}` })

条件样式(返回节点配置) ​

javascript
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 可返回另一个响应式绑定对象,实现多数据联动:

javascript
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} 平方像素`
  })
})

链式深度嵌套 ​

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

条件渲染 + 多层数据联动 ​

javascript
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 中抛出的错误会被框架捕获并输出警告,不会中断整个渲染流程:

javascript
this.add({
  $proxy: someData,
  done: (value) => {
    try {
      return JSON.parse(value)
    } catch {
      return { is: 'span', style: { color: 'red' }, children: '数据格式错误' }
    }
  }
})

update - 双向同步 ​

update 实现视图 → 数据的反向同步(数据 → 视图方向由响应式系统自动完成)。

React 环境 ​

默认使用 value + onChange:

javascript
import { Input } from 'antd'
const text = $ref('初始值')
this.add({ is: Input, value: { $proxy: text, update: true } })

Vue 3 环境 ​

默认使用 modelValue + update:modelValue:

javascript
import { ElInput } from 'element-plus'
const text = $ref('初始值')
this.add({ is: ElInput, modelValue: { $proxy: text, update: true } })

Web Components 环境 ​

Web Components 组件使用原生事件名进行双向同步,默认约定使用 value 属性 + input 事件:

javascript
// 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 为函数时可完全自定义更新逻辑:

javascript
const count = $ref(0)
this.add({
  is: 'input',
  value: { $proxy: count },
  update: (newVal, oldVal) => {
    count.value = Number(newVal) || 0
  }
})

指定事件名 ​

当默认机制不符合组件要求时,可指定事件名:

javascript
import { ElCheckbox } from 'element-plus'
const isChecked = $ref(false)

this.add({
  is: ElCheckbox,
  checked: { $proxy: isChecked, update: 'onChange' },
  children: '同意条款'
})

表单完整示例 ​

javascript
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 - 计算属性 ​

基于响应式依赖自动计算并缓存结果,仅在依赖变化时重新计算。

基本用法 ​

javascript
const state = $reactive({ count: 0 })
const double = $computed(() => state.count * 2)

console.log(double.value) // 0
state.count++
console.log(double.value) // 2

缓存机制 ​

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

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

实际场景 ​

javascript
// 数据格式化
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),使父级观察者能收到子代理变更通知。

javascript
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', {...})
嵌套属性响应❌ 不响应✅ 响应
父级观察者收到变更❌ 不通知✅ 通知
子代理自动建立父子链❌ 不建立✅ 建立
javascript
// ❌ 反例:直接赋值不创建代理
const state = $reactive({})
state.user = { name: '张三' }
state.user.name = '李四'  // 不会触发响应式更新

// ✅ 正例:$set 创建响应式代理 + 父子链
$set(state, 'user', { name: '张三' })
state.user.name = '李四'  // 触发响应式更新

defaultValue - 默认值 ​

当绑定数据为 undefined 时生效,确保视图有合理的初始值。

基本用法 ​

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

javascript
const score = $ref()
this.add({
  $proxy: score,
  defaultValue: '暂无评分',
  done: (value) => {
    if (value === '暂无评分') return value
    return `评分:${value.toFixed(1)} / 10`
  }
})

数组响应式绑定 ​

使用 $reactive 创建响应式数组,所有变异方法和索引访问都能自动触发视图更新。

创建与变异方法 ​

javascript
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            // 响应式更新

嵌套响应式 ​

数组元素为对象或数组时,递归进行响应式转换:

javascript
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 // 最内层也能响应

待办事项列表示例 ​

javascript
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 返回新数组,需手动替换:

javascript
const arr = $reactive([1, 2, 3, 4, 5])
const filtered = arr.filter(item => item > 2)
arr.splice(0, arr.length, ...filtered) // 推荐:splice 替换原数组

注意事项 ​

  • $reactive 会修改原始数组使其也成为响应式
  • 非变异方法不触发更新,需手动替换原数组
  • 超大数组(>10000元素)建议配合分页或虚拟滚动

响应式绑定的高级用法 ​

直接绑定与嵌套展平 ​

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

javascript
const message = $ref('Hello Jiess!')
this.add({ is: 'div', className: 'message', children: { $proxy: message } })

children 数组中的 $proxy 子节点绑定 ​

当 children 为数组且在子节点配置对象中直接使用 $proxy + field + done 时,可实现子节点根据响应式数据动态重建。这是 Jiess 继承体系的核心功能,常用于父子组件级联场景。

javascript
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 动态绑定覆盖

复杂节点结构 ​

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

动态类名与样式 ​

javascript
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: '动态字体大小'
})

动态属性 ​

javascript
const isDisabled = $ref(false)
this.add({
  is: 'button',
  disabled: { $proxy: isDisabled },
  children: '动态禁用按钮',
  onClick: () => {
    isDisabled.value = true
    setTimeout(() => { isDisabled.value = false }, 2000)
  }
})

$if 结合响应式绑定 ​

javascript
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 - 清理缓存 ​

清理响应式系统缓存,主要用于测试场景确保测试隔离:

javascript
const state = $reactive({ count: 0 })
$clearReactiveCache(state)

生产环境一般无需手动调用。在测试的 afterEach 中使用以确保测试隔离。

性能优化 ​

浅响应式处理大型对象 ​

javascript
const largeObj = $reactive(hugeData, { shallow: true })

避免不必要的响应式转换 ​

常量或静态数据无需转为响应式:

javascript
const PI = 3.14159              // 直接使用常量
// 不要:const PI = $reactive({ value: 3.14159 })

利用计算属性缓存 ​

复杂计算用 $computed 而非每次在 done 中重算:

javascript
const total = $computed(() => largeArray.reduce((s, i) => s + i.value, 0))

分离响应式与非响应式数据 ​

将不常变的数据与响应式数据分离,缩小响应式范围:

javascript
const state = $reactive({ count: 0, userInfo: { name: '张三' } })
const staticConfig = { theme: 'dark', locale: 'zh-CN' } // 非响应式

批量更新 ​

连续修改多个响应式属性时,框架会自动合并更新,避免逐个触发视图刷新:

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

javascript
// ✅ 推荐
const name = $ref('')
this.add({ is: Input, value: { $proxy: name, update: true } })

// ❌ 不推荐:手动操作 DOM
document.querySelector('#name').value = name.value

3. 合理使用 $watch ​

$watch 签名是 $watch(proxy, 'field', callback),精确监听指定字段:

javascript
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 或独立函数:

javascript
// ❌ 深层嵌套
{ $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 })