Appearance
自定义元素属性传递
学习目标
通过本章学习,你将掌握:
- 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应用的原生成员,极大提升了开发体验!