响应式属性

Lit 组件以 JavaScript 类字段或属性的形式接收输入并存储其状态。响应式属性是可以在更改时触发响应式更新周期的属性,它们会重新渲染组件,并可以选择性地读取或写入到特性(attributes)。

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 中通常被标记为受保护或私有。

@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 类字段中声明属性

Permalink to "在静态 properties 类字段中声明属性"

要在静态 properties 类字段中声明属性:

class MyElement extends LitElement {
static properties = {
mode: {type: String},
data: {attribute: false},
};

constructor() {
super();
this.data = {};
}
}

一个空的选项对象等同于为所有选项指定默认值。

避免使用类字段声明属性时的问题

Permalink to "避免使用类字段声明属性时的问题"

类字段与响应式属性之间存在问题。类字段是在元素实例上定义的,而响应式属性是作为访问器定义在元素原型上的。根据 JavaScript 的规则,实例属性优先于原型属性并有效地隐藏了原型属性。这意味着当使用类字段时,响应式属性访问器将不起作用,以至于设置属性不会触发元素更新。

class MyElement extends LitElement {
static properties = {foo: {type: String}}
foo = 'Default'; // ❌ 这会使 `foo` 不具有响应性
}

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,你可以使用类字段来声明响应式属性,只要你使用以下模式之一:

// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true, // 如果使用装饰器
"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';
}
}
class MyElement extends LitElement {
static properties = {foo: {type: String}}
accessor foo = 'Default';

@property()
accessor bar = 'Default';
}

选项对象可以具有以下属性:

attribute

属性是否与特性关联,或关联特性的自定义名称。默认值:true。如果 attribute 为 false,则忽略 converterreflecttype 选项。更多信息请参见设置特性名称

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 的运行时用于字符串序列化/反序列化,不应与类型检查机制混淆。

useDefault

reflect 设置为 true 时,设置为 true 可以防止默认值的初始特性反射,并在移除其对应特性时将属性重置为其默认值。

默认值是在构造函数中或使用自动访问器设置的属性的初始值。这个值会保留在内存中,所以对于非原始的 Object/Array 属性,最好避免设置 useDefault: true。更多信息,请参见启用特性反射反射特性的最佳实践

省略选项对象或指定空选项对象等同于为所有选项指定默认值。

内部响应式状态指的是不属于组件公共 API 的响应式属性。这些状态属性没有对应的特性,也不打算从组件外部使用。内部响应式状态应该由组件自身设置。

使用 @state 装饰器来声明内部响应式状态:

@state()
protected _active = false;

使用静态 properties 类字段时,你可以通过使用 state: true 选项来声明内部响应式状态:

static properties = {
_active: {state: true}
};

constructor() {
this._active = false;
}

内部响应式状态不应该从组件外部被引用。在 TypeScript 中,这些属性应该被标记为私有或受保护。我们也建议对 JavaScript 用户使用前导下划线(_)这样的约定来标识私有或受保护的属性。

内部响应式状态的工作方式与公共响应式属性完全相同,只是没有与属性关联的特性。对于内部响应式状态,你只能指定 hasChanged 函数这一个选项。

@state 装饰器还可以作为代码压缩器的提示,表明属性名称可以在压缩期间更改。

属性变化可以触发响应式更新周期,这会导致组件重新渲染其模板。

当属性发生变化时,会发生以下序列:

  1. 调用属性的 setter。
  2. setter 调用组件的 requestUpdate 方法。
  3. 比较属性的旧值和新值。
    • 默认情况下,Lit 使用严格不等式测试来确定值是否已更改(即 newValue !== oldValue)。
    • 如果属性有 hasChanged 函数,则使用属性的旧值和新值调用它。
  4. 如果检测到属性变化,则异步安排更新。如果已经安排了更新,则只执行一次更新。
  5. 调用组件的 update 方法,将更改的属性反射到特性并重新渲染组件的模板。

注意,如果你修改对象或数组属性,它不会触发更新,因为对象本身没有改变。更多信息请参见修改对象和数组属性

有很多方法可以挂钩和修改响应式更新周期。更多信息请参见响应式更新周期

关于属性变化检测的更多信息,请参见自定义变化检测

修改对象或数组不会改变对象引用,所以不会触发更新。你可以通过以下两种方式之一处理对象和数组属性:

  • 不可变数据模式。 将对象和数组视为不可变。例如,要从 myArray 中删除一个项目,构造一个新数组:

    this.myArray = this.myArray.filter((_, i) => i !== indexToRemove);

    虽然这个例子很简单,但使用像 Immer 这样的库来管理不可变数据通常很有帮助。这可以帮助避免在设置深层嵌套对象时出现棘手的样板代码。

  • 手动触发更新。 修改数据并调用 requestUpdate() 直接触发更新。例如:

    this.myArray.splice(indexToRemove, 1);
    this.requestUpdate();

    当不带参数调用时,requestUpdate() 会安排一次更新,而不调用 hasChanged() 函数。但请注意,requestUpdate() 只会导致当前组件更新。也就是说,如果一个组件使用上面显示的代码,并且组件将 this.myArray 传递给子组件,子组件将检测到数组引用没有改变,所以它不会更新。

通常,对于大多数应用程序来说,使用带有不可变对象的自上而下的数据流是最好的。 它确保每个需要渲染新值的组件都能做到(并且尽可能高效地做到,因为数据树中没有改变的部分不会导致依赖它们的组件更新)。

直接修改数据并调用 requestUpdate() 应该被视为高级用例。在这种情况下,你(或其他系统)需要识别所有使用被修改数据的组件,并在每个组件上调用 requestUpdate()。当这些组件分布在应用程序中时,这变得难以管理。如果不能稳健地做到这一点,意味着你可能修改了在应用程序两个部分中渲染的对象,但只有一个部分更新。

在简单的情况下,当你知道某个数据只在单个组件中使用时,如果你愿意,直接修改数据并调用 requestUpdate() 应该是安全的。

虽然属性(properties)对于接收 JavaScript 数据作为输入很有用,但特性(attributes)是 HTML 允许从标记配置元素的标准方式,无需使用 JavaScript 设置属性。为响应式属性同时提供属性和特性接口是 Lit 组件在各种环境中发挥作用的关键方式,包括那些没有客户端模板引擎渲染的环境,如从 CMS 提供的静态 HTML 页面。

默认情况下,Lit 为每个公共响应式属性设置一个相应的观察特性,并在特性变化时更新属性。属性值也可以选择性地被反射(写回到特性)。

虽然元素属性可以是任何类型,但特性始终是字符串。这会影响非字符串属性的观察特性反射特性

  • 观察一个特性(从特性设置属性),特性值必须从字符串转换为匹配属性类型。

  • 反射一个特性(从属性设置特性),属性值必须转换为字符串。

暴露特性的布尔属性应该默认为 false。更多信息请参见布尔特性

默认情况下,Lit 为所有公共响应式属性创建一个相应的观察特性。观察特性的名称是属性名称的小写形式:

// 观察特性名称是 "myvalue"
@property({ type: Number })
myValue = 0;
// 观察特性名称是 "myvalue"
static properties = {
myValue: { type: Number },
};

constructor() {
super();
this.myValue = 0;
}

要创建一个具有不同名称的观察特性,请将 attribute 设置为字符串:

// 观察特性将被称为 my-name
@property({ attribute: 'my-name' })
myName = 'Ogden';
// 观察特性将被称为 my-name
static properties = {
myName: { attribute: 'my-name' },
};

constructor() {
super();
this.myName = 'Ogden'
}

要防止为属性创建观察特性,请将 attribute 设置为 false。该属性将不会从标记中的特性初始化,特性变化也不会影响它。

// 此属性没有观察特性
@property({ attribute: false })
myData = {};
// 此属性没有观察特性
static properties = {
myData: { attribute: false },
};

constructor() {
super();
this.myData = {};
}

内部响应式状态永远没有关联的特性。

可以使用观察特性从标记为属性提供初始值。例如:

<my-element myvalue="99"></my-element>

Lit 有一个默认转换器,它处理 StringNumberBooleanArrayObject 属性类型。

要使用默认转换器,请在属性声明中指定 type 选项:

// 使用默认转换器
@property({ type: Number })
count = 0;
// 使用默认转换器
static properties = {
count: { type: Number },
};

constructor() {
super();
this.count = 0;
}

如果你没有为属性指定类型_或_自定义转换器,它的行为就像你指定了 type: String 一样。

下表显示了默认转换器如何处理每种类型的转换。

类型转换
String如果元素有相应的特性,将属性设置为特性值。
Number如果元素有相应的特性,将属性设置为 Number(attributeValue)
Boolean如果元素有相应的特性,将属性设置为 true。
如果没有,将属性设置为 false。
Object, Array如果元素有相应的特性,将属性值设置为 JSON.parse(attributeValue)

对于除 Boolean 之外的任何情况,如果元素没有相应的特性,属性保持其默认值,如果没有设置默认值则为 undefined

类型转换
String, Number如果属性已定义且非空,将特性设置为属性值。
如果属性为 null 或 undefined,则移除特性。
Boolean如果属性为真值,创建特性并将其值设置为空字符串。
如果属性为假值,则移除特性
Object, Array如果属性已定义且非空,将特性设置为 JSON.stringify(propertyValue)
如果属性为 null 或 undefined,则移除特性。

你可以在属性声明中使用 converter 选项指定自定义属性转换器:

myProp: {
converter: // 自定义属性转换器
}

converter 可以是对象或函数。如果是对象,它可以有 fromAttributetoAttribute 键:

prop1: {
converter: {
fromAttribute: (value, type) => {
// `value` 是一个字符串
// 将其转换为 `type` 类型的值并返回
},
toAttribute: (value, type) => {
// `value` 是 `type` 类型的
// 将其转换为字符串并返回
}
}
}

如果 converter 是一个函数,它用作 fromAttribute 的替代:

myProp: {
converter: (value, type) => {
// `value` 是一个字符串
// 将其转换为 `type` 类型的值并返回
}
}

如果没有为反射特性提供 toAttribute 函数,则使用默认转换器将特性设置为属性值。

如果 toAttribute 返回 nullundefined,则移除特性。

对于可以从特性配置的布尔属性,它必须默认为 false。如果默认为 true,你就不能从标记中将其设置为 false,因为特性的存在,无论有无值,都等同于 true。这是 web 平台中特性的标准行为。

如果这种行为不适合你的用例,有几个选择:

  • 更改属性名称使其默认为 false。例如,web 平台使用 disabled 特性(默认为 false),而不是 enabled

  • 使用字符串值或数字值的特性代替。

reflect 设置为 true 可配置属性,使其在每次变化时将其值反射到其对应特性上。反射特性对于序列化元素状态很有用,因为它们对 CSS 和 querySelector 等 DOM API 可见。

useDefault 设置为 true 可防止属性的默认值在初始时反射到其对应特性上。所有后续变化都会被反射;如果特性被移除,属性将重置为其默认值。

这与 Web 平台上 id 等特性的行为一致。元素的 id 属性默认值为 ''(空字符串),初始时没有 id 特性,但如果设置了 id 属性(即使是空字符串),相应的 id 特性就会被反射。如果 id 特性被移除,元素的 id 属性将重置为其初始值 ''

例如:

// 属性 "active" 的值将反射到特性 "active"
active: {reflect: true}
// 属性 "variant" 的值将被反射,但 "variant" 特性不会在初始时设置为属性的默认值。
variant: {reflect: true, useDefault: true}

当属性变化时,Lit 会按照使用默认转换器提供自定义转换器中描述的方式设置相应的特性值。

Lit 在更新期间跟踪反射状态。 你可能已经意识到,如果属性变化反射到特性,而特性变化又更新属性,这有可能创建一个无限循环。然而,Lit 专门跟踪属性和特性何时被设置,以防止这种情况发生。

为确保元素按预期运行并具有良好性能,在反射特性时请尽量遵循以下最佳实践:

  • 特性通常应该被视为从其所有者到元素的输入,而不是由元素本身控制,因此应该谨慎地将属性反射到特性。考虑在可能的情况下使用 :state 伪选择器Accessibility Object Model

  • 反射属性通常也应该设置 useDefault: true,因为这可以防止元素自发地生成用户未设置的特性,并有助于匹配预期的平台行为。

  • 不建议反射 object 或 array 类型的属性。这可能导致大对象序列化到 DOM 中,当使用 useDefault 时可能导致性能下降并消耗过多内存。

  • 属性装饰器不会改变分配给响应式属性的任何值,这被认为是自定义访问器的最佳实践。例如,有时原生元素会将属性限制为某些有效值,如果为属性分配了无效值,该属性将被设置为默认值。useDefault: true 不会这样做 - 它只在特性被移除时恢复默认值。如果你想在属性赋值时改变属性值,请定义并装饰一个自定义属性 setter。

默认情况下,LitElement 为所有响应式属性生成一个 getter/setter 对。每当你设置属性时,就会调用 setter:

// 声明一个属性
@property()
greeting: string = 'Hello';
...
// 之后,设置属性
this.greeting = 'Hola'; // 调用 greeting 的生成属性访问器
// 声明一个属性
static properties = {
greeting: {},
}
constructor() {
this.super();
this.greeting = 'Hello';
}
...
// 之后,设置属性
this.greeting = 'Hola'; // 调用 greeting 的生成属性访问器

生成的访问器自动调用 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 回调。自定义 setter 可用于罕见情况,即同步验证用户设置的任何值很重要时。

如果你的类为属性定义了自己的访问器,Lit 不会用生成的访问器覆盖它们。如果你的类没有为属性定义访问器,Lit 会生成它们,即使父类已经定义了该属性或访问器。

在极少数情况下,子类可能需要更改或添加父类已有属性的属性选项。

要防止 Lit 生成覆盖父类定义的访问器的属性访问器,请在属性声明中将 noAccessor 设置为 true

static properties = {
myProp: { type: Number, noAccessor: true }
};

在定义自己的访问器时,不需要设置 noAccessor

所有响应式属性都有一个函数 hasChanged(),它在设置属性时被调用。

hasChanged 比较属性的旧值和新值,并评估属性是否已更改。如果 hasChanged() 返回 true,Lit 会启动元素更新(如果尚未安排)。有关更新的更多信息,请参见响应式更新周期

hasChanged() 的默认实现使用严格不等式比较:hasChanged()newVal !== oldVal 时返回 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。