Fields、Properties 和 Attributes
在组件的 JavaScript 类中声明字段(Fields),在模板中引用它们以动态更新内容。本章是 LWC 属性和响应式系统的完整参考,涵盖从基础概念到高级模式的所有内容。
Fields、Properties 和 Attributes —— 概述
Field 和 Property 几乎可互换,但视角不同:组件作者在类中声明字段(如 itemName = "New Item"),类的实例拥有属性。对组件消费者来说,字段就是属性。在 LWC 中,只有用 @api 装饰的字段才对消费者公开为对象属性。
Property 和 Attribute 同样几乎可互换但语境不同:HTML 中讨论特性(attributes),JavaScript 中讨论属性(properties)。JavaScript camelCase(itemName)映射为 HTML kebab-case(item-name),以符合 HTML 标准——HTML 规范要求特性名全部小写且用连字符分隔。
LWC 观察字段和属性值的变化并自动重渲染——这是响应式系统的核心。LWC 还反射许多标准 Web API 的属性,让你可以像操作标准 DOM 元素一样操作 LWC 组件。对于无障碍访问,你还可以控制公共 JavaScript 属性是否出现在渲染的 HTML 特性中。
来自 Aura?Aura 中的 "attribute" 最接近的概念就是 LWC 的 JavaScript property。但 LWC 的属性系统更加标准化——它基于 Web Components 规范和原生 DOM API。
响应式系统(Reactivity)—— 核心概念
响应式是 LWC 框架最核心的机制——它是使组件动态响应数据变化的引擎:
- 框架观察字段和属性值的变化
- 检测到变化后做出反应
- 重新计算模板中使用的所有表达式
- 重新渲染组件以显示新值
模板中的公共属性默认是响应式的。如果一个字段的值发生变化,且该字段直接在模板中使用或在模板使用的某个 getter 中被引用,组件就会自动重渲染。当组件重渲染时,所有模板表达式被重新计算,renderedCallback() 生命周期钩子也会执行。
对于对象和数组,框架默认观察部分内部变化(如整体重新赋值),但如果需要深度观察嵌套属性和数组元素的变更,需要使用 @track 装饰器。Salesforce 提供了 @api 装饰器 和 @track 装饰器 的详细视频讲解。
公共属性 —— @api 装饰器
@api 装饰器是创建组件公共 API 的入口。用它装饰字段后,父组件就可以设置和读取该属性。在模板中使用的公共属性是响应式的——值改变时组件自动重渲染,同时 renderedCallback() 生命周期钩子也会执行。
@api 是 LWC 独有的装饰器(非标准 JavaScript 装饰器)。每个字段只能有一个装饰器——不能在同一个字段上同时使用 @api 和 @track。属性可以是自定义属性(你声明的)或从基础 HTMLElement 接口继承的属性。
自定义属性 —— todoItem 完整示例
// todoItem.js
import { LightningElement, api } from "lwc";
export default class TodoItem extends LightningElement {
@api itemName = "New Item"; // 公共属性,带默认值
}
<!-- todoItem.html -->
<template>
<div class="view"><label>{itemName}</label></div>
</template>
<!-- todoApp.html(父组件使用) -->
<c-todo-item item-name="Milk"></c-todo-item>
<c-todo-item item-name="Bread"></c-todo-item>
<!-- item-name (kebab-case) → itemName (camelCase) -->
命名映射是关键:JavaScript 属性用 camelCase(itemName),HTML 特性用 kebab-case(item-name)以符合 HTML 标准。实际应用中通常用 for:each 循环动态填充列表。
DOM 属性 —— 通过点号访问
当声明 @api itemName 时,该字段会变成自定义元素实例上的DOM 属性。父组件可通过点号访问:
// todoApp.js
const myItem = this.template.querySelector("c-todo-item").itemName;
// 获取引用 → 点号访问公共属性
典型模式:this.template.querySelector() 获取子组件引用 → 直接点号访问属性。这给父组件提供了对子组件公共属性的读写访问——是声明式 HTML 属性绑定的程序化对应方式。参考 lwc-recipes compositionBasics。
Fields、Objects 和 Arrays 的响应式
默认情况下,LWC 以浅层方式跟踪字段值变化——通过 === 比较检测新值赋值。两种响应式模式:
浅层(默认):仅检测字段整体的重新赋值。对原始值有效(this.name = 'new'),对对象/数组仅检测引用变化(this.obj = newObj),嵌套属性修改(this.obj.prop = 'x')或数组原地修改(this.arr.push('x'))不被检测。
深层(@track 装饰器):递归观察纯对象和数组的内部变更。允许直接修改嵌套属性(this.obj.prop = 'x')或数组元素(this.arr[0] = 'x')并触发重渲染。
原始类型与复杂类型的响应式 —— 代码演示
bool = true;
number = 42;
obj = { name: "John" };
checkMutation() {
this.bool = false; // ✅ 检测到(新值 ≠ 旧值)
this.number = 42; // ✗ 未检测(相同值 ===)
this.number = 43; // ✅ 检测到
this.obj.name = "Bob"; // ✗ 未检测(嵌套属性,引用未变)
this.obj = { name: "John" }; // ✅ 检测到(新对象,引用变化)
this.obj = { ...this.obj, title: "CEO" }; // ✅ 检测到
}
关键洞察:this.obj.name = 'Bob' 不被检测因为只修改了嵌套属性——obj 引用本身没变。this.obj = { ...this.obj, title: 'CEO' } 被检测因为展开运算符创建了全新对象。需要深度跟踪时使用 @track。
使用 @track 跟踪对象和数组内部变化
@track 装饰器启用对纯对象和数组的深度观察。它递归跟踪所有嵌套属性变化,包括嵌套对象、嵌套数组及其任意组合,循环引用也能正确处理。
@track 跟踪的内容:用 {} 创建的纯对象的所有嵌套属性变更、用 [] 创建的数组的元素增删改、嵌套对象和数组的任意混合。
@track 不跟踪的内容:继承自 Object 的复杂对象(类实例)、Date/Set/Map 对象、非纯 JavaScript 对象。对于这些类型,仍需创建新实例并赋值来触发重渲染。
观察对象属性 —— 不用 vs 用 @track
// 不用 @track —— 只检测字段级重新赋值
fullName = { firstName: "", lastName: "" };
this.fullName = { firstName: "John", lastName: "Doe" }; // ✅ 重渲染
this.fullName.firstName = "John"; // ✗ 不重渲染!
// 用 @track —— 检测属性级变更
@track fullName = { firstName: '', lastName: '' };
this.fullName.firstName = 'John'; // ✅ 重渲染
经验法则:如果属性包含对象且需要跟踪对象属性的变更,用 @track 注解。不用 @track 时只有字段完整重新赋值才会触发重渲染。
选择性跟踪 —— 只重渲染被访问的属性
即使使用了 @track,LWC 也采用选择性跟踪优化:每次渲染周期中框架记录具体访问了哪些属性。后续变更时,只有上次渲染中实际访问过的属性发生变化才会触发重渲染。
@track obj = { value1: 'Hello' };
get words() {
return Object.entries(this.obj).map(([key, value]) => ({key, value}));
}
this.obj.value1 = 'Hello World'; // ✅ 重渲染(value1 上次被访问了)
this.obj.value2 = 'Hello LWC'; // ✗ 不重渲染(value2 是全新的属性)
第一次渲染时框架记录 obj.value1 被访问。修改 value1 触发重渲染。但添加全新的 value2 不触发——因为 value2 在上次渲染中从未被访问,不影响已渲染内容。
添加新属性时的修复方案
// ❌ 不重渲染——直接给已有对象添加新属性
setValue2(e) {
this.obj.value2 = 'Hello LWC'; // value2 从未被访问过
}
// ✅ 重渲染——创建包含新旧属性完整的新对象
setValue2(e) {
this.obj = { ...this.obj, value2: 'Hello LWC' };
// 展开运算符保留 value1,同时加入 value2
// 整个对象引用变化 → 框架重新评估所有表达式
}
展开语法(...this.obj)复制原对象的所有可枚举自有属性到新对象中,然后添加或覆盖特定属性。这赋给 this.obj 一个全新引用,框架检测到字段级变化并重新评估所有表达式——包括调用 Object.entries 的 getter,现在能看到新属性。
观察数组元素 —— @track 用于数组
// 不用 @track
arr = ["a", "b"];
this.arr = ["x", "y", "z"]; // ✅ 重渲染(新数组)
this.arr[0] = "x"; // ✗ 不重渲染(引用未变)
this.arr.push("c"); // ✗ 不重渲染(引用未变)
// 用 @track
@track arr = ['a', 'b'];
this.arr[0] = 'x'; // ✅ 检测到
this.arr.push('c'); // ✅ 检测到
重要细节:框架不会自动将数组转换为字符串来显示元素级更新。需要使用 getter(如 computedArray)调用 join() 或其他序列化方法。getter 在 @track 检测到元素变化时被重新计算。
观察复杂对象 —— Date 示例
即使使用 @track,LWC不观察 Date、Set、Map 或类实例等复杂对象的内部状态变更——因为它们不是纯 JavaScript 对象。
@track x = new Date();
initDate() { this.x = new Date(); }
// ✅ 重渲染:x 指向新的 Date 对象
updateDate() { this.x.setHours(7); }
// ✗ 不重渲染:x 仍指向同一个 Date
// 浏览器控制台警告:
// "Property 'x' is set to a non-trackable object"
Init 按钮创建新 Date 并赋值,改变引用 → 重渲染。Update 按钮调用 setHours(),内部状态变了但引用不变 → 不重渲染。浏览器会记录有帮助的警告:"non-trackable object"。
复杂对象 —— 克隆模式(Clone Pattern)
克隆模式是处理所有复杂类型的通用解决方案——核心原理是 LWC 通过引用比较检测变化:
updateDate() {
const cloned = new Date(this.x.getTime()); // 1. 克隆现有对象
cloned.setHours(7); // 2. 在克隆体上修改
this.x = cloned; // 3. 赋值给字段 → 引用变化 → 重渲染!
}
| 类型 | 克隆方式 | 适用场景 |
|---|---|---|
| Date | new Date(old.getTime()) | 日期计算 |
| Set | new Set(old) | 去重集合 |
| Map | new Map(old) | 键值映射 |
| Array | [...old, newItem] | 数组操作 |
| Object | { ...old, newProp } | 对象操作 |
调试提示:设置属性为不可跟踪的值时,浏览器会记录警告。如果组件没有在预期时重渲染,查看浏览器控制台:Property "x" of [object:vm TrackDate] is set to a non-trackable object, which means changes into that object cannot be observed.
响应式字符串示例 —— helloExpressions 模板
这个实用示例将响应式、数据绑定和 getter 结合在一起。两个 lightning-input 字段绑定到同一个 handleChange 处理器,计算结果显示在 {uppercasedFullName} getter 中:
<lightning-input name="firstName" label="First Name"
onchange={handleChange}></lightning-input>
<lightning-input name="lastName" label="Last Name"
onchange={handleChange}></lightning-input>
<p class="slds-m-top_medium">
Uppercased Full Name: {uppercasedFullName}
</p>
经典模式:输入字段捕获用户数据 → 事件处理器更新组件状态 → 计算的 getter 转换状态用于显示——全部由响应式系统驱动。
响应式字符串示例 —— helloExpressions JavaScript
firstName = "";
lastName = "";
handleChange(event) {
const field = event.target.name;
if (field === "firstName") {
this.firstName = event.target.value;
} else if (field === "lastName") {
this.lastName = event.target.value;
}
}
get uppercasedFullName() {
return `${this.firstName} ${this.lastName}`.trim().toUpperCase();
}
响应式链条:用户输入 → handleChange 更新 firstName/lastName → 字段用于 uppercasedFullName getter → getter 用于模板 → 组件重渲染 → 新值显示。
注意:类中声明的字段是响应式的,但运行时动态添加的 expando 属性不是响应式的,不会触发重渲染。Expando 属性是在运行时添加到对象上的属性(如 this.newProp = 'value')——LWC 不会跟踪它们。始终在类的声明中定义所有需要响应式跟踪的字段。
实战参考:完整可运行代码见 lwc-recipes helloExpressions 组件。
属性和特性名称
理解 JavaScript 属性名与 HTML 特性名的命名规则是构建正确 LWC 组件的基础。
JavaScript 属性命名 —— 保留字和前缀
不能以这些前缀开头:on(保留给事件处理器,如 onClick)、aria(保留给无障碍属性,如 ariaDescribedby)、data(保留给数据属性,如 dataProperty)。这些前缀在 HTML/DOM 规范中有特殊含义。
保留词:slot(slot 元素投射)、part(CSS shadow parts 样式)、is(元素类型检查)。这些词与标准 DOM API 和 HTML 概念冲突,不能作为属性名使用。
HTML 特性命名 —— 规则和特殊情况
HTML 特性不能包含大写字符。有效的起始字符:小写字母、下划线(_)、美元符号($)、可选的连字符加字母(-a)。
特殊组合:双下划线(__)、下划线加连字符(_-)、连字符加下划线(-_,需连字符不是首字符时)。
大写属性名特殊情况:如果 JavaScript 属性以大写字母开头,HTML 特性必须使用特殊语法:@api Upper → -upper。大写字符被小写化并加上前导连字符作为信号。
HTML 全局特性 —— JavaScript 属性映射
部分 HTML 全局特性不遵循标准的 camelCase/kebab-case 转换。关键映射:
| HTML 特性 | JS 属性 | HTML 特性 | JS 属性 |
|---|---|---|---|
accesskey | accessKey | maxlength | maxLength |
contenteditable | contentEditable | readonly | readOnly |
tabindex | tabIndex | for | htmlFor |
colspan | colSpan | rowspan | rowSpan |
注意 for → htmlFor(for 是 JS 保留关键字),maxlength → maxLength(不是 maxlength)。
JavaScript 中的 ARIA 特性
ARIA 特性支持辅助技术。在 JavaScript 中访问时使用 camelCase:
// HTML 特性 → JavaScript 属性
aria-checked → ariaChecked
aria-label → ariaLabel
aria-describedby → ariaDescribedBy
aria-expanded → ariaExpanded
aria-errormessage → ariaErrorMessage
转换模式:去掉 aria- 前缀 → 连字符分隔转为 camelCase。ARIA 特性对屏幕阅读器、语音控制、开关设备和其他辅助技术至关重要。
LightningElement 类 —— 独特属性
LightningElement 是所有 LWC 的基础类。其独特属性包括:
- template —— 访问组件的 Shadow DOM 模板(Light DOM 组件中直接用
this) - hostElement —— 访问 LWC 的 HTMLElement(Shadow DOM 和 Light DOM 均可用)
- refs —— 通过 ref 指令访问 DOM 元素
- 五个生命周期回调:
connectedCallback(挂载)、disconnectedCallback(卸载)、render(模板选择)、renderedCallback(每次渲染后)、errorCallback(后代组件错误的错误边界)
LightningElement —— 继承属性
LightningElement 封装了标准 HTMLElement 接口。继承的属性和方法包括:addEventListener、dispatchEvent、getAttribute/hasAttribute/removeAttribute、getBoundingClientRect、getElementsByClassName/getElementsByTagName、querySelector/querySelectorAll,以及 children、classList、hidden、dir、draggable 等属性。它们与标准 Web 开发中的表现完全一致。
继续:id(LWC 会转换 id 值)、isConnected(检查 DOM 连接状态)、lang、shadowRoot(Light DOM 中为 null)、spellcheck、tabIndex、style(API v62.0+)、tagName 以及完整的 WAI-ARIA 属性集。
Web API 属性 —— Element 接口
LWC 反映了大量标准 Element 接口属性:children、classList、className、getAttribute 系列、getBoundingClientRect、querySelector/querySelectorAll、shadowRoot、slot 等。
LWS 变形注意:启用 Lightning Web Security 时,setAttribute、setAttributeNS和shadowRoot被 LWS 变形修改以实现安全沙箱。
Web API 属性 —— EventTarget、HTMLElement 和 Node
- EventTarget:
addEventListener、removeEventListener、dispatchEvent - HTMLElement:布局属性(
offsetHeight/Left/Top/Width、offsetParent)、contentEditable、dataset(LWS 修改)、显示属性(hidden、title、lang、dir) - Node:
childNodes、firstChild、lastChild、isConnected(检查是否在 DOM 中——在异步回调中很有用)
这四大接口让 LWC 组件成为完整的 DOM 公民。
WAI-ARIA 状态和属性(一)
LWC 反映大量 WAI-ARIA 属性以支持辅助技术。常用属性:ariaActiveDescendant(复合组件焦点管理)、ariaAtomic(实时区域更新)、ariaAutoComplete、ariaBusy、ariaChecked、ariaCol* 系列、ariaControls、ariaCurrent、ariaDescribedBy、ariaDisabled、ariaExpanded、ariaFlowTo。
WAI-ARIA 状态和属性(二)
继续:ariaHasPopup、ariaHidden、ariaInvalid、ariaLive(polite/assertive)、ariaModal、ariaRow* 系列、ariaValueNow/Min/Max(范围组件)、ariaPressed、ariaSort,以及 ariaBrailleLabel 和 ariaBrailleRoleDescription(盲文显示器专用)。
注意:LWC OSS v4.0.0+ 中,部分标准 WAI-ARIA 属性不再作为全局 polyfill 包含。如需了解哪些属性受到影响,请查看 Deprecated ARIA reflected properties。
使用 Getter 和 Setter 修改数据
通过自定义 setter 在每次设置公共属性时执行逻辑。黄金规则:
- 写了 setter 必须也写 getter
- 只在 getter 或 setter 之一上加 @api——不能两个都加(惯例是加在 getter 上)
- 用私有字段(下划线前缀如
_uppercaseItemName)持有属性值
此模式支持:数据转换(如 toUpperCase)、存储前验证值、规范化输入格式、触发副作用、用 try-catch 优雅处理错误。
Getter 和 Setter —— 大写转换示例
_uppercaseItemName; // 私有字段(下划线前缀 = 约定)
@api
get itemName() {
return this._uppercaseItemName; // @api 在 getter 上暴露属性
}
set itemName(value) {
this._uppercaseItemName = value.toUpperCase(); // setter 转换值
}
数据流:父组件设置 item-name="milk" → setter 接收 "milk" → 存储 "MILK" 到私有字段 → getter 返回 "MILK" → 模板显示 "MILK"。
Getter 和 Setter —— 用 try-catch 处理错误
@api
get itemName() {
try {
return this._uppercaseItemName;
} catch (e) {
return ""; // 优雅降级,返回安全默认值,防止组件崩溃
}
}
生产级组件应优雅处理错误。getter 中的 try-catch 确保内部状态异常时显示安全的回退值。setter 中也可添加验证:检查 null/undefined、验证格式、对无效输入抛出描述性错误。公共 API 应健壮——处理边界情况和无效输入而不崩溃。
布尔属性
布尔属性遵循标准 HTML 约定:特性存在 = true,缺失 = false。这意味着默认值必须是 false——这是最重要的规则,因为省略属性是表达 false 的唯一方式。
静态设置布尔属性
@api show = false; // 默认值必须为 false!
<!-- show = false(属性被省略) -->
<c-bool></c-bool>
<!-- show = true(属性存在,值为空) -->
<c-bool show></c-bool>
<!-- 错误!show="false" 评估为 TRUE! -->
<c-bool show="false"></c-bool>
<!-- 字符串 "false" 是 truthy!属性存在 = true! -->
关键:如果默认值设为 true,消费者永远无法在标记中静态设为 false,因为省略属性只会保持默认值 true。
动态设置布尔属性
<c-bool show={computedValue}></c-bool>
get computedValue() {
return this.someCondition; // 返回实际布尔值
}
与静态不同,动态绑定使用 JavaScript 表达式计算实际布尔值。当父组件状态变化时,getter 重新计算,新值向下传递到子组件的 show 属性。这是实现显示/隐藏、启用/禁用、展开/折叠等切换行为的标准模式。
将 JavaScript 属性反射到 HTML 特性
默认情况下,用 @api 暴露的属性不会出现在渲染的 HTML 中。但对无障碍访问来说,屏幕阅读器读取的是 HTML 特性而非 JavaScript 属性。反射模式:getter 用 @api 暴露属性 → 私有字段存值 → setter 调用 this.setAttribute() 推到渲染的 HTML。所有反射的 HTML 特性都是响应式的。
反射属性 —— 使用 setAttribute()
@api get title() { return this._privateTitle; }
set title(value) {
this._privateTitle = value.toUpperCase(); // 转换
this.setAttribute("title", this._privateTitle); // 反射到 DOM!
}
// 渲染输出:<c-my-component title="HOVER OVER THE COMPONENT TO SEE ME">
// ✅ title 特性在 DOM 中,屏幕阅读器可见,hover 显示 tooltip
反射属性 —— 不使用 setAttribute()
// 同样的代码但注释掉了 setAttribute:
set title(value) {
this._privateTitle = value.toUpperCase();
// this.setAttribute("title", this._privateTitle); ← 注释掉了!
}
// 渲染输出:<c-my-component>
// ❌ title 特性缺失!屏幕阅读器不可见,无 tooltip
JavaScript 属性仍然可以正常工作(component.title 仍能访问),但 HTML 特性完全不存在。任何依赖特性选择器的 CSS 或 JavaScript 也无法工作。
检查现有值和特殊特性
connectedCallback() {
const tabindex = this.getAttribute("tabindex");
if (!tabindex) {
this.setAttribute("tabindex", "0"); // 只在消费者未设置时应用默认值
}
}
最佳实践:在应用默认值前检查消费者是否已设置(this.getAttribute("tabindex"))。以下特性应始终使用 setAttribute()/getAttribute()——它们不能通过一般的属性方式可靠访问:
for—— 在 JS 中是保留关键字,必须用htmlFor或getAttribute("for")aria-activedescendant、aria-controls、aria-describedbyaria-details、aria-errormessage、aria-flowtoaria-labelledby、aria-owns
要从渲染 HTML 中隐藏特性,调用 this.removeAttribute()。
管理特性依赖 —— 概述
HTML 特性变为 JavaScript 属性赋值,但赋值顺序不保证。你不能依赖 rows 在 selectedRows 之前设置。
<c-datatable selected-rows="1,2" rows="1,2,3,4"></c-datatable>
<!-- selectedRows 可能在 rows 之前被设置! -->
解决方案:使用检查依赖项的 getter,在依赖数据可用后才执行操作。这种延迟评估模式确保无论赋值顺序如何,逻辑只在所有必要数据就绪后运行。
特性依赖 —— Datatable 示例(一)
@track state = {}; // 共享状态对象存所有属性值
@api get rows() { return this.state.rows; }
set rows(value) {
this.state.rows = value;
if (this.state.selectedRows && !this.selectedRowsSet) {
this.markSelectedRows(); // selectedRows 已就绪 → 处理
this.selectedRowsSet = true; // 设置标志防重复
}
// selectedRows 未就绪 → 跳过,等待 selectedRows setter
}
模式:共享 @track state 对象 → 每个 setter 检查另一个值是否已可用 → 两者都可用时执行依赖操作 → 设置标志避免重复工作。
特性依赖 —— Datatable 示例(二)
@api set selectedRows(value) {
this.state.selectedRows = value;
if (!this.state.rows) { // rows 还没到
this.selectedRowsSet = false;
return; // 提前返回,等 rows
}
this.markSelectedRows(); // rows 已就绪 → 执行
}
数据规范化补充模式:使用 getter 和 setter 确保公共 API 协议易于执行。组件不应更改标注了 @api 的属性值——这会破坏单向数据流。规范化模式有两种策略:
@track state = { selected: false };
privateSelected = 'false';
@api get selected() { return this.privateSelected; } // 返回原始值
set selected(value) {
this.privateSelected = value; // 存储原始值
this.state.selected = normalizeBoolean(value); // 规范化内部状态
}
// 模板中使用 state.selected(规范化的),
// 外部访问 selected 获得原始值
何时在 setter 中规范化:某个依赖值在设置时就需要使用规范化后的值(如动态追加 CSS 类)。何时在 getter 中规范化:模板需要始终能访问到有效值,即使消费者从未设置该属性。两种方法可组合使用,构建健壮、防御性组件 API。
感谢阅读本指南。如需继续学习,请参阅下一章:JavaScript。