依赖联动

Framework:

概述

依赖联动功能允许您声明字段间的依赖关系,当依赖字段的值发生变化时,自动触发联动逻辑。本章节将详细介绍依赖联动的配置和使用方法。

基础用法

依赖声明

通过 deps 属性声明字段依赖:

{
  type: 'string',
  name: 'city',
  label: '城市',
  deps: ['country'],  // 依赖 country 字段
  onDepsChange: async ({ deps, data }) => {
    // 联动逻辑
  }
}

联动回调

通过 onDepsChange 定义联动逻辑:

{
  type: 'string',
  name: 'city',
  label: '城市',
  deps: ['country'],
  onDepsChange: async ({ deps, data, schema }) => {
    const country = deps[0];
    const cities = await getCitiesByCountry(country);
    
    return {
      schema: {
        ...schema,
        props: { options: cities }
      }
    };
  }
}

依赖联动示例

联动参数详解

OnDepsChangeParams

联动回调接收以下参数:

type OnDepsChangeParams = {
  deps: Array<any>;                    // 依赖字段的当前值
  schema: FieldSchema;                 // 当前字段的配置
  name: string;                        // 当前字段名
  value: unknown;                      // 当前字段值
  data: Readonly<Record<string, unknown>>;  // 最新的表单数据
  isInitialTrigger: boolean;          // 是否为初始触发
  trigger: string;                     // 触发该回调的字段名
  triggerCount: number;               // 当前字段由 trigger 触发的次数
  totalTriggerCount: number;          // 当前字段被所有依赖字段触发的总次数
};

返回值

联动回调可以返回以下内容:

type OnDepsChangeReturn = Partial<{
  patch: Record<string, unknown>;  // 表单数据补丁
  schema: FieldSchema;             // 更新后的字段配置
}>;

链式联动与循环依赖防护

链式联动

当字段 A 的联动回调修改了字段 B 的值,而字段 B 又是字段 C 的依赖时,会自动触发链式联动。Schema FormX 内置最大 20 次联动触发限制,超过限制时自动终止联动链并在控制台输出 over maxTimes 错误。

循环依赖防护

如果字段之间形成循环依赖(如 A 依赖 B,B 依赖 A),会导致联动链无限递归。Schema FormX 通过以下机制防护:

  1. 次数限制:联动链最多触发 20 次,超过自动终止
  2. 监控参数:使用 triggerCounttotalTriggerCount 监控联动深度
// 监控联动深度
onDepsChange: ({ totalTriggerCount }) => {
  if (totalTriggerCount > 10) {
    console.warn('联动触发次数过多,可能存在循环依赖');
    return {};
  }
  // 正常联动逻辑
}

建议:确保依赖关系是单向的,避免 A→B→A 的循环依赖模式。

常见联动模式

异常处理

重要:与 validator 不同,onDepsChange 回调中的异常不会被自动捕获。如果回调中发生错误(如异步请求失败),异常会直接向上抛出。建议在 onDepsChange 中使用 try-catch 处理异常:

onDepsChange: async ({ deps }) => {
  try {
    const data = await fetchData(deps[0]);
    return { schema: { ...schema, props: { options: data } } };
  } catch (error) {
    console.error('联动数据加载失败:', error);
    return {};  // 返回空对象,保持当前状态不变
  }
}

级联选择

实现省市区级联选择:

// 省份字段
{
  type: 'string',
  name: 'province',
  label: '省份',
  props: { options: provinces }
}

// 城市字段(依赖省份)
{
  type: 'string',
  name: 'city',
  label: '城市',
  deps: ['province'],
  onDepsChange: async ({ deps, schema }) => {
    const province = deps[0];
    const cities = await getCitiesByProvince(province);
    
    return {
      patch: { city: '' },  // 清空城市
      schema: {
        ...schema,
        props: { options: cities }
      }
    };
  }
}

// 区县字段(依赖城市)
{
  type: 'string',
  name: 'district',
  label: '区县',
  deps: ['city'],
  onDepsChange: async ({ deps, schema }) => {
    const city = deps[0];
    const districts = await getDistrictsByCity(city);
    
    return {
      schema: {
        ...schema,
        props: { options: districts }
      }
    };
  }
}

条件显示

根据其他字段的值控制字段的显示:

{
  type: 'string',
  name: 'company',
  label: '公司名称',
  deps: ['userType'],
  onDepsChange: ({ deps, schema }) => {
    const userType = deps[0];
    return {
      schema: {
        ...schema,
        hidden: userType !== '企业用户',
        required: userType === '企业用户'
      }
    };
  }
}

动态验证

根据其他字段的值动态调整验证规则:

{
  type: 'string',
  name: 'endDate',
  label: '结束日期',
  deps: ['startDate'],
  onDepsChange: ({ deps, schema }) => {
    const startDate = deps[0];
    return {
      schema: {
        ...schema,
        validator: ({ value, label }) => {
          if (startDate && value && value < startDate) {
            return `${label}不能早于开始日期`;
          }
          return undefined;
        }
      }
    };
  }
}

异步数据加载

从服务器加载数据:

{
  type: 'string',
  name: 'product',
  label: '产品',
  deps: ['category'],
  onDepsChange: async ({ deps, schema }) => {
    const category = deps[0];
    
    try {
      const products = await fetchProducts(category);
      return {
        schema: {
          ...schema,
          props: { options: products }
        }
      };
    } catch (error) {
      console.error('加载产品失败:', error);
      return {};
    }
  }
}

最佳实践:推荐将异步获取数据的逻辑封装到自定义组件内部(如组件的 useEffect 中),在 onDepsChange 中仅通过 schema.props 传递数据标识(如 categoryId),由组件自行加载数据。这样可以更好地控制加载状态、错误处理和缓存逻辑。

// ✅ 推荐:组件内部处理异步逻辑
const ProductSelect = ({ value, onChange, categoryId }) => {
  const [options, setOptions] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!categoryId) return;
    setLoading(true);
    fetchProducts(categoryId)
      .then(setOptions)
      .catch(console.error)
      .finally(() => setLoading(false));
  }, [categoryId]);

  return <Select value={value} onChange={onChange} options={options} loading={loading} />;
};

// Schema 中仅传递标识
{
  type: 'string',
  name: 'product',
  component: 'ProductSelect',
  deps: ['category'],
  onDepsChange: ({ deps, schema }) => ({
    schema: { ...schema, props: { ...schema.props, categoryId: deps[0] } }
  })
}

多依赖

一个字段可以依赖多个字段:

{
  type: 'string',
  name: 'result',
  label: '结果',
  deps: ['fieldA', 'fieldB'],
  onDepsChange: ({ deps, data }) => {
    const [fieldA, fieldB] = deps;
    // 根据多个依赖字段联动
  }
}

联动触发时机

初始触发

当表单初始化时,如果依赖字段有值,会触发一次联动:

onDepsChange: ({ isInitialTrigger, data }) => {
  if (isInitialTrigger) {
    // 初始触发的处理
  }
}

值变化触发

当依赖字段的值发生变化时触发:

onDepsChange: ({ trigger, triggerCount, data }) => {
  console.log(`字段 ${trigger}${triggerCount} 次触发联动`);
}

异步联动

异步回调

联动回调支持异步操作:

onDepsChange: async ({ deps, schema }) => {
  // 异步操作
  const result = await fetchData(deps[0]);
  
  return {
    schema: {
      ...schema,
      props: { options: result }
    }
  };
}

错误处理

异步联动需要处理错误:

onDepsChange: async ({ deps, schema }) => {
  try {
    const result = await fetchData(deps[0]);
    return {
      schema: {
        ...schema,
        props: { options: result }
      }
    };
  } catch (error) {
    console.error('联动失败:', error);
    return {};  // 返回空对象,不更新配置
  }
}

性能优化

防抖处理

对于频繁触发的联动,可以使用防抖:

import { debounce } from 'lodash-es';

const debouncedFetch = debounce(async (value) => {
  return await fetchData(value);
}, 300);

{
  type: 'string',
  name: 'search',
  label: '搜索',
  deps: ['keyword'],
  onDepsChange: async ({ deps, schema }) => {
    const keyword = deps[0];
    const results = await debouncedFetch(keyword);
    
    return {
      schema: {
        ...schema,
        props: { options: results }
      }
    };
  }
}

缓存结果

对于相同参数的请求,可以缓存结果:

const cache = new Map();

{
  type: 'string',
  name: 'product',
  label: '产品',
  deps: ['category'],
  onDepsChange: async ({ deps, schema }) => {
    const category = deps[0];
    
    if (cache.has(category)) {
      return {
        schema: {
          ...schema,
          props: { options: cache.get(category) }
        }
      };
    }
    
    const products = await fetchProducts(category);
    cache.set(category, products);
    
    return {
      schema: {
        ...schema,
        props: { options: products }
      }
    };
  }
}

循环依赖防护

自动防护机制

Schema FormX 内置了循环依赖防护机制:

  1. 触发次数限制:单次联动最多触发 20 次
  2. 递归检测:自动检测并阻止无限递归
  3. 错误提示:超过限制时会在控制台输出错误信息
// 源码中的防护逻辑
const maxTimes = 20;
let times = 0;

// 超过限制时的处理
if (times > maxTimes) {
  console.error('over maxTimes');
  break;
}

常见循环依赖场景

错误示例

// ❌ 错误:A 依赖 B,B 依赖 A
const schema = [
  {
    name: 'fieldA',
    deps: ['fieldB'],
    onDepsChange: ({ deps }) => {
      return { patch: { fieldA: deps[0] } };
    }
  },
  {
    name: 'fieldB',
    deps: ['fieldA'],
    onDepsChange: ({ deps }) => {
      return { patch: { fieldB: deps[0] } };
    }
  }
];

正确示例

// ✅ 正确:单向依赖
const schema = [
  {
    name: 'province',
    label: '省份'
  },
  {
    name: 'city',
    label: '城市',
    deps: ['province'],
    onDepsChange: async ({ deps, schema }) => {
      const cities = await getCities(deps[0]);
      return { schema: { ...schema, props: { options: cities } } };
    }
  }
];

triggerCount 参数

triggerCount 参数可以帮助识别循环依赖:

{
  name: 'field',
  deps: ['dep1', 'dep2'],
  onDepsChange: ({ trigger, triggerCount, totalTriggerCount }) => {
    console.log(`触发字段: ${trigger}`);
    console.log(`该字段触发次数: ${triggerCount}`);
    console.log(`总触发次数: ${totalTriggerCount}`);
    
    // 如果触发次数过多,可能是循环依赖
    if (totalTriggerCount > 10) {
      console.warn('触发次数过多,可能存在循环依赖');
      return {};
    }
    
    // 正常联动逻辑
  }
}

链式联动

自动级联触发

patch 中的字段也是其他字段的依赖时,会自动触发链式联动:

// 省份 → 城市 → 区县
const schema = [
  { name: 'province', label: '省份' },
  {
    name: 'city',
    label: '城市',
    deps: ['province'],
    onDepsChange: async ({ deps, schema }) => {
      const cities = await getCities(deps[0]);
      return { 
        patch: { city: '' },  // 清空城市
        schema: { ...schema, props: { options: cities } } 
      };
    }
  },
  {
    name: 'district',
    label: '区县',
    deps: ['city'],
    onDepsChange: async ({ deps, schema }) => {
      const districts = await getDistricts(deps[0]);
      return { 
        schema: { ...schema, props: { options: districts } } 
      };
    }
  }
];

// 联动流程:
// 1. 用户选择省份
// 2. 触发 city 的 onDepsChange,返回 patch 和新 schema
// 3. patch 中的 city 变化,触发 district 的 onDepsChange
// 4. 直到所有联动完成

链式联动限制

  • 最多触发 20 次联动
  • 超过限制会停止并输出错误
  • 建议控制联动层级不超过 3-4 层

最佳实践

依赖管理

  1. 保持依赖链简洁:避免过深的级联依赖
  2. 确保依赖单向:依赖关系应为单向,避免循环依赖
  3. 使用异步操作处理服务端数据:通过 onDepsChange 加载远程数据

性能优化

  1. 避免在 onDepsChange 中执行耗时操作:保持回调轻量
  2. 使用防抖处理频繁触发:对高频联动使用防抖或节流
  3. 缓存结果:对相同请求参数缓存结果,避免重复请求

错误处理与用户体验

  1. 使用 try-catch 处理异常:为异步联动添加错误处理,提供兜底数据
  2. 提供加载状态:在异步加载时显示 loading 或错误提示,控制联动层级不超过 3-4 层