前言
看到公司的仓库中有内部的组件库,但是组件库采用的不是react、也不是vue,而且采用lit这样的web components搭建的一套移动端组件库。采用web components的好处是不限制框架,同时具有跨端的能力。对于公司内不同技术栈的项目,也能统一ui风格。
看了一下语法,感觉难度不大,就适当学习了一下,这篇文章当作学习的一个笔记吧🤣。
⚠️ 注:该教程对于基础不牢,刚上手 vue、react 的同学不太友好,如果有 nestjs
的基础,掌握起来会轻松很多。
官方文档:stenciljs.com/
中文文档:stenciljs.jikun.dev/
什么是 Stenciljs
Stencil
是一个生成 Web Components
(更确切地说,是自定义元素)的编译器。通过使用 TypeScript、JSX 和 CSS 创建符合标准的 Web 组件,这些组件可以用来构建高质量的组件库。
根据这段描述,Stencil.js 就是一个编译器,它将组件代码编译成标准的 Web Components,像 lit 这个组件库也是采用的是 web components
,相比于 vue、react 的组件库,使用web components
构建的组件更具有跨平台的特点,下面是对比:
特性 | Stencil | Ant Design |
---|---|---|
核心技术 | Stencil.js + Web Components | React + TypeScript |
运行时依赖 | 无框架依赖 | 强依赖 React 生态 |
编译方式 | 编译为原生 Web Components | React 组件库 |
跨框架支持 | 原生支持 React/Vue/原生 JS | 需要独立适配版本 |
如何使用 Stenciljs
初始化项目到运行
- 命令初始化项目
bash
npm init stencil
pnpm create stencil
- 选择对应内容
bash
┌ Create Stencil App 🚀
│
◇ Select a starter project.
│ `component` Collection of web components that can be used anywhere
│
◇ Project name
│ `my-web-component`
│
◇ Create component project with name "my-web-component"?
Confirm?
│ Yes
│
◇ Done!
│
◆ ✔ A new git repo was initialized
│
◆ ✔ All setup in 9 ms
- 项目目录结构

- 项目文件介绍
my-component.tsx
ts
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'my-component', // 定义web components的标签名
styleUrl: 'my-component.css', // 引入样式文件
shadow: true // 是否创建shadow dom
})
export class MyComponent {
@Prop() first: string
@Prop() middle: string
@Prop() last: string
private getText(): string {
return this.first + this.middle + this.last
}
render() {
return <div>Hello, World! I'm {this.getText()}</div>
}
}
stencil.config.ts
ts
import { Config } from '@stencil/core'
export const config: Config = {
namespace: 'my-web-component', // 定义组件库的命名空间,用于生成组件的唯一标识
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader' // 指定 ESM 加载器的路径
},
{
type: 'dist-custom-elements', // 生成独立的自定义元素
customElementsExportBehavior: 'auto-define-custom-elements', // 自动定义自定义元素
externalRuntime: false // 将运行时包含在包中
},
{
type: 'docs-readme' // 自动生成组件的 README 文档
},
{
type: 'www', // 生成用于开发和演示的 WWW 目录
serviceWorker: null // 禁用 service worker
}
],
testing: {
browserHeadless: 'shell' // 在无头模式下运行浏览器测试
}
}
package.json
这部分和上面的配置文件,都需要对工程化的知识有一定了解
json
{
"name": "my-web-component",
"version": "0.0.1",
"description": "Stencil Component Starter",
"main": "dist/index.cjs.js", // commonjs模块的入口文件
"module": "dist/index.js", // esm模块的入口文件
"types": "dist/types/index.d.ts", // ts类型定义的入口文件
"collection": "dist/collection/collection-manifest.json", // 指向组件集合的清单文件,包含组件元数据
"collection:main": "dist/collection/index.js", // 指定集合的主要入口点
"unpkg": "dist/my-web-component/my-web-component.esm.js", // 指定通过 unpkg CDN 服务分发的 ES 模块文件路径
"exports": {
".": {
// 包的主入口点
"import": "./dist/my-web-component/my-web-component.esm.js", // esm导入时失业文件
"require": "./dist/my-web-component/my-web-component.cjs.js" // commonjs导入时使用的文件
},
"./my-component": {
"import": "./dist/components/my-component.js",
"types": "./dist/components/my-component.d.ts"
},
"./loader": {
"import": "./loader/index.js",
"require": "./loader/index.cjs",
"types": "./loader/index.d.ts"
}
},
"files": ["dist/", "loader/"], // 指定发布到 npm 时包含的文件和目录,确保只有必要的文件被发布
"scripts": {
"build": "stencil build",
"dev": "stencil build --dev --watch --serve",
"test": "stencil test --spec --e2e",
"test.watch": "stencil test --spec --e2e --watchAll",
"generate": "stencil generate"
},
"devDependencies": {
"@stencil/core": "^4.27.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.5"
},
"license": "MIT"
}
index.html
html
<!DOCTYPE html>
<html
dir="ltr"
lang="en"
>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"
/>
<title>Stencil Component Starter</title>
<!-- 导入组件 -->
<script
type="module"
src="/build/my-web-component.esm.js"
></script>
<script
nomodule
src="/build/my-web-component.js"
></script>
</head>
<body>
<!-- 使用组件 -->
<my-component
first="Stencil"
middle="'Don't call me a framework'"
last="JS"
></my-component>
</body>
</html>
- 运行项目
执行 pnpm dev
运行项目即可,会自动打开开发服务器的。
创建组件
这里采用命令的方式创建组件。
- 执行命令
bash
# npm
npm run generate
# pnpm
pnpm run generate
- 填写配置
bash
✔ Component tag name (dash-case): ... app-button
✔ Which additional files do you want to generate? › Stylesheet (.css)
$ stencil generate app-button
The following files have been generated:
- src/components/app-button/app-button.tsx
- src/components/app-button/app-button.css
组件装饰器
装饰器器用于定义组件的元数据,比如组件的标签名、属性、事件、样式,这个是 ts 就支持的用法,用过 nestjs 的应该知道。
stenciljs 可以方便的构建交互式组件,支持以下装饰器:
- Component
- State
- Prop
- Watch
- Method
- Element
- Event
- Listen
@Component 装饰器
@Component
是一个装饰器,它将 TypeScript 类指定为 Stencil
组件。 每个模板组件在构建时都会转换为 Web Component。
ts
import { Component } from '@stencil/core'
@Component({
tag: 'todo-list'
// 附加选项
})
export class TodoList {
// implementation omitted
}
@Component
装饰器参数:
tag
:组件的标签名,必选assetsDirs
:包含组件所需的静态文件(assets)的目录的相对路径数组scoped
:如果为 true,组件将使用 scoped stylesheets。 如果启用了 shadow ,则此选项不能设置为 true。shadow
:如果为 true,组件将使用原生 Shadow DOM 封装。如果浏览器原生不支持 shadow-dom,它将回退到 scoped。styleUrl
:外部样式表的相对 URL,其中包含要应用于组件的样式styleUrls
:外部样式表的相对 url 列表,其中包含要应用于组件的样式。 此外,还可以提供一个对象,将命名的 "mode" 映射到一个或多个样式表。并根据不同的平台或主题来应用不同的样式。
ts
import { Component } from '@stencil/core'
@Component({
tag: 'my-component',
styleUrls: {
ios: 'my-component.ios.scss', // iOS 样式
md: 'my-component.md.scss', // Android/Material Design 样式
dark: 'my-component.dark.css', // 自定义主题
light: 'my-component.light.css' // 自定义主题
}
})
export class MyComponent {
@Prop() mode: string // 可以通过 mode 属性切换样式
}
使用:自定义 key 需要通过组件的 mode
属性来激活
html
<my-component mode="dark"></my-component>
<my-component mode="light"></my-component>
styles
:包含内联 CSS 而不是使用外部样式表的字符串,当使用 styles 时,只允许使用 CSS。
ts
import { Component } from '@stencil/core'
@Component({
tag: 'todo-list',
styles: 'div { background-color: #fff }'
})
export class TodoList {
// implementation omitted
}
@Prop 装饰器
基本使用
这个属性和 vue、react 的 props 一样,用于定义组件的属性。
ts
// 首先, 从 '@stencil/core' 导入 Prop
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'todo-list'
})
export class TodoList {
// 其次, 我们使用 @Prop() 装饰一个类成员
@Prop() name: string
render() {
// 在组件的类中,
// 它的 props 可以通过 `this` 来访问。
// 这允许我们渲染传递给 `todo-list` 的值。
return <div>To-Do List Name: {this.name}</div>
}
}
使用:
html
<!-- jsx中使用 -->
<todo-list name={"Tuesday's To-Do List"}></todo-list>;
<!-- html中使用 -->
<todo-list name="Tuesday's To-Do List"></todo-list>
对于驼峰命名的使用如下:
html
<!-- jsx -->
<todo-list-item thingToDo={"Learn about Stencil Props"}></todo-list-item>
<!-- html -->
<todo-list-item thing-to-do="Learn about Stencil Props"></todo-list-item>
必填的 prop
通过在 prop 名称之后附加 !,Stencil 根据需要标记该 attribute/property 为必填。这样可以确保在 TSX 中使用组件时,将使用该属性:
ts
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'todo-list-item'
})
export class ToDoListItem {
// Note the '!' after the variable name.
@Prop() thingToDo!: string
}
Prop 校验
要校验 Prop, 可以使用 @Watch()
装饰器
@Watch()
装饰器这里就顺带看一下,后续会说详细用法
ts
import { Component, Prop, Watch, h } from '@stencil/core'
@Component({
tag: 'todo-list-item'
})
export class TodoList {
@Prop() thingToDo!: string
@Watch('thingToDo')
validateName(newValue: string, _oldValue: string) {
const isBlank = typeof newValue !== 'string' || newValue === ''
if (isBlank) {
throw new Error('thingToDo is a required property and cannot be empty')
}
const has2chars = typeof newValue === 'string' && newValue.length >= 2
if (!has2chars) {
throw new Error('thingToDo must have a length of more than 1 character')
}
}
}
可变性
默认情况下,Prop 在组件逻辑中是不可变的。一旦用户设置了值,组件就不能在内部更新它。如果需要可变,可以看下面的@Prop() Options
。
这里还是放个例子,设置
@Prop({ mutable: true })
就行
ts
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'todo-list-item'
})
export class ToDoListItem {
@Prop({ mutable: true }) thingToDo: string
componentDidLoad() {
this.thingToDo = 'Ah! A new value!'
}
}
@Prop() Options
@Prop()
装饰器接受一个可选参数来指定特定的选项,以修改组件上 prop 的行为
ts
export interface PropOptions {
attribute?: string
mutable?: boolean
reflect?: boolean
}
- attribute
Properties 和组件 attributes 是强关联的,但不一定是同一件事。attributes 是一个 HTML 概念,但 properties 是 JavaScript 中面向对象编程固有的概念。
通常,property 名与 attribute 名相同,但情况并不总是如此。以下面的组件为例:
ts
import { Component, Prop, h } from '@stencil/core'
import { MyHttpService } from '../some/local/directory/MyHttpService'
@Component({
tag: 'todo-list-item'
})
export class ToDoListItem {
@Prop() isComplete: boolean
@Prop() thingToDo: string
@Prop() httpService: MyHttpService
}
这个组件有 3 properties, 但是编译器只会创建 2 attributes: is-complete 和 thing-to-do.
html
<todo-list-item is-complete="false" thing-to-do="Read Attribute Naming Section of Stencil Docs"></my-cmp>
请注意,httpService
类型不是原始类型(例如,不是 number、boolean 或 string)。由于 DOM 属性只能是字符串,因此使用 "http-service" 作为 DOM 属性是没有意义的。
不能像下面这样通过 HTML 属性来设置 Object 类型的 prop:
html
<todo-list-item http-service="{ get: () => {} }"></todo-list-item>
但是可以通过 js 设置属性:
html
<todo-list-item></todo-list-item>
<script>
document.querySelector('todo-list-item').httpService = {
get: async (url) => {
const res = await fetch(url)
return await res.json()
}
}
</script>
- mutable
默认情况下,Prop 在组件内部是不可变的。然而,通过将 Prop 声明为可变的,可以显式地允许在组件内部改变 Prop,如下面的例子所示:
ts
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'todo-list-item'
})
export class ToDoListItem {
@Prop({ mutable: true }) thingToDo: string
componentDidLoad() {
this.thingToDo = 'Ah! A new value!'
}
}
对于数组或对象的变更如下:
ts
// 错误
this.contents.push('Stencil')
// 正确
this.contents = [...this.contents, 'Stencil']
- reflect
在某些情况下,让 prop 和 attribute 保持同步可能很有用。在这种情况下,你可以将 @Prop() 装饰器中的 reflect 选项设置为 true。 当一个 prop 被反射时,它会作为一个 HTML 属性呈现在 DOM 中。例如:
ts
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'todo-list-item'
})
export class ToDoListItem {
@Prop({ reflect: false }) isComplete: boolean = false
@Prop({ reflect: true }) timesCompletedInPast: number = 2
@Prop({ reflect: true }) thingToDo: string =
'Read Reflect Section of Stencil Docs'
}
上面的组件使用了默认值,可以直接在 html 中使用:
html
<todo-list-item></todo-list-item>
当在 DOM 中渲染时,配置了 reflect: true 的 prop 将作为 HTML 属性在 DOM 中反映:
html
<todo-list-item
times-completed-in-past="2"
thing-to-do="Read Reflect Section of Stencil Docs"
></todo-list-item>
可以通过 document.querySelector('todo-list-item').isComplete
来获取属性。
@State 装饰器
Stencil 提供了一个装饰器,用于在某些类成员更改时触发重新渲染。触发重新渲染的组件的类成员必须使用 Stencil 的@State()装饰器进行装饰,如下所示:
ts
import { Component, h, State } from '@stencil/core'
@Component({
tag: 'app-button',
styleUrl: 'app-button.css',
shadow: true
})
export class AppButton {
@State() count = 0
render() {
return (
<div>
<slot></slot>
<span onClick={() => this.count++}>点击+1</span>
<p>{this.count}</p>
</div>
)
}
}
这个写法和 react 的类组件是一样的,当 count 改变时,组件会重新渲染。
@Watch() 装饰器
@Watch()
是一个应用于 Stencil
组件的方法的装饰器。该装饰器接受一个参数,即用 @Prop()
或 @State()
装饰的类成员的名称。 使用 @Watch()
修饰的方法将在其关联的类成员发生更改时自动运行。
这个内容就看下面的例子吧,写的也比较清楚。可以叠加使用,依赖的数据变化会触发监听回调。
⚠️ 注意:@Watch()
装饰器不会在组件初始加载时触发。
ts
import { Component, Prop, State, Watch, h } from '@stencil/core'
@Component({
tag: 'watch-demo',
styleUrl: 'watch-demo.css',
shadow: true
})
export class WatchDemo {
@Prop({ mutable: true }) num1: number = 0
@State() num2: number = 0
@Watch('num1')
num1Changed(newValue: number, oldValue: number) {
console.log('newValue', newValue, 'oldValue', oldValue)
}
@Watch('num2')
num2Changed(newValue: number, oldValue: number) {
console.log('newValue', newValue, 'oldValue', oldValue)
}
@Watch('num1')
@Watch('num2')
allChanged(newValue: boolean, oldValue: boolean, propName: string) {
// num1变化触发一次,num2变化触发一次,如果num1和num2都变化,则触发两次
console.log('同时监听', newValue, oldValue, propName)
}
render() {
return (
<div>
<button onClick={() => this.num1++}>num1++</button>
<button onClick={() => this.num2++}>num2++</button>
<button
onClick={() => {
this.num1++
this.num2++
}}
>
都变化
</button>
</div>
)
}
}
更新数组或对象:
对于数组,标准的可变数组操作,如 push()
和 unshift()
不会触发组件更新。这些函数会改变数组的内容,但不会改变对数组本身的引用。
为了修改数组,应该使用非可变数组运算符。不可变数组操作符返回一个新数组的副本,可以高效地检测。 这些包括 map()
和 filter()
,以及展开运算符
语法. 由 map()
、filter()
等返回的值应该被分配给正在被 @Prop()
或 @State()
修饰的类成员。
更新对象也是如此。
@Listen 装饰器 和 @Event 装饰器
对于事件的监听,@Listen()
和 @Event()
都是用于监听事件。
@Listen()
Listen()
装饰器用于监听 DOM 事件,包括从 @Event
分发的事件。当组件从 DOM 中添加或删除时,事件监听器会自动添加或删除。
绑定事件一个是通过装饰器,另一个是通过 jsx 本身的
onXXX
来实现。
ts
import { Component, h, Listen } from '@stencil/core'
@Component({
tag: 'event-demo',
styleUrl: 'event-demo.css',
shadow: true
})
export class EventDemo {
@Listen('click')
handleClick(event: MouseEvent) {
console.log('Clicked!', event)
}
handleClick2(event: MouseEvent) {
console.log('Clicked!', event)
}
render() {
return <div onClick={this.handleClick2}>hello</div>
}
}
使用 @Listen()
装饰器,当外部点击该元素的时候,会触发 handleClick()
方法。装饰器给我的使用体验用来定义外部的事件监听更好,内部的还是用 jsx 的onXXX
方式来定义。
外部使用:
html
<todo-list onclick={(ev) => this.someMethod(ev)} /> <todo-list onclick={(ev) =>
this.someOtherMethod(ev)} />
同时 @Listen()
还可以监听自定义事件。例如:@Listen('my-custom-event')
@Event()
@Event()
是一个装饰器,用于定义组件的自定义事件 。它和 @Listen()
不太一样,点击外部元素(web component)的时候,不会自动触发事件监听。
内部通过 this.eventName.emit()
将数据传递给外部,外部通过 event.detail
获取数据。
ts
import { Component, EventEmitter, Event, h } from '@stencil/core'
@Component({
tag: 'event-demo',
styleUrl: 'event-demo.css',
shadow: true
})
export class EventDemo {
@Event({
eventName: 'my-custom-event', // 自定义事件名称,如果不指定,默认使用属性名作为事件名
composed: true, // 事件组合性,设置为 true 时,事件可以从 Shadow DOM 内部传播到外部的 DOM
cancelable: true, // 事件可取消性,监听器可以调用 event.preventDefault() 来阻止事件的默认行为
bubbles: true // 事件冒泡性
})
clickEvent: EventEmitter // eventName,如果@Event的options设置了eventName会覆盖这个内容,但是内部使用的依然是这个值
handleClick() {
// 使用 this.eventName.emit(data) 来触发事件并传递数据
// 通过 emit() 传递的数据会在监听器的 event.detail 中接收到
// 这里需要注意this的绑定问题,在触发的时候需要用.bind(this)去改变当前内部的this指向问题
// 也可以通过 handleClick = () => {},箭头函数的方式就不用考虑.bind(this)
this.clickEvent.emit('@Event触发')
}
render() {
return <div onClick={this.handleClick.bind(this)}>hello</div>
}
}
外部使用:
html
<event-demo></event-demo>
<script>
const eventDemo = document.querySelector('event-demo')
eventDemo.addEventListener('my-custom-event', (event) => {
console.log(event.detail)
})
</script>
@Method
可以方便的导出函数,方便外部调用。
需要注意:通过使用 @Method
装饰器的方法确保公有方法返回一个 Promise。
ts
import { Method, Component } from '@stencil/core'
@Component({
tag: 'todo-list'
})
export class TodoList {
@Method()
async showPrompt() {
// show a prompt
}
}
使用:在尝试调用公共方法之前,应该使用 customElements.whenDefined
方法来确保组件是已经被定义的。
js
;(async () => {
await customElements.whenDefined('event-demo')
const todoListElement = document.querySelector('event-demo')
await todoListElement.showPrompt()
})()
@Element
@Element()
装饰器用于访问类实例中的宿主元素。这将返回一个 HTMLElement
的实例,因此可以在这里使用标准的 DOM 方法/事件。
ts
import { Component, Host, h, Element } from '@stencil/core'
@Component({
tag: 'element-demo',
styleUrl: 'element-demo.css',
shadow: true
})
export class ElementDemo {
@Element() el: HTMLElement
getDomHeight(): number {
// this.el获取到的是元素自身
console.log(this.el)
return this.el.getBoundingClientRect().height
}
render() {
return (
<Host>
<div onClick={this.getDomHeight.bind(this)}>获取dom高度</div>
</Host>
)
}
}
小点
Host 和 Fragment 组件
Host 函数组件可以在 render 函数的根节点使用,为宿主元素本身设置属性和事件监听器。<Host>
可以作为 <Fragment>
嵌入或嵌套组件
由于构建的是 web components 组件,它们天然支持嵌套使用,就和 div、span 一样。
- 提供方
ts
import { Component, Prop, h } from '@stencil/core'
@Component({
tag: 'my-embedded-component'
})
export class MyEmbeddedComponent {
@Prop() color: string = 'blue'
render() {
return <div>My favorite color is {this.color}</div>
}
}
- 使用方
ts
import { Component, h } from '@stencil/core'
@Component({
tag: 'my-parent-component'
})
export class MyParentComponent {
render() {
return (
<div>
<my-embedded-component color='red'></my-embedded-component>
</div>
)
}
}
jsx
插槽
大部分语法和 react 的 jsx 是一样的,同时该框架也支持插槽写法,其实这个是 web components 的一个特性。和 vue 的<slot>
一样
innerHTML
vue 的v-html
,react 的dangerouslySetInnerHTML
html
<div innerHTML="{svgContent}"></div>
Styling Components 组件样式
使用 Shadow DOM 进行样式设置
启用 Shadow DOM 后,阴影根内的元素是作用域化的,组件外部的样式不会应用。因此,组件内部的 CSS 选择器可以简化,因为它们只会应用到组件内的元素。我们不必包含任何特定的选择器来将样式作用域限定到组件。
:host
伪类选择器用于选择组件的Host
元素
css
:host {
color: black;
}
div {
background: blue;
}
Shadow DOM 选择器
在使用 Shadow DOM 时,如果你想要查询 web 组件内的元素,你必须首先使用 @Element 装饰器来获取宿主元素,然后你可以使用 shadowRoot 属性来执行查询。
js
this.el.shadowRoot.querySelector('xxx')
全局样式
要使全局样式对项目中的所有组件可用,stencil.config.ts 文件带有一个可选的 globalStyle 设置,它接受全局样式表的路径。
ts
export const config: Config = {
namespace: 'app',
globalStyle: 'src/global/global.css',
outputTarget: [
{
type: 'www'
}
]
}
函数式组件
这个的话和 react 是一样的,只是 react 的children
属性是在 props 中的,而 stencil 的children
属性函数的第二个参数中。
ts
const Hello = (props) => <h1>Hello, {props.name}!</h1>
// children
const Hello = (props, children) => [<h1>Hello, {props.name}</h1>, children]
// ts函数组件类型 FunctionalComponent
import { FunctionalComponent, h } from '@stencil/core'
interface HelloProps {
name: string
}
export const Hello: FunctionalComponent<HelloProps> = ({ name }) => (
<h1>Hello, {name}!</h1>
)
处理子组件:
函数组件的第二个参数接收传递的子组件,但为了使用它们,FunctionalComponent
提供了一个 utils
对象, 该对象暴露了一个 map()
方法来转换子组件,以及一个 forEach()
方法来读取它们。 不建议读取 children
数组,因为 stencil 编译器可能会在 prod 模式下重命名 vNode 属性。
例如:
ts
export const AddClass: FunctionalComponent = (_, children, utils) =>
utils.map(children, (child) => ({
...child,
vattrs: {
...child.vattrs,
class: `${child.vattrs.class} add-class`
}
}))
ts 类型:
ts
export interface FunctionalUtilities {
forEach: (
children: VNode[],
cb: (vnode: ChildNode, index: number, array: ChildNode[]) => void
) => void
map: (
children: VNode[],
cb: (vnode: ChildNode, index: number, array: ChildNode[]) => ChildNode
) => VNode[]
}
export interface ChildNode {
vtag?: string | number | Function
vkey?: string | number
vtext?: string
vchildren?: VNode[]
vattrs?: any
vname?: string
}
组件生命周期方法
Stencil.js 的生命周期方法可按组件 "挂载 - 更新 - 卸载" 三大阶段划分,部分方法还遵循自定义元素规范,各方法功能与使用场景明确:
组件挂载阶段(首次连接 DOM 至首次渲染完成)
该阶段仅在组件第一次被添加到 DOM 时执行一次(除 connectedCallback 可能多次调用外),主要用于初始化数据、加载异步资源等。
方法名 | 触发时机 | 核心作用 | 关键特性 |
---|---|---|---|
connectedCallback |
组件每次连接到 DOM 时(包括首次挂载、从 DOM 移除后重新挂载) | 执行 DOM 连接相关逻辑(如启动定时器、绑定事件) | 1. 遵循自定义元素规范; 2. 可多次调用(如组件被移动到 DOM 其他位置时); 3. 首次调用时在 componentWillLoad 之前执行 |
componentWillLoad |
组件首次连接到 DOM 后,仅执行一次 | 异步加载数据、初始化状态(不触发额外重渲染) | 1. 可返回 Promise,等待异步任务完成后再执行首次 render ; 2. 子组件的该方法先于父组件执行 |
componentDidLoad |
组件首次 render 完成后,仅执行一次 |
访问首次渲染后的 DOM 元素、初始化第三方库 | 1. 子组件的该方法先于父组件执行("从深到浅"冒泡); 2. 此时组件已完成首次渲染,可安全操作 DOM |
组件更新阶段(Prop/State 变化触发重渲染)
当组件的 Prop
(外部传入属性)或 State
(内部状态)发生变化时,触发该阶段,用于控制重渲染逻辑、处理更新前后的操作。
方法名 | 触发时机 | 核心作用 | 关键特性 |
---|---|---|---|
componentShouldUpdate |
Prop/State 变化后、重渲染前 | 决定组件是否需要重渲染 | 1. 接收 3 个参数:新值、旧值、变化的属性名; 2. 返回 true (需要重渲染)或 false (跳过重渲染); 3. 若多个属性同步变化,仅第一个触发重渲染时执行(后续跳过); 4. 不建议用于监听属性变化 ,优先用 @Watch 装饰器 |
componentWillUpdate |
Prop/State 变化后、重渲染前(首次渲染不触发) | 重渲染前的准备工作(如处理数据格式) | 1. 可返回 Promise,等待任务完成后再渲染; 2. 仅在组件需要更新时执行(受 componentShouldUpdate 结果影响) |
componentWillRender |
每次 render 前(包括首次渲染和更新渲染) |
调整渲染相关状态(避免额外重渲染) | 1. 可返回 Promise,延迟渲染; 2. 推荐在此处更新渲染状态 ,若在 componentDidLoad /componentDidUpdate 中更新,会触发额外渲染,影响性能 |
render |
组件需要渲染时(首次或更新) | 定义组件的 DOM 结构(JSX 模板) | 1. 核心渲染方法,返回 JSX 元素; 2. 首次渲染由 componentWillLoad 触发,更新渲染由 Prop/State 变化触发 |
componentDidRender |
每次 render 后(包括首次和更新) |
访问渲染后的 DOM 元素(如计算 DOM 尺寸) | 1. 若在此处更新状态,需做"脏检查"(判断数据是否真的变化),否则可能陷入无限循环 |
componentDidUpdate |
组件更新渲染后(首次渲染不触发) | 处理更新后的逻辑(如同步数据到父组件) | 1. 若在此处更新状态,必须做"脏检查",避免无限重渲染; 2. 仅在组件实际完成更新时执行 |
@Watch('propName') |
指定的 Prop/State 变化时 | 监听特定属性的变化并执行逻辑 | 1. 装饰器语法,需指定监听的属性名(如 @Watch('userName') ); 2. 每次属性变化都会触发,不受重渲染逻辑影响,是监听属性变化的最佳选择 |
组件卸载阶段(从 DOM 移除)
该阶段在组件离开 DOM 时执行,用于清理资源,避免内存泄漏。
方法名 | 触发时机 | 核心作用 | 关键特性 |
---|---|---|---|
disconnectedCallback |
组件每次从 DOM 断开连接时(包括暂时移除) | 清理资源(如清除定时器、解绑事件) | 1. 遵循自定义元素规范; 2. 可多次调用(如组件被移除后重新挂载,会先触发此方法); 3. 不可与"销毁"事件混淆,仅表示组件与 DOM 断开,非彻底销毁 |
生命周期执行顺序规则
Stencil.js 生命周期方法的执行顺序严格遵循"子组件优先"和"异步兼容"原则,即使组件异步加载,顺序也不会错乱。
首次挂载与渲染(父子组件层级)
以 cmp-a > cmp-b > cmp-c
(父-子-孙)的组件层级为例,执行顺序如下:
cmp-a
-connectedCallback
(首次连接 DOM)cmp-a
-componentWillLoad
cmp-b
-connectedCallback
cmp-b
-componentWillLoad
cmp-c
-connectedCallback
cmp-c
-componentWillLoad
cmp-c
-componentWillRender
cmp-c
-render
cmp-c
-componentDidRender
cmp-c
-componentDidLoad
(孙组件加载完成)cmp-b
-componentWillRender
cmp-b
-render
cmp-b
-componentDidRender
cmp-b
-componentDidLoad
(子组件加载完成)cmp-a
-componentWillRender
cmp-a
-render
cmp-a
-componentDidRender
cmp-a
-componentDidLoad
(父组件加载完成)
核心规律:子组件的"加载前"和"加载完成"方法先于父组件执行(从深到浅),确保父组件等待所有子组件准备就绪后再标记自身"加载完成"。
组件更新(Prop/State 变化)
当某组件的属性变化时,执行顺序如下:
- 触发
@Watch('propName')
(若监听该属性) - 执行
componentShouldUpdate
(判断是否更新,返回true
则继续) - 执行
componentWillUpdate
(更新前准备) - 执行
componentWillRender
(渲染前状态调整) - 执行
render
(重新渲染 DOM) - 执行
componentDidRender
(渲染后 DOM 操作) - 执行
componentDidUpdate
(更新后逻辑处理)
组件卸载与重新挂载
- 卸载 :组件从 DOM 移除时,触发
disconnectedCallback
(清理资源)。 - 重新挂载 :组件再次添加到 DOM 时:
- 触发
connectedCallback
(再次执行连接逻辑) - 不触发
componentWillLoad
和componentDidLoad
(仅首次挂载执行) - 直接进入更新阶段(执行
componentWillRender
→render
等)
- 触发
关键注意事项与最佳实践
-
避免无限重渲染
若在
componentDidUpdate
或componentDidRender
中更新State
,必须先做"脏检查"(对比新值与旧值是否不同),例如:tscomponentDidUpdate() { if (this.newData !== this.oldData) { // 脏检查 this.oldData = this.newData; this.setState({ data: this.newData }); } }
-
优先使用
@Watch
监听属性变化
componentShouldUpdate
仅用于控制重渲染,且可能因"多属性同步变化"跳过部分调用,监听属性变化推荐用@Watch
,例如:ts@Watch('userAge') onUserAgeChange(newAge: number, oldAge: number) { console.log(`年龄从 ${oldAge} 变为 ${newAge}`); }
-
异步任务用
componentWillLoad
处理首次加载的异步数据(如接口请求)建议在
componentWillLoad
中处理,并返回 Promise,确保父组件等待数据加载完成后再渲染:tsasync componentWillLoad() { const response = await fetch('/user-info.json'); this.userInfo = await response.json(); }
-
渲染状态更新在
componentWillRender
中执行若需调整渲染相关的状态(如格式化显示文本),优先在
componentWillRender
中处理,避免在componentDidLoad
中更新导致额外渲染:tscomponentWillRender() { this.formattedTime = new Date(this.time).toLocaleTimeString(); // 渲染前格式化 }
-
connectedCallback
与disconnectedCallback
配对使用若在
connectedCallback
中启动资源(如定时器、事件监听),必须在disconnectedCallback
中清理,避免内存泄漏:tsconnectedCallback() { this.timer = setInterval(() => { /* 逻辑 */ }, 1000); } disconnectedCallback() { clearInterval(this.timer); // 清理定时器 }