响应式属性
Lit 组件接收输入并将它们的状态存储为 JavaScript 类字段或属性。响应式属性是指当更改时可以触发响应式更新周期的属性,从而重新渲染组件,并且可以选择读写属性。
class MyElement extends LitElement {
@property()
name?: string;
}
class MyElement extends LitElement {
static properties = {
name: {},
};
}
Lit 管理您的响应式属性及其对应的属性。特别是
- 响应式更新。Lit 为每个响应式属性生成一对 getter/setter。当响应式属性发生更改时,组件会安排更新。
- 属性处理。默认情况下,Lit 会设置一个与属性相对应的观察属性,并在属性发生更改时更新属性。属性值也可以选择反射回属性。
- 超类属性。Lit 会自动应用超类声明的属性选项。您无需重新声明属性,除非您想更改选项。
- 元素升级。如果在元素已存在于 DOM 中后定义了 Lit 组件,Lit 会处理升级逻辑,确保在元素升级之前设置在元素上的任何属性在元素升级时触发正确的响应式副作用。
公共属性和内部状态
指向“公共属性和内部状态”的永久链接公共属性是组件公共 API 的一部分。一般来说,公共属性,尤其是公共响应式属性,应该被视为输入。
组件不应该更改自己的公共属性,除非响应用户输入。例如,菜单组件可能具有一个公共的 selected
属性,该属性可以由元素的所有者初始化为给定值,但在用户选择项目时由组件本身更新。在这些情况下,组件应该分派一个事件来指示组件的所有者 selected
属性已更改。有关更多详细信息,请参见 分派事件。
Lit 还支持内部响应式状态。内部响应式状态是指不是组件 API 一部分的响应式属性。这些属性没有对应的属性,并且通常在 TypeScript 中标记为 protected 或 private。
@state()
private _counter = 0;
static properties = {
_counter: {state: true}
};
constructor() {
super();
this._counter = 0;
}
组件操作自己的内部响应式状态。在某些情况下,内部响应式状态可能从公共属性初始化,例如,如果用户可见属性和内部状态之间存在昂贵的转换。
与公共响应式属性一样,更新内部响应式状态会触发更新周期。有关更多信息,请参见 内部响应式状态。
公共响应式属性
指向“公共响应式属性”的永久链接使用装饰器或静态 properties
字段来声明元素的公共响应式属性。
在任何一种情况下,您都可以传递一个选项对象来配置属性的功能。
使用装饰器声明属性
指向“使用装饰器声明属性”的永久链接将 @property
装饰器与类字段声明一起使用以声明响应式属性。
class MyElement extends LitElement {
@property({type: String})
mode?: string;
@property({attribute: false})
data = {};
}
@property
装饰器的参数是 选项对象。省略参数等同于为所有选项指定默认值。
使用装饰器。装饰器是 JavaScript 的一个提议功能,因此您需要使用像 Babel 或 TypeScript 编译器这样的编译器来使用装饰器。有关详细信息,请参见 启用装饰器。
在静态 properties 类字段中声明属性
指向“在静态 properties 类字段中声明属性”的永久链接要在静态 properties
类字段中声明属性
class MyElement extends LitElement {
static properties = {
mode: {type: String},
data: {attribute: false},
};
constructor() {
super();
this.data = {};
}
}
一个空的选项对象等同于为所有选项指定默认值。
避免在声明属性时出现类字段问题
指向“避免在声明属性时出现类字段问题”的永久链接类字段 与响应式属性存在问题交互。类字段在元素实例上定义,而响应式属性在元素原型上定义为访问器。根据 JavaScript 的规则,实例属性优先于原型属性并有效地隐藏了原型属性。这意味着当使用类字段时,响应式属性访问器不起作用,因此设置属性不会触发元素更新。
class MyElement extends LitElement {
static properties = {foo: {type: String}}
foo = 'Default'; // ❌ this will make `foo` not reactive
}
在 JavaScript 中,在声明响应式属性时不能使用类字段。相反,必须在元素构造函数中初始化属性
class MyElement extends LitElement {
static properties = {foo: {type: String}}
constructor() {
super();
this.foo = 'Default';
}
}
或者,您可以使用 带有 Babel 的标准装饰器 来声明响应式属性。
class MyElement extends LitElement {
@property()
accessor foo = 'Default';
}
对于 TypeScript,您可以使用类字段来声明响应式属性,只要您使用以下模式之一
- 将
useDefineForClassFields
编译器选项设置为false
。当 使用带有 TypeScript 的装饰器 时,这已经是推荐做法。
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true, // If using decorators
"useDefineForClassFields": false,
}
}
class MyElement extends LitElement {
static properties = {foo: {type: String}}
foo = 'Default';
@property()
bar = 'Default';
}
- 在字段上添加
declare
关键字,并将字段的初始化程序放在构造函数中。
class MyElement extends LitElement {
declare foo: string;
static properties = {foo: {type: String}}
constructor() {
super();
this.foo = 'Default';
}
}
- 在字段上添加
accessor
关键字以使用 自动访问器。
class MyElement extends LitElement {
static properties = {foo: {type: String}}
accessor foo = 'Default';
@property()
accessor bar = 'Default';
}
属性选项
指向“属性选项”的永久链接选项对象可以具有以下属性
attribute
属性是否与属性相关联,或者关联属性的自定义名称。默认值:true。如果
attribute
为 false,则converter
、reflect
和type
选项将被忽略。有关更多信息,请参见 设置属性名称。converter
hasChanged
每次设置属性时都会调用一个函数,以确定属性是否已更改,以及是否应该触发更新。如果未指定,LitElement 使用严格不等式检查 (
newValue !== oldValue
) 来确定属性值是否已更改。有关更多信息,请参见 自定义变更检测。noAccessor
设置为 true 以避免生成默认属性访问器。此选项很少需要。默认值:false。有关更多信息,请参见 阻止 Lit 生成属性访问器。
reflect
属性值是否反射回关联属性。默认值:false。有关更多信息,请参见 启用属性反射。
state
设置为 true 以将属性声明为内部响应式状态。内部响应式状态会像公共响应式属性一样触发更新,但 Lit 不会为其生成属性,用户也不应该从组件外部访问它。等同于使用
@state
装饰器。默认值:false。有关更多信息,请参见 内部响应式状态。type
当将字符串值属性转换为属性时,Lit 的默认属性转换器会将字符串解析为给定的类型,反之亦然,当将属性反射回属性时。如果设置了
converter
,则此字段将传递给转换器。如果type
未指定,则默认转换器将其视为type: String
。参见 使用默认转换器。当使用 TypeScript 时,此字段通常应该与为字段声明的 TypeScript 类型匹配。但是,
type
选项由 Lit 的运行时用于字符串序列化/反序列化,不应与类型检查机制混淆。- 调用属性的设置器。
- 设置器调用组件的
requestUpdate
方法。 - 比较属性的旧值和新值。
- 默认情况下,Lit 使用严格不等测试来确定值是否已更改(即
newValue !== oldValue
)。 - 如果属性具有
hasChanged
函数,则会使用属性的旧值和新值来调用它。
- 默认情况下,Lit 使用严格不等测试来确定值是否已更改(即
- 如果检测到属性更改,则异步安排更新。如果已经安排了更新,则只会执行一次更新。
- 调用组件的
update
方法,将更改的属性反映到属性并重新渲染组件的模板。 不可变数据模式。将对象和数组视为不可变的。例如,要从
myArray
中删除一个项,请构造一个新数组this.myArray = this.myArray.filter((_, i) => i !== indexToRemove);
虽然此示例很简单,但使用像Immer这样的库来管理不可变数据通常很有帮助。这可以帮助避免在设置深层嵌套对象时出现棘手的样板代码。
手动触发更新。修改数据并调用
requestUpdate()
以直接触发更新。例如this.myArray.splice(indexToRemove, 1);
this.requestUpdate();
当没有参数调用时,
requestUpdate()
会安排更新,而不会调用hasChanged()
函数。但是请注意,requestUpdate()
只会导致当前组件更新。也就是说,如果一个组件使用上面显示的代码,并且该组件将this.myArray
传递给子组件,那么子组件将检测到数组引用没有改变,因此不会更新。要观察属性(从属性设置属性),必须将属性值从字符串转换为匹配属性类型。
要反映属性(从属性设置属性),必须将属性值转换为字符串。
更改属性名称,使其默认为 false。例如,web 平台使用
disabled
属性(默认为 false),而不是enabled
。使用字符串值或数字值属性代替。
省略选项对象或指定一个空的选项对象等同于为所有选项指定默认值。
内部响应式状态
指向“内部响应式状态”的永久链接内部响应式状态是指不是组件公共 API 一部分的响应式属性。这些状态属性没有对应的属性,也不打算从组件外部使用。内部响应式状态应该由组件本身设置。
使用 @state
装饰器声明内部响应式状态
@state()
protected _active = false;
使用静态 properties
类字段,您可以通过使用 state: true
选项声明内部响应式状态。
static properties = {
_active: {state: true}
};
constructor() {
this._active = false;
}
不应从组件外部引用内部响应式状态。在 TypeScript 中,这些属性应该标记为 private 或 protected。我们还建议使用像前导下划线 (_
) 这样的约定来标识 JavaScript 用户的私有或受保护属性。
内部响应式状态的工作原理与公共响应式属性相同,区别在于没有与该属性关联的属性。您可以为内部响应式状态指定的唯一选项是hasChanged
函数。
@state
装饰器还可以作为代码压缩器的提示,表明该属性名称可以在压缩过程中更改。
属性更改时会发生什么
“属性更改时会发生什么”的永久链接属性更改可以触发一个响应式更新周期,这会导致组件重新渲染其模板。
当属性更改时,将执行以下顺序
请注意,如果您修改对象或数组属性,它不会触发更新,因为对象本身没有更改。有关更多信息,请参见修改对象和数组属性。
有多种方法可以挂钩并修改响应式更新周期。有关更多信息,请参见响应式更新周期。
有关属性更改检测的更多信息,请参见自定义更改检测。
修改对象和数组属性
“修改对象和数组属性”的永久链接修改对象或数组不会更改对象引用,因此不会触发更新。您可以通过以下两种方式之一处理对象和数组属性
通常,对大多数应用程序而言,使用自上而下的数据流和不可变对象是最好的。它确保每个需要渲染新值的组件都会渲染(并且尽可能高效地渲染,因为数据树中没有更改的部分不会导致依赖它们的组件更新)。
直接修改数据并调用requestUpdate()
应该被认为是一种高级用例。在这种情况下,您(或其他一些系统)需要识别使用修改数据的全部组件,并在每个组件上调用requestUpdate()
。当这些组件分布在整个应用程序中时,这将难以管理。如果不健壮地执行此操作,则意味着您可能会修改在应用程序的两个部分中渲染的对象,但只有一部分更新。
在简单情况下,如果您知道特定数据仅在单个组件中使用,那么如果您愿意,直接修改数据并调用requestUpdate()
应该是安全的。
虽然属性非常适合接收 JavaScript 数据作为输入,但属性是 HTML 允许从标记配置元素的标准方式,无需使用 JavaScript 来设置属性。为其响应式属性提供属性和属性接口是 Lit 组件在各种环境中都非常有用的关键方式,包括那些在没有客户端模板引擎的情况下渲染的环境,例如从 CMS 提供的静态 HTML 页面。
默认情况下,Lit 为每个公共响应式属性设置一个观察到的属性,并在属性更改时更新属性。属性值也可以选择反映(写入回属性)。
虽然元素属性可以是任何类型,但属性始终是字符串。这会影响非字符串属性的观察到的属性和反映的属性
暴露属性的布尔属性应默认为 false。有关更多信息,请参见布尔属性。
设置属性名称
“设置属性名称”的永久链接默认情况下,Lit 为所有公共响应式属性创建一个相应的观察到的属性。观察到的属性的名称是属性名称,小写
// observed attribute name is "myvalue"
@property({ type: Number })
myValue = 0;
// observed attribute name is "myvalue"
static properties = {
myValue: { type: Number },
};
constructor() {
super();
this.myValue = 0;
}
要创建具有不同名称的观察到的属性,请将attribute
设置为字符串
// Observed attribute will be called my-name
@property({ attribute: 'my-name' })
myName = 'Ogden';
// Observed attribute will be called my-name
static properties = {
myName: { attribute: 'my-name' },
};
constructor() {
super();
this.myName = 'Ogden'
}
要防止为属性创建观察到的属性,请将attribute
设置为false
。属性不会从标记中的属性初始化,并且属性更改不会影响它。
// No observed attribute for this property
@property({ attribute: false })
myData = {};
// No observed attribute for this property
static properties = {
myData: { attribute: false },
};
constructor() {
super();
this.myData = {};
}
内部响应式状态永远不会有关联的属性。
可以将观察到的属性用于从标记中为属性提供初始值。例如
<my-element myvalue="99"></my-element>
使用默认转换器
“使用默认转换器”的永久链接Lit 具有一个默认转换器,它处理String
、Number
、Boolean
、Array
和Object
属性类型。
要使用默认转换器,请在属性声明中指定type
选项
// Use the default converter
@property({ type: Number })
count = 0;
// Use the default converter
static properties = {
count: { type: Number },
};
constructor() {
super();
this.count = 0;
}
如果您没有为属性指定类型或自定义转换器,它会像您指定了type: String
一样运行。
下面的表格显示了默认转换器如何处理每种类型的转换。
从属性到属性
类型 | 转换 |
---|---|
字符串 | 如果元素具有相应的属性,则将属性设置为属性值。 |
数字 | 如果元素具有相应的属性,则将属性设置为Number(attributeValue) 。 |
布尔值 | 如果元素具有相应的属性,则将属性设置为 true。 如果没有,则将属性设置为 false。 |
Object 、Array | 如果元素具有相应的属性,则将属性值设置为JSON.parse(attributeValue) 。 |
对于除Boolean
以外的任何情况,如果元素没有相应的属性,则属性保留其默认值,或者如果未设置默认值,则保留undefined
。
从属性到属性
类型 | 转换 |
---|---|
String 、Number | 如果属性已定义且不为空,则将属性设置为属性值。 如果属性为空或未定义,则删除属性。 |
布尔值 | 如果属性为真值,则创建属性并将其值设置为一个空字符串。 如果属性为假值,则删除属性 |
Object 、Array | 如果属性已定义且不为空,则将属性设置为JSON.stringify(propertyValue) 。如果属性为空或未定义,则删除属性。 |
提供自定义转换器
“提供自定义转换器”的永久链接您可以在属性声明中使用converter
选项指定自定义属性转换器
myProp: {
converter: // Custom property converter
}
converter
可以是对象或函数。如果是对象,它可以具有fromAttribute
和toAttribute
的键
prop1: {
converter: {
fromAttribute: (value, type) => {
// `value` is a string
// Convert it to a value of type `type` and return it
},
toAttribute: (value, type) => {
// `value` is of type `type`
// Convert it to a string and return it
}
}
}
如果converter
是函数,它将代替fromAttribute
使用
myProp: {
converter: (value, type) => {
// `value` is a string
// Convert it to a value of type `type` and return it
}
}
如果未为反映的属性提供toAttribute
函数,则使用默认转换器将属性设置为属性值。
如果toAttribute
返回null
或undefined
,则删除属性。
布尔属性
“布尔属性”的永久链接要使布尔属性可从属性配置,它必须默认为 false。如果它默认为 true,则无法从标记中将其设置为 false,因为属性的存在,无论是否有值,都等同于 true。这是 web 平台中属性的标准行为。
如果此行为不适合您的用例,则有以下两种选择
启用属性反射
“启用属性反射”的永久链接您可以配置属性,以便只要它发生更改,其值就会反映到其相应的属性。反映的属性非常有用,因为属性对 CSS 可见,并且对 DOM API(如querySelector
)可见。
例如
// Value of property "active" will reflect to attribute "active"
active: {reflect: true}
当属性更改时,Lit 会按照使用默认转换器或提供自定义转换器中的描述设置相应的属性值。
属性通常应该被视为从其所有者到元素的输入,而不是由元素本身控制,因此应谨慎地将属性反映到属性。如今,它对于样式和可访问性等情况是必要的,但随着平台添加诸如:state
伪选择器和可访问性对象模型之类的功能,这种情况可能会发生变化,这些功能填补了这些空白。
不建议反映类型为对象或数组的属性。这会导致大型对象序列化到 DOM,从而导致性能下降。
Lit 在更新期间跟踪反射状态。您可能已经意识到,如果属性更改反映到属性,并且属性更改更新属性,则它有可能创建一个无限循环。但是,Lit 会跟踪属性和属性何时被专门设置,以防止这种情况发生
自定义属性访问器
“自定义属性访问器”的永久链接默认情况下,LitElement 为所有响应式属性生成一对 getter/setter。每当您设置属性时,都会调用 setter
// Declare a property
@property()
greeting: string = 'Hello';
...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor
// Declare a property
static properties = {
greeting: {},
}
constructor() {
this.super();
this.greeting = 'Hello';
}
...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor
生成的访问器会自动调用requestUpdate()
,如果尚未开始更新,则启动更新。
创建自定义属性访问器
“创建自定义属性访问器”的永久链接要指定获取和设置属性的工作方式,您可以定义自己的 getter/setter 对。例如
private _prop = 0;
@property()
set prop(val: number) {
this._prop = Math.floor(val);
}
get prop() { return this._prop; }
static properties = {
prop: {},
};
_prop = 0;
set prop(val) {
this._prop = Math.floor(val);
}
get prop() { return this._prop; }
要将自定义属性访问器与@property
或@state
装饰器一起使用,请将装饰器放在 setter 上,如上所示。@property
或@state
修饰的 setter 会调用requestUpdate()
。
在大多数情况下,您无需创建自定义属性访问器。 要从现有属性计算值,我们建议使用 willUpdate
回调,它允许您在更新周期内设置值,而不会触发额外的更新。要执行元素更新后的自定义操作,我们建议使用 updated
回调。自定义设置器可在罕见情况下使用,此时同步验证用户设置的任何值非常重要。
如果您的类为属性定义了自己的访问器,Lit 不会用生成的访问器覆盖它们。如果您的类未为属性定义访问器,即使超类已定义属性或访问器,Lit 也会生成它们。
阻止 Lit 生成属性访问器
“防止 Lit 生成属性访问器”的永久链接在罕见情况下,子类可能需要更改或添加其超类上存在的属性的属性选项。
要防止 Lit 生成覆盖超类定义的访问器的属性访问器,请在属性声明中将 noAccessor
设置为 true
static properties = {
myProp: { type: Number, noAccessor: true }
};
定义自己的访问器时,您无需设置 noAccessor
。
自定义变更检测
“自定义更改检测”的永久链接所有反应式属性都具有一个函数 hasChanged()
,该函数在设置属性时被调用。
hasChanged
比较属性的旧值和新值,并评估属性是否已更改。如果 hasChanged()
返回 true,则 Lit 会启动元素更新(如果尚未计划)。有关更新的更多信息,请参阅 反应式更新周期 。
hasChanged()
的默认实现使用严格的不等式比较:如果 newVal !== oldVal
,则 hasChanged()
返回 true
。
要自定义属性的 hasChanged()
,请将其指定为属性选项
@property({
hasChanged(newVal: string, oldVal: string) {
return newVal?.toLowerCase() !== oldVal?.toLowerCase();
}
})
myProp: string | undefined;
static properties = {
myProp: {
hasChanged(newVal, oldVal) {
return newVal?.toLowerCase() !== oldVal?.toLowerCase();
}
}
};
在以下示例中,hasChanged()
仅对奇数值返回 true。