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