Skip to content

自定义元素属性传递

学习目标

通过本章学习,你将掌握:

  • React 19属性传递机制的改进
  • 字符串、数字、布尔值属性传递
  • 对象和数组属性传递
  • 函数属性传递和回调
  • 属性类型检测和转换
  • 性能优化策略
  • TypeScript类型定义
  • 最佳实践和常见陷阱

第一部分:属性传递基础

1.1 React 18的属性传递限制

在React 19之前,向Custom Elements传递属性存在严重限制。

React 18的默认行为:

jsx
// React 18
function App() {
  const data = { name: 'John', age: 30 };
  const items = [1, 2, 3];
  const handler = () => console.log('clicked');
  
  return (
    <my-element 
      stringValue="hello"      // ✅ 字符串:正常
      numberValue={42}         // ❌ 转为字符串 "42"
      boolValue={true}         // ❌ 转为字符串 "true"
      objectValue={data}       // ❌ 转为字符串 "[object Object]"
      arrayValue={items}       // ❌ 转为字符串 "1,2,3"
      funcValue={handler}      // ❌ 转为字符串 "() => console.log('clicked')"
    ></my-element>
  );
}

// 实际DOM输出:
// <my-element 
//   stringvalue="hello"
//   numbervalue="42"
//   boolvalue="true"
//   objectvalue="[object Object]"
//   arrayvalue="1,2,3"
//   funcvalue="() => console.log('clicked')"
// ></my-element>

React 18的解决方法(使用ref):

jsx
// React 18:必须手动设置属性
function React18Workaround() {
  const ref = useRef(null);
  const data = { name: 'John', age: 30 };
  const items = [1, 2, 3];
  const handler = () => console.log('clicked');
  
  useEffect(() => {
    if (ref.current) {
      // 手动设置每个非字符串属性
      ref.current.objectValue = data;
      ref.current.arrayValue = items;
      ref.current.funcValue = handler;
    }
  }, [data, items, handler]);
  
  return (
    <my-element 
      ref={ref}
      stringValue="hello"
    ></my-element>
  );
}

问题分析:

1. 开发体验差
- 需要使用ref和useEffect
- 代码冗长复杂
- 容易出错

2. 性能问题
- 额外的useEffect执行
- 可能的时序问题
- 更多的重新渲染

3. 类型安全问题
- TypeScript难以正确推断
- 需要类型断言
- 容易产生运行时错误

4. 维护困难
- 属性和ref设置分离
- 逻辑分散
- 难以理解

1.2 React 19的属性传递改进

React 19彻底解决了这个问题,支持直接传递任意JavaScript值。

React 19的行为:

jsx
// React 19
function App() {
  const data = { name: 'John', age: 30 };
  const items = [1, 2, 3];
  const handler = () => console.log('clicked');
  
  return (
    <my-element 
      stringValue="hello"      // ✅ 字符串
      numberValue={42}         // ✅ 数字
      boolValue={true}         // ✅ 布尔值
      objectValue={data}       // ✅ 对象
      arrayValue={items}       // ✅ 数组
      funcValue={handler}      // ✅ 函数
    ></my-element>
  );
}

// 无需ref和useEffect!

工作原理:

javascript
// React 19内部逻辑(简化版)
function setPropertyOnElement(element, name, value) {
  // 1. 检查元素是否在构造函数中定义了该属性
  if (name in element) {
    // 2. 作为JavaScript属性设置(保持原始类型)
    element[name] = value;
  } else {
    // 3. 作为HTML属性设置(转为字符串)
    if (value == null || value === false) {
      element.removeAttribute(name);
    } else if (value === true) {
      element.setAttribute(name, '');
    } else {
      element.setAttribute(name, String(value));
    }
  }
}

关键要求:

javascript
// Custom Element必须在构造函数中定义属性
class MyElement extends HTMLElement {
  constructor() {
    super();
    
    // 关键:在这里定义所有属性
    this.objectValue = undefined;  // ✅ React会检测到
    this.arrayValue = undefined;   // ✅ React会检测到
    this.funcValue = undefined;    // ✅ React会检测到
  }
  
  connectedCallback() {
    // ❌ 在这里定义太晚了,React检测不到
    // this.lateProperty = undefined;
  }
}

customElements.define('my-element', MyElement);

1.3 属性类型识别机制

React 19如何决定使用属性还是特性(attribute)?

决策流程:

接收到属性传递
    |

检查元素原型链是否包含该属性名
    |
    ├─→ 是 → 设置为JavaScript属性(element.prop = value)
    |         保持原始类型(对象、数组、函数等)
    |
    └─→ 否 → 设置为HTML属性(element.setAttribute(name, String(value)))
              转换为字符串

实际示例:

javascript
class TypeAwareElement extends HTMLElement {
  constructor() {
    super();
    
    // 这些会被识别为属性(properties)
    this.complexData = undefined;
    this.callback = undefined;
    this.items = undefined;
  }
  
  connectedCallback() {
    console.log('Received:');
    console.log('- complexData type:', typeof this.complexData);
    console.log('- callback type:', typeof this.callback);
    console.log('- items type:', Array.isArray(this.items) ? 'array' : typeof this.items);
    
    // 这些会被识别为特性(attributes)
    console.log('- name attribute:', this.getAttribute('name'));
    console.log('- title attribute:', this.getAttribute('title'));
  }
}

customElements.define('type-aware-element', TypeAwareElement);

React使用:

jsx
function App() {
  return (
    <type-aware-element
      // 作为属性传递(保持原始类型)
      complexData={{ user: { id: 1 }, settings: {} }}
      callback={() => alert('Hello')}
      items={[1, 2, 3, 4, 5]}
      
      // 作为特性传递(转为字符串)
      name="John Doe"
      title="Welcome"
    ></type-aware-element>
  );
}

// 输出:
// Received:
// - complexData type: object
// - callback type: function
// - items type: array
// - name attribute: John Doe
// - title attribute: Welcome

第二部分:基本类型传递

2.1 字符串属性

字符串属性是最基本的传递方式。

静态字符串:

jsx
function App() {
  return (
    <my-element 
      name="John Doe"
      title="Welcome to React 19"
      description="This is a description"
    ></my-element>
  );
}

动态字符串:

jsx
function App() {
  const [username, setUsername] = useState('');
  const [message, setMessage] = useState('');
  
  return (
    <div>
      <input 
        value={username}
        onChange={e => setUsername(e.target.value)}
        placeholder="Enter username"
      />
      
      <my-element 
        username={username}
        message={message || 'No message'}
      ></my-element>
    </div>
  );
}

Custom Element接收字符串:

javascript
class StringElement extends HTMLElement {
  static get observedAttributes() {
    return ['username', 'message'];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name} changed: "${oldValue}" -> "${newValue}"`);
    this.render();
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    const username = this.getAttribute('username') || 'Anonymous';
    const message = this.getAttribute('message') || '';
    
    this.innerHTML = `
      <div class="user-info">
        <h2>${username}</h2>
        <p>${message}</p>
      </div>
    `;
  }
}

customElements.define('string-element', StringElement);

模板字符串和特殊字符:

jsx
function App() {
  const userName = "John O'Brien";
  const description = `Multi-line
  description with
  special characters: <>&"'`;
  
  return (
    <my-element 
      name={userName}
      description={description}
    ></my-element>
  );
}

// React自动转义,安全传递

2.2 数字属性

数字可以作为属性或特性传递。

作为属性(推荐):

jsx
function App() {
  const [count, setCount] = useState(0);
  const [price, setPrice] = useState(99.99);
  
  return (
    <number-element 
      count={count}
      price={price}
      max={100}
    ></number-element>
  );
}

Custom Element定义:

javascript
class NumberElement extends HTMLElement {
  constructor() {
    super();
    // 在构造函数中定义,接收为数字类型
    this.count = 0;
    this.price = 0;
    this.max = 100;
  }
  
  set count(value) {
    this._count = Number(value); // 确保是数字
    this.render();
  }
  
  get count() {
    return this._count;
  }
  
  set price(value) {
    this._price = Number(value);
    this.render();
  }
  
  get price() {
    return this._price;
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    this.innerHTML = `
      <div>
        <p>Count: ${this._count} / ${this.max}</p>
        <p>Price: $${this._price.toFixed(2)}</p>
        <progress value="${this._count}" max="${this.max}"></progress>
      </div>
    `;
  }
}

customElements.define('number-element', NumberElement);

数字运算和验证:

jsx
function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);
  const [max, setMax] = useState(10);
  
  const increment = () => {
    setCount(prev => Math.min(prev + step, max));
  };
  
  const decrement = () => {
    setCount(prev => Math.max(prev - step, 0));
  };
  
  return (
    <div>
      <label>
        Step:
        <input 
          type="number" 
          value={step}
          onChange={e => setStep(Number(e.target.value))}
          min="1"
        />
      </label>
      
      <counter-display
        value={count}
        max={max}
        step={step}
      ></counter-display>
      
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

2.3 布尔属性

布尔值的传递有特殊规则。

作为属性:

jsx
function App() {
  const [disabled, setDisabled] = useState(false);
  const [loading, setLoading] = useState(false);
  const [checked, setChecked] = useState(true);
  
  return (
    <boolean-element 
      disabled={disabled}
      loading={loading}
      checked={checked}
    ></boolean-element>
  );
}

Custom Element处理布尔值:

javascript
class BooleanElement extends HTMLElement {
  constructor() {
    super();
    // 定义为属性
    this.disabled = false;
    this.loading = false;
    this.checked = false;
  }
  
  set disabled(value) {
    this._disabled = Boolean(value);
    this.updateDisabledState();
  }
  
  get disabled() {
    return this._disabled;
  }
  
  connectedCallback() {
    this.innerHTML = `
      <button id="btn">
        <span class="text">Click Me</span>
        <span class="loader" style="display: none;">Loading...</span>
      </button>
    `;
    this.button = this.querySelector('#btn');
    this.updateDisabledState();
  }
  
  updateDisabledState() {
    if (this.button) {
      this.button.disabled = this._disabled || this._loading;
    }
  }
  
  set loading(value) {
    this._loading = Boolean(value);
    this.updateLoadingState();
  }
  
  updateLoadingState() {
    const text = this.querySelector('.text');
    const loader = this.querySelector('.loader');
    
    if (text && loader) {
      text.style.display = this._loading ? 'none' : 'inline';
      loader.style.display = this._loading ? 'inline' : 'none';
    }
    
    this.updateDisabledState();
  }
}

customElements.define('boolean-element', BooleanElement);

HTML特性式布尔值:

jsx
// 也可以作为HTML特性传递
function App() {
  const [isDisabled, setIsDisabled] = useState(false);
  
  return (
    <my-button 
      disabled={isDisabled}  // 作为属性
      aria-disabled={isDisabled ? 'true' : 'false'} // 作为特性
    ></my-button>
  );
}

处理特性式布尔值:

javascript
class MyButton extends HTMLElement {
  static get observedAttributes() {
    return ['disabled', 'aria-disabled'];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'disabled') {
      // newValue为null(属性被移除)或空字符串(属性存在)
      const isDisabled = newValue !== null;
      this.updateDisabled(isDisabled);
    }
  }
}

第三部分:复杂类型传递

3.1 对象属性

React 19可以直接传递对象,保持引用和类型。

基本对象传递:

jsx
interface UserData {
  id: number;
  name: string;
  email: string;
  avatar?: string;
}

function App() {
  const user: UserData = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    avatar: '/avatars/john.jpg'
  };
  
  return (
    <user-card user={user}></user-card>
  );
}

Custom Element接收对象:

javascript
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.user = null;
  }
  
  set user(value) {
    this._user = value;
    this.render();
  }
  
  get user() {
    return this._user;
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    if (!this._user) {
      this.shadowRoot.innerHTML = '<div>No user data</div>';
      return;
    }
    
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
          display: flex;
          align-items: center;
          gap: 12px;
        }
        .avatar {
          width: 50px;
          height: 50px;
          border-radius: 50%;
        }
        .info h3 {
          margin: 0 0 4px 0;
        }
        .info p {
          margin: 0;
          color: #666;
          font-size: 14px;
        }
      </style>
      
      <div class="card">
        ${this._user.avatar ? `<img class="avatar" src="${this._user.avatar}" alt="${this._user.name}">` : ''}
        <div class="info">
          <h3>${this._user.name}</h3>
          <p>${this._user.email}</p>
          <p>ID: ${this._user.id}</p>
        </div>
      </div>
    `;
  }
}

customElements.define('user-card', UserCard);

嵌套对象:

jsx
interface ComplexData {
  user: {
    profile: {
      name: string;
      bio: string;
    };
    settings: {
      theme: string;
      notifications: boolean;
    };
  };
  metadata: {
    createdAt: Date;
    updatedAt: Date;
  };
}

function App() {
  const complexData: ComplexData = {
    user: {
      profile: {
        name: 'John',
        bio: 'Developer'
      },
      settings: {
        theme: 'dark',
        notifications: true
      }
    },
    metadata: {
      createdAt: new Date('2024-01-01'),
      updatedAt: new Date()
    }
  };
  
  return (
    <complex-element data={complexData}></complex-element>
  );
}

对象更新策略:

jsx
function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    email: 'john@example.com',
    preferences: {
      theme: 'light',
      language: 'en'
    }
  });
  
  // 方式1:整体更新(触发重新渲染)
  const updateName = (newName: string) => {
    setUser(prev => ({
      ...prev,
      name: newName
    }));
  };
  
  // 方式2:深度更新嵌套属性
  const updateTheme = (newTheme: string) => {
    setUser(prev => ({
      ...prev,
      preferences: {
        ...prev.preferences,
        theme: newTheme
      }
    }));
  };
  
  return (
    <user-profile-element user={user}></user-profile-element>
  );
}

对象比较和优化:

javascript
class OptimizedElement extends HTMLElement {
  constructor() {
    super();
    this._data = null;
    this._dataJSON = '';
  }
  
  set data(value) {
    // 深度比较避免不必要的更新
    const newJSON = JSON.stringify(value);
    
    if (newJSON !== this._dataJSON) {
      this._data = value;
      this._dataJSON = newJSON;
      this.render();
    } else {
      console.log('Data unchanged, skipping render');
    }
  }
  
  get data() {
    return this._data;
  }
}

3.2 数组属性

数组传递和操作。

基本数组:

jsx
function App() {
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1', done: false },
    { id: 2, text: 'Item 2', done: true },
    { id: 3, text: 'Item 3', done: false }
  ]);
  
  const addItem = () => {
    const newItem = {
      id: Date.now(),
      text: `Item ${items.length + 1}`,
      done: false
    };
    setItems(prev => [...prev, newItem]);
  };
  
  return (
    <div>
      <todo-list items={items}></todo-list>
      <button onClick={addItem}>Add Item</button>
    </div>
  );
}

Custom Element处理数组:

javascript
class TodoList extends HTMLElement {
  constructor() {
    super();
    this.items = [];
  }
  
  set items(value) {
    if (!Array.isArray(value)) {
      console.error('items must be an array');
      return;
    }
    
    this._items = value;
    this.render();
  }
  
  get items() {
    return this._items;
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    if (!this._items || this._items.length === 0) {
      this.innerHTML = '<p>No items</p>';
      return;
    }
    
    this.innerHTML = `
      <ul>
        ${this._items.map(item => `
          <li data-id="${item.id}" class="${item.done ? 'done' : ''}">
            <input type="checkbox" ${item.done ? 'checked' : ''} />
            <span>${item.text}</span>
          </li>
        `).join('')}
      </ul>
    `;
    
    // 添加事件监听
    this.querySelectorAll('input[type="checkbox"]').forEach((checkbox, index) => {
      checkbox.addEventListener('change', (e) => {
        this.dispatchEvent(new CustomEvent('itemToggle', {
          detail: { 
            id: this._items[index].id,
            done: e.target.checked 
          },
          bubbles: true,
          composed: true
        }));
      });
    });
  }
}

customElements.define('todo-list', TodoList);

数组操作:

jsx
function TodoApp() {
  const [todos, setTodos] = useState([]);
  
  const handleItemToggle = (event: CustomEvent<{ id: number; done: boolean }>) => {
    const { id, done } = event.detail;
    setTodos(prev => prev.map(item => 
      item.id === id ? { ...item, done } : item
    ));
  };
  
  const handleItemDelete = (event: CustomEvent<{ id: number }>) => {
    setTodos(prev => prev.filter(item => item.id !== event.detail.id));
  };
  
  return (
    <todo-list
      items={todos}
      onitemToggle={handleItemToggle}
      onitemDelete={handleItemDelete}
    ></todo-list>
  );
}

大数组性能优化:

jsx
function BigListExample() {
  const [items, setItems] = useState(() => 
    Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }))
  );
  
  // 使用useMemo避免每次渲染都传递新数组引用
  const memoizedItems = useMemo(() => items, [items]);
  
  // 或者只传递可见项
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 });
  const visibleItems = useMemo(() => 
    items.slice(visibleRange.start, visibleRange.end),
    [items, visibleRange]
  );
  
  return (
    <virtualized-list
      items={visibleItems}
      totalCount={items.length}
      onrangeChange={(e) => setVisibleRange(e.detail)}
    ></virtualized-list>
  );
}

3.3 Date和特殊对象

传递Date、Map、Set等特殊对象。

Date对象:

jsx
function App() {
  const [startDate, setStartDate] = useState(new Date('2024-01-01'));
  const [endDate, setEndDate] = useState(new Date());
  
  return (
    <date-range-picker
      startDate={startDate}
      endDate={endDate}
      onstartDateChange={(e) => setStartDate(e.detail.date)}
      onendDateChange={(e) => setEndDate(e.detail.date)}
    ></date-range-picker>
  );
}

Custom Element处理Date:

javascript
class DateRangePicker extends HTMLElement {
  constructor() {
    super();
    this.startDate = new Date();
    this.endDate = new Date();
  }
  
  set startDate(value) {
    this._startDate = value instanceof Date ? value : new Date(value);
    this.render();
  }
  
  get startDate() {
    return this._startDate;
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    this.innerHTML = `
      <div class="date-range">
        <input 
          type="date" 
          id="start"
          value="${this._startDate.toISOString().split('T')[0]}"
        />
        <span>to</span>
        <input 
          type="date" 
          id="end"
          value="${this._endDate.toISOString().split('T')[0]}"
        />
      </div>
    `;
    
    this.querySelector('#start').addEventListener('change', (e) => {
      this.dispatchEvent(new CustomEvent('startDateChange', {
        detail: { date: new Date(e.target.value) },
        bubbles: true,
        composed: true
      }));
    });
  }
}

customElements.define('date-range-picker', DateRangePicker);

Map和Set对象:

jsx
function App() {
  const [selectedIds, setSelectedIds] = useState(new Set([1, 2, 3]));
  const [userMap, setUserMap] = useState(new Map([
    [1, { name: 'John', role: 'admin' }],
    [2, { name: 'Jane', role: 'user' }]
  ]));
  
  return (
    <selection-manager
      selectedIds={selectedIds}
      userMap={userMap}
      onselectionChange={(e) => setSelectedIds(e.detail.selection)}
    ></selection-manager>
  );
}

Custom Element处理特殊对象:

javascript
class SelectionManager extends HTMLElement {
  constructor() {
    super();
    this.selectedIds = new Set();
    this.userMap = new Map();
  }
  
  set selectedIds(value) {
    this._selectedIds = value instanceof Set ? value : new Set(value);
    this.render();
  }
  
  set userMap(value) {
    this._userMap = value instanceof Map ? value : new Map(Object.entries(value));
    this.render();
  }
  
  render() {
    const selectedUsers = Array.from(this._selectedIds)
      .map(id => this._userMap.get(id))
      .filter(Boolean);
    
    this.innerHTML = `
      <div>
        <h3>Selected Users (${selectedUsers.length})</h3>
        <ul>
          ${selectedUsers.map(user => `
            <li>${user.name} (${user.role})</li>
          `).join('')}
        </ul>
      </div>
    `;
  }
}

customElements.define('selection-manager', SelectionManager);

第四部分:函数属性传递

4.1 回调函数

函数作为属性传递,实现回调机制。

基本回调:

jsx
function App() {
  const [result, setResult] = useState(null);
  
  const handleClick = (data: any) => {
    console.log('Element clicked:', data);
    setResult(data);
  };
  
  const handleChange = (value: string) => {
    console.log('Value changed:', value);
  };
  
  const handleSubmit = async (formData: any) => {
    console.log('Submitting:', formData);
    await api.submit(formData);
  };
  
  return (
    <interactive-element
      onClick={handleClick}
      onChange={handleChange}
      onSubmit={handleSubmit}
    ></interactive-element>
  );
}

Custom Element使用回调:

javascript
class InteractiveElement extends HTMLElement {
  constructor() {
    super();
    this.onClick = null;
    this.onChange = null;
    this.onSubmit = null;
  }
  
  connectedCallback() {
    this.innerHTML = `
      <div>
        <button id="btn">Click</button>
        <input id="input" type="text" />
        <button id="submit">Submit</button>
      </div>
    `;
    
    // 使用回调
    this.querySelector('#btn').addEventListener('click', () => {
      if (this.onClick) {
        this.onClick({ timestamp: Date.now(), source: 'button' });
      }
    });
    
    this.querySelector('#input').addEventListener('input', (e) => {
      if (this.onChange) {
        this.onChange(e.target.value);
      }
    });
    
    this.querySelector('#submit').addEventListener('click', () => {
      if (this.onSubmit) {
        const formData = {
          value: this.querySelector('#input').value,
          timestamp: Date.now()
        };
        this.onSubmit(formData);
      }
    });
  }
}

customElements.define('interactive-element', InteractiveElement);

4.2 异步回调

处理返回Promise的回调:

jsx
function App() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const handleSave = async (data: any): Promise<{ success: boolean; message: string }> => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.save(data);
      return { success: true, message: 'Saved successfully' };
    } catch (err) {
      setError(err.message);
      return { success: false, message: err.message };
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form-element
      onSave={handleSave}
      loading={loading}
      error={error}
    ></form-element>
  );
}

Custom Element处理异步:

javascript
class FormElement extends HTMLElement {
  constructor() {
    super();
    this.onSave = null;
    this.loading = false;
    this.error = null;
  }
  
  async handleSubmit() {
    if (!this.onSave) return;
    
    const formData = this.collectFormData();
    
    try {
      // 调用异步回调
      const result = await this.onSave(formData);
      
      if (result.success) {
        this.showSuccess(result.message);
      } else {
        this.showError(result.message);
      }
    } catch (error) {
      this.showError(error.message);
    }
  }
  
  collectFormData() {
    const inputs = this.querySelectorAll('input');
    const data = {};
    inputs.forEach(input => {
      data[input.name] = input.value;
    });
    return data;
  }
  
  showSuccess(message) {
    const alert = this.querySelector('.alert');
    alert.className = 'alert success';
    alert.textContent = message;
  }
  
  showError(message) {
    const alert = this.querySelector('.alert');
    alert.className = 'alert error';
    alert.textContent = message;
  }
}

customElements.define('form-element', FormElement);

4.3 验证器函数

传递验证函数:

jsx
interface ValidationResult {
  valid: boolean;
  errors: string[];
}

function App() {
  const validators = {
    email: (value: string): ValidationResult => {
      const errors: string[] = [];
      
      if (!value) {
        errors.push('Email is required');
      } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        errors.push('Invalid email format');
      }
      
      return {
        valid: errors.length === 0,
        errors
      };
    },
    
    password: (value: string): ValidationResult => {
      const errors: string[] = [];
      
      if (!value) {
        errors.push('Password is required');
      } else {
        if (value.length < 8) {
          errors.push('Password must be at least 8 characters');
        }
        if (!/[A-Z]/.test(value)) {
          errors.push('Password must contain uppercase letter');
        }
        if (!/[0-9]/.test(value)) {
          errors.push('Password must contain number');
        }
      }
      
      return {
        valid: errors.length === 0,
        errors
      };
    }
  };
  
  return (
    <validation-form validators={validators}></validation-form>
  );
}

Custom Element使用验证器:

javascript
class ValidationForm extends HTMLElement {
  constructor() {
    super();
    this.validators = {};
  }
  
  set validators(value) {
    this._validators = value || {};
  }
  
  connectedCallback() {
    this.innerHTML = `
      <form>
        <div class="field">
          <label>Email:</label>
          <input type="email" name="email" />
          <div class="errors" data-field="email"></div>
        </div>
        
        <div class="field">
          <label>Password:</label>
          <input type="password" name="password" />
          <div class="errors" data-field="password"></div>
        </div>
        
        <button type="submit">Submit</button>
      </form>
    `;
    
    // 实时验证
    this.querySelectorAll('input').forEach(input => {
      input.addEventListener('blur', () => {
        this.validateField(input.name, input.value);
      });
    });
    
    // 表单提交验证
    this.querySelector('form').addEventListener('submit', (e) => {
      e.preventDefault();
      this.validateAll();
    });
  }
  
  validateField(fieldName, value) {
    const validator = this._validators[fieldName];
    if (!validator) return;
    
    const result = validator(value);
    const errorsEl = this.querySelector(`[data-field="${fieldName}"]`);
    
    if (result.valid) {
      errorsEl.innerHTML = '';
      errorsEl.classList.remove('show');
    } else {
      errorsEl.innerHTML = result.errors.map(err => 
        `<span class="error">${err}</span>`
      ).join('');
      errorsEl.classList.add('show');
    }
  }
  
  validateAll() {
    const inputs = this.querySelectorAll('input');
    let allValid = true;
    
    inputs.forEach(input => {
      this.validateField(input.name, input.value);
      const errorsEl = this.querySelector(`[data-field="${input.name}"]`);
      if (errorsEl.classList.contains('show')) {
        allValid = false;
      }
    });
    
    if (allValid) {
      this.dispatchEvent(new CustomEvent('formValid', {
        detail: this.getFormData(),
        bubbles: true,
        composed: true
      }));
    }
  }
  
  getFormData() {
    const inputs = this.querySelectorAll('input');
    const data = {};
    inputs.forEach(input => {
      data[input.name] = input.value;
    });
    return data;
  }
}

customElements.define('validation-form', ValidationForm);

4.4 格式化函数

传递数据格式化函数:

jsx
interface Formatter {
  (value: any): string;
}

function App() {
  const formatters: Record<string, Formatter> = {
    currency: (value: number) => `$${value.toFixed(2)}`,
    percentage: (value: number) => `${(value * 100).toFixed(1)}%`,
    date: (value: Date) => value.toLocaleDateString(),
    fileSize: (value: number) => {
      if (value < 1024) return `${value} B`;
      if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
      return `${(value / 1024 / 1024).toFixed(1)} MB`;
    }
  };
  
  const data = [
    { label: 'Price', value: 99.99, format: 'currency' },
    { label: 'Discount', value: 0.15, format: 'percentage' },
    { label: 'Date', value: new Date(), format: 'date' },
    { label: 'Size', value: 2048576, format: 'fileSize' }
  ];
  
  return (
    <data-display
      items={data}
      formatters={formatters}
    ></data-display>
  );
}

Custom Element应用格式化:

javascript
class DataDisplay extends HTMLElement {
  constructor() {
    super();
    this.items = [];
    this.formatters = {};
  }
  
  set items(value) {
    this._items = value;
    this.render();
  }
  
  set formatters(value) {
    this._formatters = value;
    this.render();
  }
  
  render() {
    this.innerHTML = `
      <table>
        <tbody>
          ${this._items.map(item => {
            const formatter = this._formatters[item.format];
            const formattedValue = formatter ? formatter(item.value) : String(item.value);
            
            return `
              <tr>
                <td>${item.label}</td>
                <td>${formattedValue}</td>
              </tr>
            `;
          }).join('')}
        </tbody>
      </table>
    `;
  }
}

customElements.define('data-display', DataDisplay);

第五部分:属性更新和响应

5.1 属性变化检测

Custom Element检测属性变化:

javascript
class ReactiveElement extends HTMLElement {
  constructor() {
    super();
    this._config = null;
  }
  
  set config(value) {
    const oldValue = this._config;
    this._config = value;
    
    // 触发变化回调
    this.onConfigChange(oldValue, value);
  }
  
  get config() {
    return this._config;
  }
  
  onConfigChange(oldValue, newValue) {
    console.log('Config changed:', { oldValue, newValue });
    
    // 比较具体字段
    if (!oldValue || oldValue.theme !== newValue.theme) {
      this.updateTheme(newValue.theme);
    }
    
    if (!oldValue || oldValue.language !== newValue.language) {
      this.updateLanguage(newValue.language);
    }
    
    // 触发事件通知React
    this.dispatchEvent(new CustomEvent('configChange', {
      detail: { oldValue, newValue },
      bubbles: true,
      composed: true
    }));
  }
  
  updateTheme(theme) {
    this.className = `theme-${theme}`;
  }
  
  updateLanguage(language) {
    this.setAttribute('lang', language);
  }
}

customElements.define('reactive-element', ReactiveElement);

React端监听变化:

jsx
function App() {
  const [config, setConfig] = useState({
    theme: 'light',
    language: 'en'
  });
  
  const handleConfigChange = (event: CustomEvent) => {
    console.log('Config changed in element:', event.detail);
  };
  
  const updateTheme = () => {
    setConfig(prev => ({ ...prev, theme: prev.theme === 'light' ? 'dark' : 'light' }));
  };
  
  return (
    <div>
      <button onClick={updateTheme}>Toggle Theme</button>
      
      <reactive-element
        config={config}
        onconfigChange={handleConfigChange}
      ></reactive-element>
    </div>
  );
}

5.2 批量属性更新

一次性更新多个属性:

jsx
function App() {
  const [settings, setSettings] = useState({
    user: { name: 'John', email: 'john@example.com' },
    theme: 'light',
    language: 'en',
    notifications: true,
    autoSave: false
  });
  
  const updateAll = () => {
    // 一次性更新整个配置对象
    setSettings({
      user: { name: 'Jane', email: 'jane@example.com' },
      theme: 'dark',
      language: 'zh',
      notifications: false,
      autoSave: true
    });
  };
  
  const updatePartial = () => {
    // 部分更新
    setSettings(prev => ({
      ...prev,
      theme: 'dark',
      notifications: false
    }));
  };
  
  return (
    <settings-panel settings={settings}></settings-panel>
  );
}

Custom Element优化批量更新:

javascript
class SettingsPanel extends HTMLElement {
  constructor() {
    super();
    this._settings = null;
    this._updateScheduled = false;
  }
  
  set settings(value) {
    this._settings = value;
    
    // 批量更新:使用requestAnimationFrame
    if (!this._updateScheduled) {
      this._updateScheduled = true;
      requestAnimationFrame(() => {
        this.render();
        this._updateScheduled = false;
      });
    }
  }
  
  render() {
    if (!this._settings) return;
    
    this.innerHTML = `
      <div class="settings theme-${this._settings.theme}">
        <div class="user-info">
          <h3>${this._settings.user.name}</h3>
          <p>${this._settings.user.email}</p>
        </div>
        
        <div class="preferences">
          <label>
            <input type="checkbox" ${this._settings.notifications ? 'checked' : ''} />
            Notifications
          </label>
          
          <label>
            <input type="checkbox" ${this._settings.autoSave ? 'checked' : ''} />
            Auto Save
          </label>
          
          <select>
            <option value="en" ${this._settings.language === 'en' ? 'selected' : ''}>English</option>
            <option value="zh" ${this._settings.language === 'zh' ? 'selected' : ''}>中文</option>
          </select>
        </div>
      </div>
    `;
  }
}

customElements.define('settings-panel', SettingsPanel);

5.3 增量更新策略

只更新变化的部分:

javascript
class IncrementalElement extends HTMLElement {
  constructor() {
    super();
    this._data = null;
    this._cache = new Map();
  }
  
  set data(value) {
    const oldData = this._data;
    this._data = value;
    
    if (!oldData) {
      // 首次渲染
      this.fullRender();
      return;
    }
    
    // 增量更新
    this.incrementalUpdate(oldData, value);
  }
  
  fullRender() {
    this.innerHTML = `
      <div>
        <div class="header" data-field="title"></div>
        <div class="content" data-field="content"></div>
        <div class="footer" data-field="stats"></div>
      </div>
    `;
    this.updateAllFields();
  }
  
  incrementalUpdate(oldData, newData) {
    // 只更新变化的字段
    if (oldData.title !== newData.title) {
      this.updateField('title', newData.title);
    }
    
    if (JSON.stringify(oldData.content) !== JSON.stringify(newData.content)) {
      this.updateField('content', newData.content);
    }
    
    if (JSON.stringify(oldData.stats) !== JSON.stringify(newData.stats)) {
      this.updateField('stats', newData.stats);
    }
  }
  
  updateField(fieldName, value) {
    const element = this.querySelector(`[data-field="${fieldName}"]`);
    if (!element) return;
    
    switch(fieldName) {
      case 'title':
        element.innerHTML = `<h2>${value}</h2>`;
        break;
      case 'content':
        element.innerHTML = `<p>${value}</p>`;
        break;
      case 'stats':
        element.innerHTML = `<span>Views: ${value.views}, Likes: ${value.likes}</span>`;
        break;
    }
  }
}

customElements.define('incremental-element', IncrementalElement);

React使用增量更新:

jsx
function App() {
  const [data, setData] = useState({
    title: 'Initial Title',
    content: 'Initial content',
    stats: { views: 0, likes: 0 }
  });
  
  const updateTitle = () => {
    setData(prev => ({ ...prev, title: 'Updated Title' }));
    // 只有title字段会被更新
  };
  
  const incrementViews = () => {
    setData(prev => ({
      ...prev,
      stats: { ...prev.stats, views: prev.stats.views + 1 }
    }));
    // 只有stats字段会被更新
  };
  
  return (
    <div>
      <button onClick={updateTitle}>Update Title</button>
      <button onClick={incrementViews}>Increment Views</button>
      
      <incremental-element data={data}></incremental-element>
    </div>
  );
}

第六部分:TypeScript类型系统

6.1 基础类型定义

为Custom Elements定义TypeScript类型。

简单类型定义:

typescript
// types/my-elements.d.ts
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'simple-element': {
        // 基本类型
        name?: string;
        age?: number;
        active?: boolean;
        
        // 可选和必需
        requiredProp: string;
        optionalProp?: string;
      };
    }
  }
}

使用类型定义:

tsx
function App() {
  return (
    <simple-element
      requiredProp="必需的值"  // 必须提供
      name="John"
      age={30}
      active={true}
    ></simple-element>
  );
}

// TypeScript会检查:
// - requiredProp必须提供
// - 属性类型必须匹配

6.2 接口和类型别名

使用接口定义复杂类型:

typescript
// types/data-types.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  avatar?: string;
}

export interface Product {
  id: string;
  title: string;
  price: number;
  stock: number;
  category: string;
  tags: string[];
}

export interface ChartData {
  labels: string[];
  datasets: Array<{
    label: string;
    data: number[];
    backgroundColor?: string;
    borderColor?: string;
  }>;
}

元素类型定义:

typescript
// types/custom-elements.d.ts
import { User, Product, ChartData } from './data-types';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'user-profile': {
        user: User;
        editable?: boolean;
        onuserUpdate?: (event: CustomEvent<Partial<User>>) => void;
      };
      
      'product-card': {
        product: Product;
        onaddToCart?: (event: CustomEvent<{ productId: string; quantity: number }>) => void;
      };
      
      'chart-widget': {
        data: ChartData;
        type?: 'line' | 'bar' | 'pie';
        onchartClick?: (event: CustomEvent<{ datasetIndex: number; index: number }>) => void;
      };
    }
  }
}

类型安全使用:

tsx
import { User, Product } from './data-types';

function App() {
  const user: User = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    role: 'admin'
  };
  
  const product: Product = {
    id: 'prod-1',
    title: 'React 19 Book',
    price: 49.99,
    stock: 100,
    category: 'Books',
    tags: ['React', 'Programming']
  };
  
  const handleUserUpdate = (event: CustomEvent<Partial<User>>) => {
    const updates = event.detail;
    console.log('User updates:', updates);
  };
  
  return (
    <div>
      <user-profile
        user={user}
        editable
        onuserUpdate={handleUserUpdate}
      />
      
      <product-card product={product} />
    </div>
  );
}

6.3 泛型类型定义

使用泛型创建可复用的类型:

typescript
// types/generic-elements.d.ts
interface ListItem {
  id: string | number;
  [key: string]: any;
}

interface Column<T> {
  key: keyof T;
  title: string;
  width?: number;
  sortable?: boolean;
  render?: (value: any, item: T) => string;
}

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'data-table': {
        data?: ListItem[];
        columns?: Column<any>[];
        pageSize?: number;
        currentPage?: number;
        onrowClick?: (event: CustomEvent<ListItem>) => void;
        onsort?: (event: CustomEvent<{ column: string; direction: 'asc' | 'desc' }>) => void;
        onpageChange?: (event: CustomEvent<number>) => void;
      };
    }
  }
}

泛型组件使用:

tsx
interface Employee {
  id: number;
  name: string;
  department: string;
  salary: number;
  hireDate: Date;
}

function EmployeeTable() {
  const employees: Employee[] = [
    { id: 1, name: 'John', department: 'Engineering', salary: 100000, hireDate: new Date('2020-01-01') },
    { id: 2, name: 'Jane', department: 'Marketing', salary: 90000, hireDate: new Date('2021-06-15') }
  ];
  
  const columns: Column<Employee>[] = [
    { key: 'id', title: 'ID', width: 60 },
    { key: 'name', title: 'Name', width: 150, sortable: true },
    { key: 'department', title: 'Department', width: 120, sortable: true },
    { 
      key: 'salary', 
      title: 'Salary', 
      width: 100,
      render: (value: number) => `$${value.toLocaleString()}`
    },
    { 
      key: 'hireDate', 
      title: 'Hire Date', 
      width: 120,
      render: (value: Date) => value.toLocaleDateString()
    }
  ];
  
  const handleRowClick = (event: CustomEvent<Employee>) => {
    const employee = event.detail;
    console.log('Selected employee:', employee.name);
  };
  
  return (
    <data-table
      data={employees}
      columns={columns}
      pageSize={10}
      onrowClick={handleRowClick}
    />
  );
}

6.4 联合类型和可选属性

定义灵活的类型:

typescript
type Size = 'small' | 'medium' | 'large';
type Variant = 'default' | 'primary' | 'success' | 'warning' | 'danger';
type Placement = 'top' | 'bottom' | 'left' | 'right';

interface ButtonProps {
  label?: string;
  size?: Size;
  variant?: Variant;
  disabled?: boolean;
  loading?: boolean;
  icon?: string;
  iconPosition?: 'left' | 'right';
}

interface TooltipProps {
  content: string;
  placement?: Placement;
  delay?: number;
  trigger?: 'hover' | 'click' | 'focus';
}

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'custom-button': ButtonProps & {
        onclick?: (event: MouseEvent) => void;
        children?: React.ReactNode;
      };
      
      'custom-tooltip': TooltipProps & {
        onshow?: (event: CustomEvent) => void;
        onhide?: (event: CustomEvent) => void;
        children?: React.ReactNode;
      };
    }
  }
}

类型安全的使用:

tsx
function App() {
  const buttonSize: Size = 'large';
  const tooltipPlacement: Placement = 'top';
  
  return (
    <div>
      <custom-button
        label="Save"
        size={buttonSize}
        variant="primary"
        icon="save"
        iconPosition="left"
        onclick={() => console.log('Saving...')}
      >
        Save Document
      </custom-button>
      
      <custom-tooltip
        content="Click to save your work"
        placement={tooltipPlacement}
        delay={200}
        trigger="hover"
      >
        <span>Hover me</span>
      </custom-tooltip>
    </div>
  );
}

第七部分:性能优化

7.1 属性缓存

使用useMemo缓存复杂属性:

tsx
function OptimizedApp() {
  const [rawData, setRawData] = useState([]);
  const [filter, setFilter] = useState('all');
  const [sort, setSort] = useState('name');
  
  // 缓存计算结果
  const processedData = useMemo(() => {
    let result = [...rawData];
    
    // 过滤
    if (filter !== 'all') {
      result = result.filter(item => item.category === filter);
    }
    
    // 排序
    result.sort((a, b) => {
      if (sort === 'name') return a.name.localeCompare(b.name);
      if (sort === 'price') return a.price - b.price;
      return 0;
    });
    
    return result;
  }, [rawData, filter, sort]);
  
  // 缓存配置对象
  const config = useMemo(() => ({
    filter,
    sort,
    theme: 'light'
  }), [filter, sort]);
  
  return (
    <data-grid
      data={processedData}
      config={config}
    ></data-grid>
  );
}

7.2 引用稳定性

保持函数引用稳定:

tsx
function StableReferences() {
  const [items, setItems] = useState([]);
  
  // 使用useCallback保持函数引用稳定
  const handleItemClick = useCallback((id: number) => {
    console.log('Item clicked:', id);
  }, []);
  
  const handleItemUpdate = useCallback((id: number, changes: any) => {
    setItems(prev => prev.map(item => 
      item.id === id ? { ...item, ...changes } : item
    ));
  }, []);
  
  const handleItemDelete = useCallback((id: number) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);
  
  // 稳定的回调对象
  const callbacks = useMemo(() => ({
    onClick: handleItemClick,
    onUpdate: handleItemUpdate,
    onDelete: handleItemDelete
  }), [handleItemClick, handleItemUpdate, handleItemDelete]);
  
  return (
    <item-manager
      items={items}
      callbacks={callbacks}
    ></item-manager>
  );
}

7.3 属性变化追踪

追踪属性变化以调试性能:

tsx
function DebugComponent() {
  const [data, setData] = useState({ count: 0, text: '' });
  const elementRef = useRef<HTMLElement>(null);
  
  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;
    
    // 监控属性访问
    let accessCount = 0;
    const originalSetter = Object.getOwnPropertyDescriptor(
      Object.getPrototypeOf(element),
      'data'
    )?.set;
    
    if (originalSetter) {
      Object.defineProperty(element, 'data', {
        set(value) {
          accessCount++;
          console.log(`Property 'data' set ${accessCount} times`);
          originalSetter.call(this, value);
        },
        get() {
          return element._data;
        }
      });
    }
  }, []);
  
  return (
    <debug-element
      ref={elementRef}
      data={data}
    ></debug-element>
  );
}

性能分析:

jsx
function PerformanceAnalysis() {
  const [data, setData] = useState({});
  const renderCount = useRef(0);
  const propUpdateCount = useRef(0);
  
  useEffect(() => {
    renderCount.current++;
    console.log(`Component rendered ${renderCount.current} times`);
  });
  
  useEffect(() => {
    propUpdateCount.current++;
    console.log(`Data prop updated ${propUpdateCount.current} times`);
  }, [data]);
  
  return (
    <div>
      <div>Renders: {renderCount.current}</div>
      <div>Prop Updates: {propUpdateCount.current}</div>
      
      <monitored-element data={data}></monitored-element>
    </div>
  );
}

第八部分:高级模式

8.1 配置对象模式

使用单一配置对象简化API:

jsx
interface ElementConfig {
  appearance: {
    theme: 'light' | 'dark';
    size: 'small' | 'medium' | 'large';
    variant: 'default' | 'outlined' | 'filled';
  };
  behavior: {
    disabled: boolean;
    loading: boolean;
    clickable: boolean;
  };
  data: {
    title: string;
    description: string;
    metadata: Record<string, any>;
  };
  callbacks: {
    onClick?: () => void;
    onHover?: () => void;
    onChange?: (value: any) => void;
  };
}

function App() {
  const config: ElementConfig = {
    appearance: {
      theme: 'dark',
      size: 'large',
      variant: 'filled'
    },
    behavior: {
      disabled: false,
      loading: false,
      clickable: true
    },
    data: {
      title: 'Welcome',
      description: 'This is a description',
      metadata: { version: '1.0' }
    },
    callbacks: {
      onClick: () => console.log('Clicked'),
      onChange: (value) => console.log('Changed:', value)
    }
  };
  
  return (
    <configurable-element config={config}></configurable-element>
  );
}

Custom Element使用配置:

javascript
class ConfigurableElement extends HTMLElement {
  constructor() {
    super();
    this.config = null;
  }
  
  set config(value) {
    this._config = value;
    this.applyConfig();
  }
  
  applyConfig() {
    if (!this._config) return;
    
    const { appearance, behavior, data, callbacks } = this._config;
    
    // 应用外观
    this.className = `theme-${appearance.theme} size-${appearance.size} variant-${appearance.variant}`;
    
    // 应用行为
    this.toggleAttribute('disabled', behavior.disabled);
    this.toggleAttribute('loading', behavior.loading);
    this.toggleAttribute('clickable', behavior.clickable);
    
    // 渲染数据
    this.innerHTML = `
      <div>
        <h3>${data.title}</h3>
        <p>${data.description}</p>
      </div>
    `;
    
    // 绑定回调
    if (behavior.clickable && callbacks.onClick) {
      this.addEventListener('click', callbacks.onClick);
    }
  }
}

customElements.define('configurable-element', ConfigurableElement);

8.2 Builder模式

使用Builder模式构建复杂配置:

tsx
class ElementConfigBuilder {
  private config: Partial<ElementConfig> = {};
  
  withAppearance(appearance: ElementConfig['appearance']) {
    this.config.appearance = appearance;
    return this;
  }
  
  withBehavior(behavior: ElementConfig['behavior']) {
    this.config.behavior = behavior;
    return this;
  }
  
  withData(data: ElementConfig['data']) {
    this.config.data = data;
    return this;
  }
  
  withCallbacks(callbacks: ElementConfig['callbacks']) {
    this.config.callbacks = callbacks;
    return this;
  }
  
  build(): ElementConfig {
    return this.config as ElementConfig;
  }
}

function App() {
  const config = new ElementConfigBuilder()
    .withAppearance({ theme: 'dark', size: 'large', variant: 'filled' })
    .withBehavior({ disabled: false, loading: false, clickable: true })
    .withData({ title: 'Title', description: 'Description', metadata: {} })
    .withCallbacks({ onClick: () => console.log('Clicked') })
    .build();
  
  return (
    <configurable-element config={config}></configurable-element>
  );
}

8.3 属性验证

运行时验证传递的属性:

javascript
class ValidatedElement extends HTMLElement {
  constructor() {
    super();
    this._data = null;
    this._schema = {
      name: { type: 'string', required: true, minLength: 1, maxLength: 50 },
      age: { type: 'number', required: true, min: 0, max: 150 },
      email: { type: 'string', required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
      tags: { type: 'array', required: false, itemType: 'string' }
    };
  }
  
  set data(value) {
    // 验证数据
    const validation = this.validate(value);
    
    if (!validation.valid) {
      console.error('Validation failed:', validation.errors);
      
      this.dispatchEvent(new CustomEvent('validationError', {
        detail: { errors: validation.errors },
        bubbles: true,
        composed: true
      }));
      
      return;
    }
    
    this._data = value;
    this.render();
  }
  
  validate(data) {
    const errors = [];
    
    Object.entries(this._schema).forEach(([field, rules]) => {
      const value = data[field];
      
      // 检查必需字段
      if (rules.required && (value === undefined || value === null)) {
        errors.push(`${field} is required`);
        return;
      }
      
      if (value === undefined || value === null) return;
      
      // 检查类型
      const actualType = Array.isArray(value) ? 'array' : typeof value;
      if (actualType !== rules.type) {
        errors.push(`${field} must be ${rules.type}, got ${actualType}`);
        return;
      }
      
      // 字符串验证
      if (rules.type === 'string') {
        if (rules.minLength && value.length < rules.minLength) {
          errors.push(`${field} must be at least ${rules.minLength} characters`);
        }
        if (rules.maxLength && value.length > rules.maxLength) {
          errors.push(`${field} must be at most ${rules.maxLength} characters`);
        }
        if (rules.pattern && !rules.pattern.test(value)) {
          errors.push(`${field} has invalid format`);
        }
      }
      
      // 数字验证
      if (rules.type === 'number') {
        if (rules.min !== undefined && value < rules.min) {
          errors.push(`${field} must be at least ${rules.min}`);
        }
        if (rules.max !== undefined && value > rules.max) {
          errors.push(`${field} must be at most ${rules.max}`);
        }
      }
      
      // 数组验证
      if (rules.type === 'array' && rules.itemType) {
        const invalidItems = value.filter(item => typeof item !== rules.itemType);
        if (invalidItems.length > 0) {
          errors.push(`${field} must contain only ${rules.itemType} items`);
        }
      }
    });
    
    return {
      valid: errors.length === 0,
      errors
    };
  }
  
  render() {
    if (!this._data) return;
    
    this.innerHTML = `
      <div>
        <h3>${this._data.name}</h3>
        <p>Age: ${this._data.age}</p>
        <p>Email: ${this._data.email}</p>
        ${this._data.tags ? `<p>Tags: ${this._data.tags.join(', ')}</p>` : ''}
      </div>
    `;
  }
}

customElements.define('validated-element', ValidatedElement);

React使用验证:

tsx
function App() {
  const [data, setData] = useState({
    name: 'John',
    age: 30,
    email: 'john@example.com',
    tags: ['developer', 'react']
  });
  const [validationErrors, setValidationErrors] = useState<string[]>([]);
  
  const handleValidationError = (event: CustomEvent<{ errors: string[] }>) => {
    setValidationErrors(event.detail.errors);
  };
  
  const updateName = (newName: string) => {
    setData(prev => ({ ...prev, name: newName }));
  };
  
  return (
    <div>
      {validationErrors.length > 0 && (
        <div className="errors">
          {validationErrors.map((error, i) => (
            <div key={i} className="error">{error}</div>
          ))}
        </div>
      )}
      
      <input 
        value={data.name}
        onChange={e => updateName(e.target.value)}
      />
      
      <validated-element
        data={data}
        onvalidationError={handleValidationError}
      ></validated-element>
    </div>
  );
}

7.2 深度比较优化

避免不必要的更新:

jsx
import { isEqual } from 'lodash';

function DeepCompareExample() {
  const [data, setData] = useState({ user: { name: 'John' }, items: [1, 2, 3] });
  const prevDataRef = useRef(data);
  
  // 只在数据真正变化时更新
  const stableData = useMemo(() => {
    if (isEqual(prevDataRef.current, data)) {
      return prevDataRef.current; // 返回旧引用
    }
    prevDataRef.current = data;
    return data;
  }, [data]);
  
  return (
    <deep-compare-element data={stableData}></deep-compare-element>
  );
}

Custom Element端比较:

javascript
class DeepCompareElement extends HTMLElement {
  constructor() {
    super();
    this._data = null;
  }
  
  set data(value) {
    // 浅比较
    if (this._data === value) {
      console.log('Reference unchanged, skipping update');
      return;
    }
    
    // 深度比较(如果需要)
    if (this.deepEqual(this._data, value)) {
      console.log('Value unchanged, skipping update');
      return;
    }
    
    this._data = value;
    this.render();
  }
  
  deepEqual(obj1, obj2) {
    return JSON.stringify(obj1) === JSON.stringify(obj2);
  }
}

7.3 虚拟化大数据

处理大量数据传递:

jsx
function VirtualizedData() {
  const [allData] = useState(() => 
    Array.from({ length: 100000 }, (_, i) => ({
      id: i,
      name: `Item ${i}`,
      value: Math.random()
    }))
  );
  
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 });
  
  // 只传递可见数据
  const visibleData = useMemo(() => 
    allData.slice(visibleRange.start, visibleRange.end),
    [allData, visibleRange.start, visibleRange.end]
  );
  
  const handleScroll = (event: CustomEvent<{ start: number; end: number }>) => {
    setVisibleRange(event.detail);
  };
  
  return (
    <virtual-list
      items={visibleData}
      totalCount={allData.length}
      itemHeight={40}
      onscroll={handleScroll}
    ></virtual-list>
  );
}

第九部分:常见陷阱

9.1 属性定义位置错误

错误示例:

javascript
// ❌ 错误:在connectedCallback中定义
class WrongElement extends HTMLElement {
  connectedCallback() {
    this.data = undefined; // 太晚了!
  }
}

// ❌ 错误:在原型上定义
class WrongElement extends HTMLElement {
  data = undefined; // 类字段,但React检测不到
}

// ✅ 正确:在constructor中定义
class CorrectElement extends HTMLElement {
  constructor() {
    super();
    this.data = undefined; // React可以检测到
  }
}

9.2 属性命名冲突

避免与HTML原生属性冲突:

javascript
// ❌ 避免使用这些名称
class BadNaming extends HTMLElement {
  constructor() {
    super();
    this.value = undefined;  // ⚠️ 可能与<input>的value冲突
    this.name = undefined;   // ⚠️ 可能与HTML name属性冲突
    this.id = undefined;     // ⚠️ 会与HTML id属性冲突
  }
}

// ✅ 使用自定义前缀
class GoodNaming extends HTMLElement {
  constructor() {
    super();
    this.customValue = undefined;
    this.customName = undefined;
    this.elementData = undefined;
  }
}

9.3 类型转换问题

处理类型转换:

javascript
class TypeSafeElement extends HTMLElement {
  constructor() {
    super();
    this.count = 0;
  }
  
  set count(value) {
    // 确保类型安全
    if (typeof value === 'string') {
      this._count = Number(value);
    } else if (typeof value === 'number') {
      this._count = value;
    } else {
      console.warn('count must be number or string, got:', typeof value);
      this._count = 0;
    }
    
    if (isNaN(this._count)) {
      this._count = 0;
    }
    
    this.render();
  }
  
  get count() {
    return this._count;
  }
}

9.4 内存泄漏

避免函数引用导致的内存泄漏:

jsx
// ❌ 可能导致内存泄漏
function LeakyComponent() {
  const [data, setData] = useState([]);
  
  // 每次渲染都创建新函数
  const handleClick = (id) => {
    setData(prev => prev.filter(item => item.id !== id));
  };
  
  return (
    <list-element onClick={handleClick}></list-element>
  );
}

// ✅ 使用useCallback避免泄漏
function SafeComponent() {
  const [data, setData] = useState([]);
  
  const handleClick = useCallback((id) => {
    setData(prev => prev.filter(item => item.id !== id));
  }, []); // 稳定引用
  
  return (
    <list-element onClick={handleClick}></list-element>
  );
}

常见问题

Q1: 为什么对象属性传递不生效?

A: 确保在构造函数中定义属性。

javascript
// ❌ 错误
class MyElement extends HTMLElement {
  connectedCallback() {
    console.log(this.data); // undefined
  }
}

// ✅ 正确
class MyElement extends HTMLElement {
  constructor() {
    super();
    this.data = undefined; // 必须在这里定义
  }
  
  connectedCallback() {
    console.log(this.data); // 正确接收到对象
  }
}

Q2: 如何处理null和undefined?

A: 在setter中进行检查。

javascript
class NullSafeElement extends HTMLElement {
  constructor() {
    super();
    this.data = null;
  }
  
  set data(value) {
    if (value === null || value === undefined) {
      this._data = null;
      this.renderEmpty();
      return;
    }
    
    this._data = value;
    this.render();
  }
  
  renderEmpty() {
    this.innerHTML = '<div>No data available</div>';
  }
  
  render() {
    this.innerHTML = `<div>${JSON.stringify(this._data)}</div>`;
  }
}

Q3: 属性更新为什么不触发重新渲染?

A: 检查是否正确实现了setter。

javascript
// ❌ 错误:直接赋值
class WrongElement extends HTMLElement {
  constructor() {
    super();
    this.data = undefined;
  }
  
  // 缺少setter,无法检测变化
}

// ✅ 正确:实现getter/setter
class CorrectElement extends HTMLElement {
  constructor() {
    super();
    this._data = undefined;
  }
  
  set data(value) {
    this._data = value;
    if (this.isConnected) {
      this.render();
    }
  }
  
  get data() {
    return this._data;
  }
  
  render() {
    // 更新UI
  }
}

Q4: 如何传递React组件作为属性?

A: 不能直接传递,但可以使用render prop模式。

jsx
// ❌ 不能这样做
<my-element 
  component={<MyReactComponent />}  // 不支持
></my-element>

// ✅ 使用render函数
<my-element 
  renderContent={(data) => <MyReactComponent data={data} />}
></my-element>

或使用Slot:

jsx
<my-element>
  <div slot="content">
    <MyReactComponent />
  </div>
</my-element>

Q5: 性能优化的最佳时机是什么?

A: 先测量,再优化。

jsx
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration) => {
    console.log(`${id} ${phase} took ${actualDuration}ms`);
    
    if (actualDuration > 16) {
      console.warn('Slow render detected!');
    }
  };
  
  return (
    <Profiler id="CustomElement" onRender={onRender}>
      <my-element data={largeData}></my-element>
    </Profiler>
  );
}

总结

React 19属性传递改进:

核心特性:

1. 类型支持
✅ 字符串、数字、布尔值
✅ 对象和数组
✅ 函数和回调
✅ Date和特殊对象

2. 自动检测
✅ 属性vs特性
✅ 类型保持
✅ 智能转换

3. TypeScript
✅ 完整类型定义
✅ 类型检查
✅ 自动补全

4. 性能
✅ 优化的更新机制
✅ 引用稳定性
✅ 批量更新

最佳实践:

1. 在constructor中定义所有属性
2. 实现getter/setter
3. 使用useMemo/useCallback优化
4. 正确的TypeScript类型定义
5. 运行时验证
6. 性能监控
7. 错误处理
8. 文档完善

React 19的属性传递机制让Custom Elements成为React应用的原生成员,极大提升了开发体验!