普通视图
AI时代,金融科技如何落地“对话就能办业务”?
第3章:基础组件 —— 3.1 文本及样式
第2章:第一个Flutter应用 —— 2.8 Flutter异常捕获
nuxt2.x部署到linux
前端使用 docx-preview 实现word解析实战
从零理解React Context:神奇的上下文机制
this函数的指向问题
第2章:第一个Flutter应用 —— 2.7 调试Flutter应用
一文讲透鸿蒙开发应用框架体系
鸿蒙应用程序框架开发快速指南
Stage模型提供了一套完整的应用框架体系:
| 组件 | 作用 | 使用场景 |
|---|---|---|
| UIAbility | UI组件 | 用户交互界面 |
| ExtensionAbility | 扩展组件 | 卡片、输入法等特定场景 |
| AbilityStage | 组件管理器 | Module初始化 |
| Context | 上下文 | 获取资源和能力 |
| Worker/TaskPool | 多线程 | 耗时操作 |
通过本文的学习,您应该能够:
- ✅ 理解Stage模型的核心概念
- ✅ 掌握UIAbility生命周期管理
- ✅ 了解不同启动模式的使用场景
- ✅ 熟练使用Context获取资源和能力
- ✅ 掌握进程和线程模型
- ✅ 能够开发完整的HarmonyOS应用
一、Stage模型概述
1.1 什么是Stage模型
Stage模型是HarmonyOS提供的应用模型,它采用面向对象的开发方式,支持应用组件级的跨端迁移和多端协同。Stage模型的核心概念包括:
- UIAbility组件: 包含UI的应用组件,用于与用户交互
- ExtensionAbility组件: 面向特定场景的扩展组件
- AbilityStage: Module级别的组件管理器
- WindowStage: 应用进程内的窗口管理器
- Context: 应用上下文,提供运行时资源和能力
graph TB
A[应用App] --> B[Module 1]
A --> C[Module 2]
B --> D[AbilityStage]
D --> E[UIAbility 1]
D --> F[UIAbility 2]
D --> G[ExtensionAbility]
E --> H[WindowStage]
H --> I[UI页面]
1.2 配置声明
在module.json5中声明UIAbility:
{
"module": {
"name": "entry",
"type": "entry",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"launchType": "singleton"
}
]
}
}
二、UIAbility组件
2.1 UIAbility生命周期
UIAbility的核心生命周期包括: onCreate、onForeground、onBackground、onDestroy。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
// 1. 创建时触发,只执行一次
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('Ability onCreate');
// 执行初始化业务逻辑
}
// 2. WindowStage创建后触发
onWindowStageCreate(windowStage: window.WindowStage): void {
console.info('Ability onWindowStageCreate');
// 订阅WindowStage事件
windowStage.on('windowStageEvent', (data) => {
let stageEventType: window.WindowStageEventType = data;
switch (stageEventType) {
case window.WindowStageEventType.SHOWN:
console.info('windowStage foreground');
break;
case window.WindowStageEventType.HIDDEN:
console.info('windowStage background');
break;
}
});
// 加载UI页面
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error(`Failed to load content. Code: ${err.code}`);
return;
}
console.info('Succeeded in loading content');
});
}
// 3. 切换到前台时触发
onForeground(): void {
console.info('Ability onForeground');
// 申请系统资源
}
// 4. 切换到后台时触发
onBackground(): void {
console.info('Ability onBackground');
// 释放UI不可见时的资源
}
// 5. WindowStage即将销毁时触发
onWindowStageWillDestroy(windowStage: window.WindowStage): void {
console.info('Ability onWindowStageWillDestroy');
// 释放WindowStage资源
windowStage.off('windowStageEvent');
}
// 6. WindowStage销毁后触发
onWindowStageDestroy(): void {
console.info('Ability onWindowStageDestroy');
// 释放UI资源
}
// 7. 销毁时触发
onDestroy(): void {
console.info('Ability onDestroy');
// 释放系统资源、保存数据
}
// 8. 再次启动时触发(singleton模式)
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('Ability onNewWant');
// 更新数据
}
}
2.2 UIAbility生命周期流程图
sequenceDiagram
participant User as 用户
participant System as 系统
participant Ability as UIAbility
participant Window as WindowStage
User->>System: 启动应用
System->>Ability: onCreate()
System->>Window: 创建WindowStage
Window->>Ability: onWindowStageCreate()
Ability->>Window: loadContent(页面)
System->>Ability: onForeground()
Note over Ability: UIAbility进入前台
User->>System: 切换到其他应用
System->>Ability: onBackground()
Note over Ability: UIAbility进入后台
User->>System: 再次打开应用
System->>Ability: onNewWant()
System->>Ability: onForeground()
User->>System: 关闭应用
System->>Ability: onBackground()
System->>Ability: onWindowStageWillDestroy()
Ability->>Window: off('windowStageEvent')
System->>Ability: onWindowStageDestroy()
System->>Ability: onDestroy()
2.3 UIAbility启动模式
2.3.1 singleton(单实例模式)
系统中只存在唯一一个该UIAbility实例:
{
"module": {
"abilities": [
{
"launchType": "singleton"
}
]
}
}
2.3.2 multiton(多实例模式)
每次启动都创建新的实例:
{
"module": {
"abilities": [
{
"launchType": "multiton"
}
]
}
}
2.3.3 specified(指定实例模式)
根据业务逻辑动态决定创建新实例或复用已有实例:
{
"module": {
"abilities": [
{
"launchType": "specified"
}
]
}
}
在AbilityStage中实现onAcceptWant():
import { AbilityStage, Want } from '@kit.AbilityKit';
export default class MyAbilityStage extends AbilityStage {
onAcceptWant(want: Want): string {
if (want.abilityName === 'SpecifiedAbility') {
if (want.parameters) {
// 返回自定义标识
return `SpecifiedAbilityInstance_${want.parameters.instanceKey}`;
}
}
return 'MyAbilityStage';
}
}
启动specified模式的UIAbility:
import { common, Want } from '@kit.AbilityKit';
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let want: Want = {
deviceId: '',
bundleName: 'com.example.myapp',
abilityName: 'SpecifiedAbility',
moduleName: 'entry',
parameters: {
instanceKey: 'document_001' // 自定义标识
}
};
context.startAbility(want);
三、ExtensionAbility组件
ExtensionAbility是面向特定场景的应用组件,主要类型包括:
| ExtensionAbility类型 | 功能描述 | 允许三方实现 |
|---|---|---|
| FormExtensionAbility | 卡片扩展 | 是 |
| WorkSchedulerExtensionAbility | 延时任务 | 是 |
| InputMethodExtensionAbility | 输入法 | 是 |
| AccessibilityExtensionAbility | 无障碍服务 | 是 |
| BackupExtensionAbility | 数据备份 | 是 |
| ShareExtensionAbility | 分享扩展 | 是 |
3.1 FormExtensionAbility示例
import { FormExtensionAbility, formBindingData } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
export default class MyFormExtensionAbility extends FormExtensionAbility {
// 卡片创建
onAddForm(want: Want) {
console.info('FormExtensionAbility onAddForm');
let dataObj: Record<string, string> = {
'temperature': '25°C',
'time': '12:00'
};
let obj = formBindingData.createFormBindingData(dataObj);
return obj;
}
// 卡片更新
onUpdateForm(formId: string) {
console.info('FormExtensionAbility onUpdateForm');
// 更新卡片数据
}
// 卡片删除
onRemoveForm(formId: string) {
console.info('FormExtensionAbility onRemoveForm');
}
}
四、Context使用
4.1 Context类型对比
| Context类型 | 说明 | 主要能力 |
|---|---|---|
| ApplicationContext | 应用全局上下文 | 获取应用信息、监听前后台变化 |
| AbilityStageContext | 模块级别上下文 | 获取模块信息和路径 |
| UIAbilityContext | UIAbility组件上下文 | 启动Ability、连接服务 |
| ExtensionContext | ExtensionAbility组件上下文 | 特定场景能力 |
4.2 获取Context
4.2.1 获取ApplicationContext
import { UIAbility } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate() {
let applicationContext = this.context.getApplicationContext();
console.info(`Application bundleName: ${applicationContext.applicationInfo.name}`);
}
}
4.2.2 在页面中获取UIAbilityContext
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
build() {
Column() {
Button('启动另一个Ability')
.onClick(() => {
this.context.startAbility({
bundleName: 'com.example.myapp',
abilityName: 'SecondAbility'
});
})
}
}
}
4.3 获取应用文件路径
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
@Entry
@Component
struct Index {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
createFile() {
let applicationContext = this.context.getApplicationContext();
// 获取应用缓存目录
let cacheDir = applicationContext.cacheDir;
// 获取应用文件目录
let filesDir = applicationContext.filesDir;
// 创建并写入文件
let file = fileIo.openSync(
filesDir + '/data.txt',
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE
);
fileIo.writeSync(file.fd, 'Hello HarmonyOS');
fileIo.closeSync(file);
console.info(`File created at: ${filesDir}/data.txt`);
}
build() {
Column() {
Button('创建文件')
.onClick(() => this.createFile())
}
}
}
4.4 监听应用前后台变化
import { UIAbility, ApplicationStateChangeCallback } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate() {
let applicationStateChangeCallback: ApplicationStateChangeCallback = {
onApplicationForeground() {
console.info('Application moved to foreground');
},
onApplicationBackground() {
console.info('Application moved to background');
}
};
let applicationContext = this.context.getApplicationContext();
applicationContext.on('applicationStateChange', applicationStateChangeCallback);
}
}
五、进程与线程模型
5.1 进程模型
应用运行时可能包含以下进程类型:
graph TB
A[应用App] --> B[主进程]
A --> C[ExtensionAbility进程]
A --> D[Render进程]
A --> E[子进程可选]
B --> B1[UIAbility1]
B --> B2[UIAbility2]
B --> B3[ServiceExtension系统]
C --> C1[FormExtensionAbility]
C --> C2[ShareExtensionAbility]
D --> D1[Web组件渲染]
进程类型说明:
- 主进程: 所有UIAbility默认运行在主进程中
- ExtensionAbility进程: 同类型的ExtensionAbility运行在独立进程
- Render进程: Web组件的渲染进程
- 子进程: 开发者可通过childProcessManager创建
5.2 线程模型
// 主线程中启动Worker
import { worker } from '@kit.ArkTS';
// 创建Worker线程
const workerInstance = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
// 发送消息到Worker
workerInstance.postMessage({ type: 'start', data: 'Hello' });
// 接收Worker消息
workerInstance.onmessage = (message) => {
console.info(`Received from worker: ${message.data}`);
};
// 使用TaskPool
import { taskpool } from '@kit.ArkTS';
@Concurrent
function computeTask(data: number): number {
// 耗时计算
return data * data;
}
// 提交任务到TaskPool
taskpool.execute(computeTask, 100).then((result) => {
console.info(`Task result: ${result}`);
});
Worker.ets (workers目录下):
import { worker, MessageEvents, ErrorEvent } from '@kit.ArkTS';
const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents) => {
console.info(`Worker received: ${JSON.stringify(e.data)}`);
// 执行耗时操作
const result = heavyComputation(e.data);
// 发送结果回主线程
workerPort.postMessage({ result });
};
function heavyComputation(data: any): any {
// 执行复杂计算
return data;
}
六、实战示例:待办事项应用
6.1 应用架构
TodoApp/
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets
│ │ ├── pages/
│ │ │ ├── Index.ets (待办列表)
│ │ │ └── Detail.ets (待办详情)
│ │ └── model/
│ │ └── TodoModel.ets (数据模型)
│ └── module.json5
6.2 EntryAbility实现
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { preferences } from '@kit.ArkData';
export default class EntryAbility extends UIAbility {
private dataStore: preferences.Preferences | null = null;
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
console.info('EntryAbility onCreate');
// 初始化数据存储
let context = this.context;
let dataPreferences = preferences.getPreferencesSync(
context,
{ name: 'TodoDataStore' }
);
this.dataStore = dataPreferences;
// 存储到AppStorage供全局访问
AppStorage.setOrCreate('dataStore', this.dataStore);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error(`Failed to load content. Code: ${err.code}`);
return;
}
console.info('Succeeded in loading content');
});
}
}
6.3 TodoModel数据模型
import { preferences } from '@kit.ArkData';
export interface TodoItem {
id: string;
title: string;
content: string;
completed: boolean;
createTime: number;
}
export class TodoModel {
private dataStore: preferences.Preferences;
constructor(dataStore: preferences.Preferences) {
this.dataStore = dataStore;
}
// 获取所有待办
async getAllTodos(): Promise<TodoItem[]> {
let todosStr = this.dataStore.getSync('todos', '[]') as string;
return JSON.parse(todosStr);
}
// 添加待办
async addTodo(todo: TodoItem): Promise<void> {
let todos = await this.getAllTodos();
todos.push(todo);
this.dataStore.putSync('todos', JSON.stringify(todos));
await this.dataStore.flush();
}
// 更新待办
async updateTodo(id: string, updates: Partial<TodoItem>): Promise<void> {
let todos = await this.getAllTodos();
let index = todos.findIndex(t => t.id === id);
if (index !== -1) {
todos[index] = { ...todos[index], ...updates };
this.dataStore.putSync('todos', JSON.stringify(todos));
await this.dataStore.flush();
}
}
// 删除待办
async deleteTodo(id: string): Promise<void> {
let todos = await this.getAllTodos();
todos = todos.filter(t => t.id !== id);
this.dataStore.putSync('todos', JSON.stringify(todos));
await this.dataStore.flush();
}
}
6.4 Index页面(待办列表)
import { router } from '@kit.ArkUI';
import { preferences } from '@kit.ArkData';
import { TodoItem, TodoModel } from '../model/TodoModel';
@Entry
@Component
struct Index {
@State todoList: TodoItem[] = [];
@State inputTitle: string = '';
private todoModel: TodoModel | null = null;
aboutToAppear() {
// 获取数据存储实例
let dataStore = AppStorage.get('dataStore') as preferences.Preferences;
this.todoModel = new TodoModel(dataStore);
this.loadTodos();
}
async loadTodos() {
if (this.todoModel) {
this.todoList = await this.todoModel.getAllTodos();
}
}
async addTodo() {
if (this.inputTitle.trim() === '') {
return;
}
let newTodo: TodoItem = {
id: Date.now().toString(),
title: this.inputTitle,
content: '',
completed: false,
createTime: Date.now()
};
if (this.todoModel) {
await this.todoModel.addTodo(newTodo);
await this.loadTodos();
}
this.inputTitle = '';
}
async toggleTodo(id: string, completed: boolean) {
if (this.todoModel) {
await this.todoModel.updateTodo(id, { completed: !completed });
await this.loadTodos();
}
}
async deleteTodo(id: string) {
if (this.todoModel) {
await this.todoModel.deleteTodo(id);
await this.loadTodos();
}
}
build() {
Column({ space: 15 }) {
// 标题栏
Text('我的待办')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 20, top: 20 })
// 输入区域
Row({ space: 10 }) {
TextInput({ placeholder: '添加新待办', text: this.inputTitle })
.layoutWeight(1)
.onChange((value: string) => {
this.inputTitle = value;
})
Button('添加')
.onClick(() => this.addTodo())
}
.width('90%')
.padding(10)
// 统计信息
Row() {
Text(`总计: ${this.todoList.length}`)
.fontSize(14)
.fontColor('#666')
Text(`已完成: ${this.todoList.filter(t => t.completed).length}`)
.fontSize(14)
.fontColor('#666')
.margin({ left: 20 })
}
.width('90%')
// 待办列表
List({ space: 10 }) {
ForEach(this.todoList, (todo: TodoItem) => {
ListItem() {
Row({ space: 10 }) {
Checkbox({ name: todo.id, group: 'todoGroup' })
.select(todo.completed)
.onChange((checked: boolean) => {
this.toggleTodo(todo.id, todo.completed);
})
Column({ space: 5 }) {
Text(todo.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.decoration({
type: todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None
})
.opacity(todo.completed ? 0.5 : 1)
Text(new Date(todo.createTime).toLocaleString())
.fontSize(12)
.fontColor('#999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Button('详情')
.fontSize(14)
.onClick(() => {
router.pushUrl({
url: 'pages/Detail',
params: { todoId: todo.id }
});
})
Button('删除')
.fontSize(14)
.backgroundColor('#ff6b6b')
.onClick(() => {
this.deleteTodo(todo.id);
})
}
.width('100%')
.padding(15)
.backgroundColor('#f5f5f5')
.borderRadius(10)
}
}, (todo: TodoItem) => todo.id)
}
.width('90%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#fff')
}
}
6.5 Detail页面(待办详情)
import { router } from '@kit.ArkUI';
import { preferences } from '@kit.ArkData';
import { TodoItem, TodoModel } from '../model/TodoModel';
@Entry
@Component
struct Detail {
@State todo: TodoItem | null = null;
@State editTitle: string = '';
@State editContent: string = '';
private todoModel: TodoModel | null = null;
private todoId: string = '';
aboutToAppear() {
// 获取传递的参数
let params = router.getParams() as Record<string, string>;
this.todoId = params.todoId;
// 初始化数据模型
let dataStore = AppStorage.get('dataStore') as preferences.Preferences;
this.todoModel = new TodoModel(dataStore);
this.loadTodoDetail();
}
async loadTodoDetail() {
if (this.todoModel) {
let todos = await this.todoModel.getAllTodos();
this.todo = todos.find(t => t.id === this.todoId) || null;
if (this.todo) {
this.editTitle = this.todo.title;
this.editContent = this.todo.content;
}
}
}
async saveTodo() {
if (this.todoModel && this.todo) {
await this.todoModel.updateTodo(this.todoId, {
title: this.editTitle,
content: this.editContent
});
router.back();
}
}
build() {
Column({ space: 20 }) {
// 顶部导航
Row() {
Button('返回')
.onClick(() => router.back())
Text('待办详情')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Button('保存')
.onClick(() => this.saveTodo())
}
.width('100%')
.padding(15)
.backgroundColor('#f0f0f0')
// 编辑区域
Column({ space: 15 }) {
Column({ space: 5 }) {
Text('标题')
.fontSize(14)
.fontColor('#666')
TextInput({ text: this.editTitle })
.onChange((value: string) => {
this.editTitle = value;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Column({ space: 5 }) {
Text('内容')
.fontSize(14)
.fontColor('#666')
TextArea({ text: this.editContent })
.height(200)
.onChange((value: string) => {
this.editContent = value;
})
}
.alignItems(HorizontalAlign.Start)
.width('100%')
if (this.todo) {
Column({ space: 5 }) {
Text(`创建时间: ${new Date(this.todo.createTime).toLocaleString()}`)
.fontSize(12)
.fontColor('#999')
Text(`状态: ${this.todo.completed ? '已完成' : '未完成'}`)
.fontSize(12)
.fontColor(this.todo.completed ? '#4caf50' : '#ff9800')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
}
.width('90%')
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor('#fff')
}
}
七、最佳实践
7.1 生命周期管理
- onCreate(): 只执行一次初始化操作
- onForeground/onBackground: 及时申请和释放资源
- 避免在onBackground()中执行耗时操作
- 使用onDestroy()释放资源
7.2 Context使用建议
- 不要缓存Context: 避免内存泄漏
- 使用正确的Context类型: 不要强制转换
- 及时释放资源: 注销事件监听
7.3 多线程开发
- UI操作只在主线程执行
- 耗时任务使用TaskPool或Worker
- TaskPool适合短期任务,Worker适合长期任务
7.4 进程管理
- 合理配置启动模式: 根据业务选择singleton/multiton/specified
- 注意进程间隔离: 不同进程数据不共享
- 控制进程数量: 避免创建过多进程
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
在真实的应用中,我们通常需要在多个页面之间跳转,比如从首页跳转到详情页、从列表页跳转到设置页等。Flutter 提供了强大的导航系统 Navigator 来管理页面之间的跳转。
本篇教程将带你全面掌握 Navigator 1.0 的核心用法,包括最基础的 push/pop 操作以及更规范的命名路由。
Flutter 将应用中的页面(Screen 或 Page)视为路由(Route) ,并使用一个栈(Stack) 来管理这些路由。Navigator Widget 就是这个路由栈的管理者。
-
基本原理:
-
Navigator.push(): 将一个新的路由(页面)推入 (push) 导航栈的顶部,用户会看到新页面。 -
Navigator.pop(): 将导航栈顶部的路由弹出 (pop),用户会返回到上一个页面。 - 这个“后进先出”(LIFO)的栈结构,完美契合了移动应用中页面跳转和返回的常见模式。
-
学习目标
- 掌握
Navigator.push()和Navigator.pop()的基本用法,实现页面间的跳转和返回。 - 学会如何在页面跳转时传递数据,以及如何在上一个页面接收返回的数据。
- 理解并掌握命名路由 (Named Routes) 的概念和优势。
- 学会使用
onGenerateRoute来处理带参数的命名路由。
1. 基础导航:push 与 pop
这是最直接、最基础的页面跳转方式。我们通常会用 MaterialPageRoute 来包裹我们的页面 Widget,因为它提供了平台自适应的页面切换动画。
Navigator.push():跳转到新页面
要跳转到一个新页面,你需要调用 Navigator.push(),并提供当前的 BuildContext 和一个 Route 对象(通常是 MaterialPageRoute)。
Navigator.pop():返回上一页
要返回,只需调用 Navigator.pop()。Flutter 的 AppBar 会自动添加一个返回按钮,它的功能就是调用 Navigator.pop()。
数据传递
- 向前传递数据:在创建新页面的 Widget 时,通过其构造函数传递数据。
-
向后返回数据:
Navigator.pop()方法可以接受一个可选的参数,作为这个页面的返回值。同时,Navigator.push()方法会返回一个Future,这个Future会在页面被 pop 时完成,并携带返回值。我们可以使用await来接收它。
代码示例:一个完整的数据传递流程
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: FirstScreen(),
);
}
}
// 第一个页面
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
Future<void> _navigateAndDisplaySelection(BuildContext context) async {
// 1. 使用 await 等待 SecondScreen 返回结果
final result = await Navigator.push(
context,
MaterialPageRoute(
// 2. 通过构造函数向前传递数据
builder: (context) => const SecondScreen(data: 'Hello from FirstScreen!'),
),
);
// 5. 接收到返回的数据后,显示一个 SnackBar
if (context.mounted && result != null) {
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('$result')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Screen')),
body: Center(
child: ElevatedButton(
onPressed: () => _navigateAndDisplaySelection(context),
child: const Text('Go to Second Screen'),
),
),
);
}
}
// 第二个页面
class SecondScreen extends StatelessWidget {
final String data;
// 构造函数接收数据
const SecondScreen({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Received data: $data'), // 显示接收到的数据
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 3. Pop 并返回数据 "Yes!"
Navigator.pop(context, 'Yes!');
},
child: const Text('Go back with "Yes!"'),
),
ElevatedButton(
onPressed: () {
// 4. Pop 并返回数据 "No."
Navigator.pop(context, 'No.');
},
child: const Text('Go back with "No."'),
),
],
),
),
);
}
}
2. 命名路由 (Named Routes)
当应用变得复杂,页面众多时,在每个跳转的地方都写一遍 MaterialPageRoute(builder: ...) 会让代码变得混乱且难以管理。命名路由就是为了解决这个问题而生的。
核心思想:事先给每个页面(路由)起一个独一无二的名字(通常是字符串,如 '/home' 或 '/product/details'),然后将这些名字和对应的页面 Widget 在一个中心位置(MaterialApp)注册。之后,只需要通过名字就可以进行跳转。
优点:
-
代码整洁:
Navigator.pushNamed(context, '/details')比Navigator.push(context, MaterialPageRoute(...))简洁得多。 -
集中管理:所有路由都在
MaterialApp中定义,一目了然,方便维护。 - 解耦:发起跳转的页面不需要知道目标页面的具体实现。
如何实现?
- 在
MaterialApp中定义initialRoute(初始路由) 和routes(路由表)。 - 使用
Navigator.pushNamed(context, routeName)来进行跳转。
如何通过命名路由传递参数?
routes 表定义的 WidgetBuilder 是无参数的,这使得直接传递数据变得困难。为了解决这个问题,我们需要使用更强大的 onGenerateRoute。
-
onGenerateRoute: 这是MaterialApp的一个回调函数。当pushNamed一个未在routes表中注册的路由时,Flutter 会调用onGenerateRoute。这给了我们一个拦截路由、解析参数并创建页面的机会。
流程:
- 在
pushNamed时,通过arguments参数传递数据:Navigator.pushNamed(context, '/details', arguments: product)。 - 在
onGenerateRoute回调中,通过settings.arguments获取传递过来的数据。 - 根据路由名和参数,手动创建
MaterialPageRoute并返回。
代码示例:使用命名路由和 onGenerateRoute
import 'package:flutter/material.dart';
// 一个简单的数据模型
class Product {
final String title;
final String description;
const Product(this.title, this.description);
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// 1. 定义路由生成规则
onGenerateRoute: (settings) {
// 如果是详情页路由 ('/details')
if (settings.name == ProductDetailsScreen.routeName) {
// 提取参数
final args = settings.arguments as Product;
// 创建并返回 MaterialPageRoute
return MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(product: args);
},
);
}
// 可选:断言确保所有路由都被处理
assert(false, 'Need to implement ${settings.name}');
return null;
},
// 设置首页
home: const HomeScreen(),
);
}
}
// 首页:显示一个产品列表
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final products = List.generate(
20,
(i) => Product('Product ${i + 1}', 'This is the description for product ${i + 1}'),
);
return Scaffold(
appBar: AppBar(title: const Text('Product List')),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(products[index].title),
onTap: () {
// 2. 使用 pushNamed 并通过 arguments 传递数据
Navigator.pushNamed(
context,
ProductDetailsScreen.routeName,
arguments: products[index],
);
},
);
},
),
);
}
}
// 产品详情页
class ProductDetailsScreen extends StatelessWidget {
// 推荐:将路由名定义为静态常量,防止拼写错误
static const routeName = '/details';
final Product product;
const ProductDetailsScreen({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.title)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(product.description),
),
),
);
}
}
总结:如何选择?
-
基础
push/pop:- 优点: 简单直接,易于理解。
- 缺点: 当应用复杂时,路由逻辑分散,难以维护。
- 适用场景: 非常简单的应用,或者页面间的耦合度非常高、不可能被其他地方复用时。
-
命名路由:
- 优点: 集中管理,代码清晰,易于维护,是构建大型应用的基石。
- 缺点: 需要预先定义所有路由,设置上稍微复杂一点。
- 适用场景: 推荐在绝大多数应用中使用。它是构建可扩展、可维护 Flutter 应用的标准做法。
掌握了 Navigator 1.0 的用法,你就已经能够构建出功能完整的、多页面的 Flutter 应用了。接下来,我们将探讨 Flutter 中一个最核心、也最重要的话题:状态管理,这是构建复杂交互应用的关键。我们下篇见!
深入理解JavaScript执行机制:编译阶段与执行阶段的奥秘
深入理解JavaScript执行机制:编译阶段与执行阶段的奥秘
在JavaScript的世界里,你是否曾困惑于"变量未定义却能使用"、"函数在声明前就能调用"等现象?这些看似违反直觉的行为背后,是V8引擎精心设计的执行机制在起作用。今天,让我们一起揭开JavaScript执行机制的神秘面纱。
一、JavaScript执行的两个阶段
JavaScript的执行分为两个关键阶段:编译阶段和执行阶段。V8引擎在代码执行前的"霎那"进行编译,而不是像C++那样提前编译成机器码。
编译阶段:检测语法错误、进行变量提升 执行阶段:真正执行代码
这个机制是JavaScript与C++/Java等编译型语言的重要区别——JavaScript是编译型语言,但执行在运行时。
二、变量提升:var的魔法
var声明的变量会进行变量提升,在编译阶段被提升到作用域顶部,初始化为undefined。
让我们看一个经典例子:
var a = 1;
function fn(a) {
console.log(a); // 3
var a = 2;
console.log(a); // 2
}
fn(3);
console.log(a); // 1
这段代码的执行结果看似违反直觉,但背后是JavaScript引擎精心设计的执行机制在起作用。让我们一步步拆解这个过程。
编译阶段:引擎的"提前准备"
在JavaScript执行前,V8引擎会进行编译阶段,这相当于引擎在"阅读"代码时做的一次"预处理"。这个阶段不会执行代码,而是为执行阶段做准备。
全局作用域的编译
在全局作用域中,引擎会:
- 发现
var a = 1,将其提升为var a = undefined
编译结果:var a = undefined
函数fn作用域的编译
在函数fn的作用域中,引擎会:
- 发现
var a = 2,将其提升为var a = undefined
编译结果:var a = undefined
💡 关键点:编译阶段只处理变量声明,将它们"提升"到作用域顶部,但不会执行赋值操作。变量被初始化为
undefined。
执行阶段:代码的真正运行
在编译完成后,引擎进入执行阶段,开始真正执行代码。
1. 全局代码执行
-
var a = 1:全局变量a被赋值为1 -
fn(3):调用函数fn,传入参数3
2. 函数fn的执行过程
-
参数a的赋值:
fn(3)调用时,参数a被设置为3- 此时,函数作用域中的
a(编译阶段提升的var a = undefined)被赋值为3
- 此时,函数作用域中的
-
第一次console.log(a) :打印
a,输出3- 这是函数参数
a的值(3)
- 这是函数参数
-
var a = 2:将
a赋值为2- 这是函数作用域中的
a(不是参数),覆盖了参数值3
- 这是函数作用域中的
-
第二次console.log(a) :打印
a,输出2- 这是函数作用域中
a的最新值(2)
- 这是函数作用域中
3. 函数执行完毕
- 函数fn执行完毕,其作用域被销毁
- 全局作用域的
a值仍为1
4. 最后一步:console.log(a)
- 打印全局变量
a,输出1 - 这是全局作用域中的
a(var a = 1赋值的)
在函数fn中:
-
编译阶段:
var a = undefined和function a = function() {}都被提升 -
执行阶段:
-
var a = 2将a赋值为2(覆盖了编译阶段的undefined) -
function a() {}是函数声明,但不会覆盖已赋值的变量
-
这与C++等编译型语言不同,JavaScript的编译阶段不会立即执行赋值,而是将变量提升到作用域顶部,初始化为undefined,然后在执行阶段才进行实际赋值。
三、let/const:告别变量提升
let和const声明的变量不会进行变量提升,而是进入"暂时性死区"(TDZ),在声明前使用会报错。
// 严格模式下
'use strict'
var a = 1;
var a = 2; // 无错误,覆盖
let b = 3;
let b = 4; // 报错:Identifier 'b' has already been declared
console.log(a);
console.log(b);
![]()
在严格模式下,var重复声明不会报错,但会覆盖,console.log(a)结果为2;;而let重复声明会直接报错。这体现了let/const更严格的变量管理机制。
四、函数声明 vs 变量声明
在JavaScript中,变量提升和函数提升是两个看似相似但本质不同的概念。通过对比以下两段代码,我们将彻底理解它们的差异,特别是函数声明的完整提升特性。
代码1:
var a = 1;
function fn(a) {
console.log(a);
var a = 2;
console.log(a);
}
fn(3);
console.log(a);
代码2:
var a = 1;
function fn(a) {
console.log(a);
var a = 2;
function a() {};
console.log(a);
}
fn(3);
console.log(a);
这两段代码的唯一区别在于第二段代码中多了一个function a() {}。但结果却揭示了JavaScript中一个重要的机制:函数声明的完整提升。
代码1的编译结果
- 全局作用域:
var a = undefined - 函数fn作用域:
var a = undefined(函数参数a)和var a = undefined(函数内部var a)
代码2的编译结果
- 全局作用域:
var a = undefined - 函数fn作用域:
var a = undefined(函数参数a)、var a = undefined(函数内部var a)和function a = function() {}(函数声明a)
关键点:函数声明function a() {}不仅被提升,整个函数体也被提升,而变量声明var a = 2只提升声明,不提升赋值。
执行阶段:
让我们一步步分析代码2的执行过程:
-
全局作用域:
-
var a = 1;:全局变量a被赋值为1
-
-
函数fn调用:
-
fn(3);:参数a被设置为3
-
-
函数fn内部执行:
-
console.log(a);:输出[Function: a](a被覆盖成了函数) -
var a = 2;:将函数内部的a赋值为2 -
function a() {};:函数声明被提升,但不会覆盖已经赋值的变量 -
console.log(a);:输出2(函数内部a的最新值,来自var a = 2)
-
-
函数执行完毕:
- 函数内部的a被销毁
-
全局作用域:
-
console.log(a);:输出1(全局变量a的值)
-
为什么函数声明不会覆盖变量赋值?
这是理解JavaScript执行机制的关键点:
- 函数声明:在编译阶段被提升,整个函数体(包括函数名和函数体)都被提升
- 变量声明:只提升声明,赋值操作留在原地执行
在代码2中,函数声明function a() {}在编译阶段被提升,但执行阶段var a = 2已经将a赋值为2,所以函数声明不会覆盖这个赋值。函数声明只是将a指向一个函数,但var a = 2已经将a指向了一个数字。
一个生动的比喻
想象一下,你有一个房间(作用域),里面有三个"标签":
- 一个标签写着"a"(函数参数)
- 一个标签写着"a"(函数内部变量)
- 一个标签写着"函数a"(函数声明)
在编译阶段,所有标签都被贴到了房间的顶部:
- 两个"a"标签(一个用于函数参数,一个用于函数内部变量)
- "函数a"标签
在执行阶段:
- 函数参数a被设置为3
- 函数内部变量a被赋值为2(覆盖了之前的"a"标签)
- "函数a"标签被贴在了墙上,但不会覆盖已经赋值的"数字2"标签
所以,当你打印a时,看到的是"2",而不是函数。
与let/const的对比
如果将代码2中的var替换为let:
let a = 1;
function fn(a) {
console.log(a);
let a = 2;
function a() {};
console.log(a); // ReferenceError: Cannot access 'a' before initialization
}
fn(3);
这会抛出错误,因为let声明的变量不会被提升,而是进入"暂时性死区"(TDZ)。在let a = 2执行前,变量a处于TDZ中,访问它会报错。
结论:函数提升的完整特性
通过以上分析,我们可以清晰地总结:
-
变量提升:只提升变量的声明,赋值留在原地执行。变量被初始化为
undefined。 - 函数提升:提升函数名和整个函数体,函数声明优先级高于变量声明。
- 函数声明与变量声明的关系:函数声明不会覆盖已经赋值的变量,但会覆盖未赋值的变量。
函数提升的"完整性"是其与变量提升的关键区别。函数声明不仅将函数名提升到作用域顶部,还将整个函数体(包括函数逻辑)提升,使我们可以在函数定义之前调用它。
理解这个机制,你就能避免许多JavaScript的"奇怪行为",写出更清晰、更可靠的代码。记住:函数提升是"完整的",而变量提升是"部分的"。这是JavaScript执行机制中最微妙但最重要的区别之一。
五、函数声明 vs 函数表达式
函数声明会进行提升,可以在声明前调用;而函数表达式不会提升。
// 函数声明:提升
showName();
function showName() {
console.log('函数showName被执行');
}
// 函数表达式:不会提升
func();
let func = () => {
console.log('函数表达式不会提升');
};
// 会报错:func is not a function
这是因为函数声明在编译阶段被提升,而函数表达式只是普通变量赋值,需要等到执行阶段才会被赋值。
六、执行上下文与调用栈
JavaScript的执行依赖于执行上下文,包括变量环境和词法环境。
- 全局执行上下文:在代码开始执行时创建
- 函数执行上下文:当函数被调用时创建
调用栈是管理执行上下文的数据结构,遵循"先进后出"原则:
- 全局执行上下文被压入调用栈
- 函数执行时,创建新的函数执行上下文压入栈
- 函数执行完毕,执行上下文出栈,变量被回收
七、简单数据类型与复杂数据类型
理解执行机制还需了解内存管理:
- 简单数据类型(字符串、数字):存储在栈内存,直接存储值
- 复杂数据类型(对象、数组):存储在堆内存,存储的是地址
let str = 'hello';
let str2 = str; // 值拷贝
str2 = '你好';
console.log(str, str2);
let obj = { name: '郑老板', age: 18 };
let obj2 = obj; // 地址拷贝
obj2.age++;
console.log(obj, obj2);
![]()
对象是通过引用传递的,而不是通过值传递。这意味着:
-
obj是一个变量,它保存的是对象在内存中的地址(引用) -
let obj2 = obj;这行代码只是将obj的引用复制给obj2,并没有创建新的对象 - 所以
obj和obj2都指向内存中同一个对象 - 当执行
obj2.age++时,实际上是在修改内存地址0x7F8A3B2C处的对象,所以obj和obj2都会看到这个修改。
八、为什么JavaScript要这样设计?
V8引擎设计JavaScript执行机制有其深意:
- 编译总是在执行前的一霎那:确保代码的即时性
- 全局和函数体的编译生成执行上下文:为执行做好准备
- 函数执行完毕后销毁执行上下文:实现变量垃圾回收
这种机制使得JavaScript既能像解释型语言一样灵活,又能保持一定的性能。
结语:理解机制,写出更优代码
JavaScript的执行机制看似复杂,实则有其逻辑。理解编译阶段与执行阶段的区别,掌握var/let/const、函数声明/表达式的不同,能帮助我们:
- 避免常见的"变量未定义"错误
- 优化代码结构,提高可读性
- 更好地理解作用域链和闭包
正如V8引擎的设计理念: "一边编译,后执行,在编译,再执行" 。理解了这个机制,你就不再被"奇怪的执行顺序"困扰,而是能预见代码的执行行为,写出更健壮、更高效的JavaScript代码。
记住,JavaScript不是简单的"从上到下执行",而是一个编译-执行的智能过程。掌握这个机制,你就能真正驾驭JavaScript,成为一名更优秀的前端开发者。
面试必考:从setTimeout到Promise和fetch
前言
JavaScript是一种单线程语言,这意味着它一次只能执行一个任务。这种设计简化了编程模型,但也带来了挑战:如何处理耗时操作而不阻塞主线程?
一、 异步编程基础
1.同步 vs 异步执行
// 同步代码示例
console.log(1);
console.log(2);
console.log(3);
// 输出顺序:1 2 3
2.异步代码示例
console.log(1);
setTimeout(function() {
console.log(2);
}, 1000);
console.log(3);
// 输出顺序:1 3 2(1秒后)
其中setTimeout是一个延迟函数,设定在一秒后执行,所以改变了原有的代码执行顺序
3.事件循环机制
JavaScript通过事件循环机制处理异步操作:
- 同步代码立即执行
- 异步代码被放入事件队列
- 当调用栈为空时,事件队列中的任务按顺序执行
二、Promise:异步编程的解决方案
1.Promise基本用法
const promise = new Promise((resolve, reject) => {
// 异步操作代码
// 成功时调用 resolve(value)
// 失败时调用 reject(error)
});
2. 示例
console.log(1);
const p = new Promise((resolve) => {
setTimeout(function() {
console.log(2);
resolve();
}, 3000);
});
p.then(() => {
console.log(3);
});
console.log(4);
// 输出顺序:1 4 2 3
3.Promise处理文件读取
import fs from 'fs';
console.log(1);
const p = new Promise((resolve, reject) => {
console.log(3); // 同步执行
fs.readFile('./a.txt', function(err, data) {
if (err) {
reject(err);
return;
}
resolve(data.toString());
});
});
p.then(data => {
console.log(data);
}).catch(err => {
console.log(err);
});
console.log(4);
// 输出顺序:1 3 4 [文件内容]
注意:被Promise实例包裹的同步代码并不会延时触发,而是正常按照顺序执行
三、fetch API:网络请求
// 基本用法
fetch("https://api.github.com/orgs/lemoncode/members")
.then((response) => response.json())
.then((data) => {
// 处理数据
console.log(data);
})
.catch((error) => {
console.error('Error:', error);
});
实际应用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GitHub Members</title>
</head>
<body>
<ul id="members"></ul>
<script>
fetch("https://api.github.com/orgs/lemoncode/members")
.then((response) => response.json())
.then((members) => {
const membersList = document.getElementById("members");
membersList.innerHTML = members
.map((member) => {
return `<li>${member.login}</li>`;
})
.join("");
})
.catch((error) => {
console.error('Error fetching members:', error);
});
</script>
</body>
</html>
异步编程最佳实践
1. 错误处理
// 使用catch处理Promise错误
someAsyncFunction()
.then(result => {
// 处理成功结果
})
.catch(error => {
// 处理错误
console.error('An error occurred:', error);
});
2. 并行执行多个异步操作
// 使用Promise.all同时执行多个异步操作
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(([users, posts, comments]) => {
// 所有请求都完成后执行
})
.catch(error => {
// 任何一个请求失败都会进入这里
});
3. async/await语法糖
// 使用async/await使异步代码更易读
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}
总结
JavaScript的异步编程模型虽然初看起来复杂,但理解其核心概念后,可以编写出高效、非阻塞的代码。关键要点包括:
- JavaScript是单线程的,使用事件循环处理异步操作
- setTimeout是基础的异步机制
- Promise提供了更强大的异步控制能力
- fetch是处理网络请求的现代API
- async/await语法让异步代码更易读
掌握这些概念和工具,将帮助你编写更高效、更可靠的JavaScript应用程序。
Vue 项目上线前必查!8 个易忽略知识点,90% 开发者都踩过坑
Vue 项目上线前必查!8 个易忽略知识点,90% 开发者都踩过坑
最近最近接手了一个朋友的 Vue3 项目,改 bug 改到怀疑人生 —— 明明语法看着没毛病,页面就是不更新;父子组件传值偶尔失效;打包后样式突然错乱… 排查后发现全是些 “不起眼” 的知识点在作祟。
这些知识点不像响应式、生命周期那样被反复强调,却偏偏是面试高频考点和项目线上问题的重灾区。今天就带大家逐个拆解,每个都附代码示例和避坑方案,新手能避坑,老手能查漏,建议收藏备用!🚀
1. Scoped 样式的 “隐形泄露”,父子组件样式串味了
写组件时大家都习惯加scoped让样式局部化,但你可能遇到过:父组件的样式莫名其妙影响了子组件?这可不是 Vue 的 bug。
隐藏陷阱
Vue 为scoped样式的元素添加独特属性(如data-v-xxx)来隔离样式,但子组件的根节点会同时继承父组件和自身的 scoped 样式。比如这样的代码:
vue
<!-- 父组件 App.vue -->
<template>
<h4>父组件标题</h4>
<HelloWorld />
</template>
<style scoped>
h4 { color: red; }
</style>
<!-- 子组件 HelloWorld.vue -->
<template>
<h4>子组件标题</h4> <!-- 会被父组件的red样式影响 -->
</template>
<style scoped></style>
最终子组件的 h4 也会变成红色,很多人第一次遇到都会懵圈。
避坑方案
-
给子组件根元素加唯一 class,避免标签选择器冲突
vue
<!-- 优化后 HelloWorld.vue --> <template> <div class="hello-world"> <h4>子组件标题</h4> </div> </template> -
Vue3 支持多根节点,直接用多个根元素打破继承链
-
尽量用 class 选择器替代标签选择器,减少冲突概率
2. 数组 / 对象响应式失效?别再直接改索引了
这是 Vue 响应式系统的经典 “坑”,Vue3 用 Proxy 优化了不少,但某些场景依然会踩雷。
隐藏陷阱
Vue 的响应式依赖数据劫持实现,但以下两种操作无法被监听:
- 给对象新增未声明的属性
- 直接修改数组索引或长度
vue
<template>
<div>{{ user.age }}</div>
<div>{{ list[0] }}</div>
<button @click="modifyData">修改数据</button>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({ name: '张三' })
const list = reactive(['苹果'])
const modifyData = () => {
user.age = 25 // 新增属性,页面不更新
list[0] = '香蕉' // 直接改索引,页面不更新
}
</script>
点击按钮后,数据确实变了,但页面纹丝不动。
避坑方案
针对不同数据类型用正确姿势修改:
vue
<script setup>
import { reactive } from 'vue'
const user = reactive({ name: '张三' })
const list = reactive(['苹果'])
const modifyData = () => {
// 对象新增属性:直接赋值即可(Vue3 Proxy支持)
user.age = 25
// 数组修改:用splice或替换数组
list.splice(0, 1, '香蕉')
// 也可直接替换整个数组
// list = ['香蕉', '橙子']
}
</script>
小贴士:Vue2 中需用
this.$set(user, 'age', 25),Vue3 的 Proxy 无需额外 API,但修改数组索引仍需用数组方法。
3. setup 里的异步请求,别漏了 Suspense 配合
Vue3 的 Composition API 是趋势,但很多人在 setup 里写异步请求时,遇到过数据渲染延迟或报错的问题。
隐藏陷阱
setup 函数执行时组件还未挂载,若直接在 setup 中写 async/await,返回的 Promise 会导致组件渲染异常,因为 setup 本身不支持直接返回 Promise。
vue
<!-- 错误示例 -->
<script setup>
import axios from 'axios'
const data = ref(null)
// 直接用await会导致组件初始化异常
const res = await axios.get('/api/list')
data.value = res.data
</script>
避坑方案
用 Vue3 内置的<Suspense>组件包裹异步组件,搭配异步 setup 使用:
vue
<!-- 父组件 -->
<template>
<Suspense>
<template #default>
<DataList />
</template>
<template #fallback>
<div>加载中...</div> <!-- 加载占位 -->
</template>
</Suspense>
</template>
<!-- DataList.vue 异步组件 -->
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const data = ref(null)
// setup可以写成async函数
const fetchData = async () => {
const res = await axios.get('/api/list')
data.value = res.data
}
fetchData()
</script>
这样既能正常发起异步请求,又能优雅处理加载状态,提升用户体验。
4. 非 props 属性 “悄悄继承”,DOM 多了莫名属性
给组件传了没在 props 中声明的属性(如 id、class),结果发现子组件根元素自动多了这些属性,有时会导致样式或功能冲突。
隐藏陷阱
这是 Vue 的非 props 属性继承特性,像 id、class、name 这类未被 props 接收的属性,会默认挂载到子组件的根元素上。比如:
vue
<!-- 父组件 -->
<template>
<UserCard id="user-card" class="card-style" />
</template>
<!-- 子组件 UserCard.vue 未声明对应props -->
<template>
<div>用户信息卡片</div> <!-- 最终会被渲染为<div id="user-card" class="card-style"> -->
</template>
若子组件根元素已有 class,会和继承的 class 合并,有时会覆盖预期样式。
避坑方案
-
禁止继承:用
inheritAttrs: false关闭自动继承vue
<script setup> // 关闭非props属性继承 defineOptions({ inheritAttrs: false }) </script> -
手动控制属性位置:用
$attrs将属性挂载到指定元素vue
<template> <div> <div v-bind="$attrs">只给这个元素加继承属性</div> </div> </template>
5. 生命周期的 “顺序陷阱”,父子组件执行顺序搞反了
Vue2 升级 Vue3 后,生命周期不仅改了命名,父子组件的执行顺序也有差异,这是面试高频题,也是项目中异步逻辑出错的常见原因。
隐藏陷阱
很多人仍沿用 Vue2 的思维写 Vue3 代码,比如认为父组件的onMounted会比子组件先执行,结果 DOM 操作时报错。
| 阶段 | Vue2 执行顺序 | Vue3 执行顺序 |
|---|---|---|
| 初始化 | 父 beforeCreate→父 created→父 beforeMount→子 beforeCreate→子 created→子 beforeMount→子 mounted→父 mounted | 父 setup→父 onBeforeMount→子 setup→子 onBeforeMount→子 onMounted→父 onMounted |
避坑方案
-
数据初始化:Vue3 可在 setup 中直接用 async/await 发起请求,配合 Suspense
-
DOM 操作:务必在
onMounted中执行,且要清楚子组件的 mounted 会比父组件先触发 -
清理工作:定时器、事件监听一定要在
onBeforeUnmount中清除,避免内存泄漏vue
<script setup> import { onMounted, onBeforeUnmount } from 'vue' let timer = null onMounted(() => { timer = setInterval(() => { console.log('定时器运行中') }, 1000) }) // 组件卸载前清除定时器 onBeforeUnmount(() => { clearInterval(timer) }) </script>
6. CSS 中用 v-bind,动态样式的正确打开方式
Vue3.2 + 支持在 CSS 中直接用 v-bind 绑定数据,这个特性很实用,但很多人不知道它的底层逻辑和使用限制。
隐藏陷阱
直接在 CSS 中绑定计算属性时,误以为修改数据后样式不会实时更新,或者担心影响性能。
vue
<template>
<div class="text">动态颜色文本</div>
<button @click="changeColor">切换颜色</button>
</template>
<script setup>
import { ref, computed } from 'vue'
const primaryColor = ref('red')
const textColor = computed(() => primaryColor.value)
const changeColor = () => {
primaryColor.value = primaryColor.value === 'red' ? 'blue' : 'red'
}
</script>
<style>
.text {
color: v-bind(textColor);
}
</style>
避坑方案
- 无需担心性能:v-bind 会被编译成 CSS 自定义属性,通过内联样式应用到组件,数据变更时仅更新自定义属性
- 支持多种数据类型:可绑定 ref、reactive、computed,甚至是 props 传递的值
- 与 scoped 兼容:动态样式同样支持局部作用域,不会污染全局
7. ref 获取元素,别在 onMounted 前急着用
用 ref 获取 DOM 元素是基础操作,但新手常犯的错是在 DOM 未挂载完成时就调用元素方法。
隐藏陷阱
在setup或onBeforeMount中获取 ref,结果拿到undefined。
vue
<template>
<input ref="inputRef" type="text" />
</template>
<script setup>
import { ref, onBeforeMount } from 'vue'
const inputRef = ref(null)
onBeforeMount(() => {
inputRef.value.focus() // 报错:Cannot read property 'focus' of null
})
</script>
避坑方案
-
基础用法:在
onMounted中操作 ref 元素,此时 DOM 已完全挂载vue
<script setup> import { ref, onMounted } from 'vue' const inputRef = ref(null) onMounted(() => { inputRef.value.focus() // 正常生效 }) </script> -
动态元素:若 ref 绑定在 v-for 渲染的元素上,inputRef 会变成数组,需通过索引访问
-
组件 ref:获取子组件实例时,子组件需用
defineExpose暴露属性和方法
8. watch 监听数组 / 对象,深度监听别写错了
watch 是 Vue 中处理响应式数据变化的核心 API,但监听复杂数据类型时,很容易出现 “监听不到变化” 的问题。
隐藏陷阱
直接监听数组或对象时,默认只监听引用变化,对内部属性的修改无法触发监听。
vue
<script setup>
import { ref, watch } from 'vue'
const user = ref({ name: '张三', age: 20 })
// 错误:监听不到age的变化
watch(user, (newVal) => {
console.log('用户信息变了', newVal)
})
const changeAge = () => {
user.value.age = 25 // 仅修改内部属性,不触发监听
}
</script>
避坑方案
根据 Vue 版本选择正确的监听方式:
-
Vue3 监听 ref 包裹的对象:开启深度监听
vue
watch(user, (newVal) => { console.log('用户信息变了', newVal) }, { deep: true }) // 开启深度监听 -
精准监听单个属性:用函数返回值的方式,性能更优
vue
// 只监听age变化,无需深度监听 watch(() => user.value.age, (newAge) => { console.log('年龄变了', newAge) })
最后总结
Vue 这些易忽略的知识点,本质上都是对底层原理理解不透彻导致的。很多时候我们只顾着实现功能,却忽略了这些细节,等到项目上线出现 bug 才追悔莫及。
以上 8 个知识点,建议结合代码逐个实操验证。如果本文帮你避开了坑,欢迎点赞收藏,也可以在评论区分享你踩过的 Vue 神坑,一起避雷成长!💪
从 "外卖点单" 到 Promise:揭秘 JavaScript 异步的底层逻辑
你是否经历过这些场景?
- 打开网页点击按钮,页面瞬间变成"幻灯片"卡死?
- 写代码时
console.log(2)明明在setTimeout后面,却先打印了3?这些"诡异"现象背后,藏着 JavaScript 异步编程的核心逻辑!
作为前端开发者,我们每天都在和异步打交道,但你真的理解它的底层原理吗?
为什么 JS 必须是单线程?Promise 到底解决了什么问题?fetch 的底层原理是什么?
今天,我们将通过"外卖点单"的类比,一步步揭开 JS 异步的神秘面纱!
一、为什么 JS 必须有"异步"?—— 单线程的"生存智慧"
🧬 JavaScript 的"基因":单线程设计
JavaScript 是单线程语言——同一时间只能做一件事。
想象一个小厨房里的厨师:
- 炒青菜时不能同时煎牛排
- 必须等青菜出锅才能处理下一道菜
为什么这样设计?
因为 JS 最初是为浏览器打造的脚本语言,负责 DOM 操作和事件响应:
❌ 如果允许多线程:
线程A删除按钮 + 线程B点击按钮 = 页面崩溃!
✅ 单线程避免了资源竞争问题,保证执行安全
⚠️ 单线程的致命问题:阻塞
当遇到耗时任务时(如网络请求),单线程会完全阻塞:
🍲 厨师炖一锅2小时的汤 → 后面所有客人干等 → 页面卡死!
解决方案:同步 vs 异步
| 任务类型 | 特点 | 示例 |
|---|---|---|
| 同步任务 | 立即执行,按顺序完成 |
console.log()、变量声明 |
| 异步任务 | 延迟处理,不阻塞主线程 |
setTimeout、fetch
|
外卖点单类比 🍔:
- 客人下单(发起异步任务)
- 服务员不傻等 → 继续接待新客人(执行同步任务)
- 菜做好后 → 通知客人取餐(执行回调)
💡 异步的本质:
把耗时任务交给别人处理,自己先去做别的事
二、异步任务如何"插队"?—— Event Loop 的工作流程
🏦 银行办理业务类比
| 组件 | 类比说明 |
|---|---|
| 调用栈 (Call Stack) | 正在办理业务的窗口 |
| 任务队列 (Task Queue) | 等候区的排号单 |
| Event Loop | 叫号员:检查窗口是否空闲javascript |
运行
console.log(1); // 同步任务
setTimeout(() => {
console.log(2); // 异步任务
}, 5000);
console.log(3); // 同步任务
🔍 执行流程详解
-
console.log(1)→ 执行 → 输出1→ 出栈 ✅ - 遇到
setTimeout→ 交给浏览器线程处理 → 继续执行后续代码 -
console.log(3)→ 执行 → 输出3→ 出栈 ✅ - 5秒后:定时器完成 → 回调函数进入任务队列
- Event Loop 检测到调用栈空闲 → 将回调移入调用栈
-
console.log(2)→ 执行 → 输出2
✅ 最终输出:1 → 3 → 2
📌 关键结论:
异步任务不会立刻执行,而是等主线程空闲后,由 Event Loop 从任务队列中取出执行
三、Promise:给异步任务一张"可控的取餐号"
🌪️ 回调地狱问题
早期异步代码像"剥洋葱",层层嵌套难以维护:javascript
运行
// 嵌套三层的回调地狱
setTimeout(() => {
console.log("第一步");
setTimeout(() => {
console.log("第二步");
setTimeout(() => {
console.log("第三步");
}, 1000);
}, 1000);
}, 1000);
💡 Promise 的核心价值
Promise = 可控的取餐号
- 明确知道任务何时成功/失败
- 按顺序处理多个异步任务
- 消除回调地狱
🎯 Promise 的三大核心特性
1. 三种不可逆状态
| 状态 | 含义 | 触发方式 |
|---|---|---|
pending |
等待中(初始状态) | 创建 Promise 时 |
fulfilled |
成功完成 | 调用 resolve()
|
rejected |
失败 | 调用 reject()
|
2. 链式调用 .then() 和 .catch()
const p = new Promise((_, reject) => {
fs.readFile('./b.txt', (err, data) => {
err ? reject(err) : resolve(data.toString());
});
});
p.then(data => {
console.log('成功:', data);
}).catch(err => {
console.log('失败:', err.message); // 捕获 reject 和异常
});
📌 .catch() 会捕获:
-
reject()触发的错误 -
.then()中抛出的异常(类似 try-catch) -
四、fetch:基于 Promise 的网络请求利器
✅ 基本用法
fetch('https://api.github.com/orgs/lemoncode/members')
.then(response => response.json())
.then(members => {
const list = members.map(m => `<li>${m.login}</li>`).join('');
document.getElementById('members').innerHTML = list;
})
.catch(err => console.error('请求失败:', err));
🔍 底层执行流程
![]()
⚠️ 关键注意事项
HTTP 错误不会触发 reject!
404/500 等状态码属于"服务器正常响应",需手动检查:
fetch(url)
.then(response => {
if (!response.ok) { // 检查 HTTP 状态
throw new Error(`HTTP错误:${response.status}`);
}
return response.json();
})
.catch(err => console.error('错误:', err));
五、异步编程进化史:从回调到 async/await
📈 发展历程
| 时代 | 特点 | 问题 |
|---|---|---|
| 回调函数 | 最原始方式 | 回调地狱,嵌套过深 |
| Promise | 链式调用,状态管理 |
.then 嵌套仍显繁琐 |
| async/await | 用同步写法处理异步 | 最清晰的代码结构 |
💫 async/await 实战
async function getMembers() {
try {
// await 会"暂停"函数执行,但不阻塞主线程
const response = await fetch('https://api.github.com/orgs/lemoncode/members');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const members = await response.json();
console.log(members);
} catch (err) {
console.error('出错了:', err);
}
}
✅ 优势:
- 代码结构像同步一样清晰
- 错误处理统一用 try/catch
- 避免 Promise 链式调用的嵌套
🌟 总结:理解异步 = 掌握 JS 的"运行法则"
| 核心概念 | 关键要点 |
|---|---|
| 单线程 | 为避免 DOM 操作冲突而设计,必须通过异步避免阻塞 |
| Event Loop | 异步调度中心:协调调用栈(窗口)和任务队列(排号),实现非阻塞执行 |
| Promise | 通过状态管理(pending/fulfilled/rejected)和链式调用,解决回调地狱问题 |
| fetch | 基于 Promise 的网络请求 API,注意 HTTP 错误需手动检查 response.ok
|
| async/await | 语法糖,让异步代码像同步一样可读,底层仍基于 Promise |
💡 下次遇到"执行顺序诡异"时:
从这三个角度分析:
1️⃣ 调用栈当前在执行什么?
2️⃣ 任务队列中有什么等待执行?
3️⃣ Promise 状态如何变化?
你会发现所有"诡异"现象,都有章可循!
🚀 动手实践建议
- 用 Promise 封装一个
setTimeout函数 - 尝试用 async/await 重构回调地狱代码
- 在浏览器开发者工具中调试异步代码,观察调用栈变化
掌握异步编程,你就能真正掌控 JavaScript 的运行脉搏!
现在,打开编辑器,亲手体验"掌控异步"的快感吧! 💻✨
《JavaScript Promise 完全解析:executor、状态与链式调用》
深入理解 JavaScript Promise:从构造到状态流转
在现代 JavaScript 开发中,Promise 已成为处理异步操作的标准工具。但很多人只是“会用”,却不理解它背后的运行机制。本文将带你从零开始,深入剖析 Promise 的构造方式、内部状态变化规则,以及 .then() 和 .catch() 是如何协同工作的。
一、Promise 是如何构造的?
Promise 是一个内置的构造函数,通过 new Promise() 创建实例:
const myPromise = new Promise((resolve, reject) => {
// executor(执行器)函数
});
构造函数接收一个参数:executor 函数
-
这个函数会立即同步执行(不是异步!)。
-
Promise有三种状态
-
- pending(待定)
-
- fulfilled(已兑现)
-
- rejected(已拒绝)
-
Promise两个内部属性
-
- state
-
- result
-
Promise接收两个参数:
-
resolve(value):用于将 Promise state变为 fulfilled(成功)、result:value -
reject(error):用于将 Promise state变为 rejected(失败)、result:error
-
![]()
注意:即使你没有调用
resolve或reject,Promise 也会一直处于pending状态,永远不会完成。
示例:基本构造
console.log(1);
//Promise 异步任务同步化
//许诺 Promise 包含一个耗时性的任务
const p=new Promise((resolve)=>{
setTimeout(function() {
console.log(2);
resolve();
}, 3000);
})
p.then(()=>{
console.log(3);
})
console.log(4);
输出顺序:
![]()
1和4作为同步任务先执行
2在3秒后输出
当异步任务结束成功后.then处理函数输出3
这说明:executor 是同步执行的,但其中的异步逻辑(如 setTimeout)会被放入任务队列。
二、Promise 状态的不可逆性
状态一旦改变,就不可逆转
const p = new Promise((resolve, reject) => {
resolve('第一次 resolve');
resolve('第二次 resolve'); // 无效!
reject('尝试 reject'); // 也无效!
});
p.then(console.log); // 只输出 "第一次 resolve"
executor 只能调用一个
resolve或一个reject。任何状态的更改都是最终的。 所有其他的再对resolve和reject的调用都会被忽略:
三、.then():监听状态变化的桥梁
.then() 是 Promise 最核心的方法,用于注册成功和失败的回调函数。
语法
promise.then(
onFulfilled, // 当状态变为 fulfilled 时调用
onRejected // 当状态变为 rejected 时调用(可选)
);
每次只根据异步处理结果运行一个函数
关键特性
-
.then()总是返回一个新的 Promise,支持链式调用。 - 如果
onFulfilled返回一个值,新 Promise 会被resolve该值。 - 如果
onFulfilled抛出异常,新 Promise 会被reject。
示例:成功与失败处理
const p = new Promise((resolve, reject) => {
Math.random() > 0.5 ? resolve('OK') : reject(new Error('NO'));
});
p.then(
result => console.log(' 成功:', result),
error => console.log(' 失败:', error.message)
);
提示:虽然
.then()支持两个参数,但更推荐使用.catch()统一处理错误。
四、.catch():专为错误设计的处理器
.catch(onRejected) 是 .then(null, onRejected) 的语法糖。
promise
.then(result => { /* 处理成功 */ })
.catch(error => { /* 处理失败 */ });
为什么推荐用 .catch()?
-
统一错误捕获:链式调用中,任何一个环节抛出异常,都会被后续的
.catch()捕获。 -
避免遗漏错误处理:如果只用
.then()的第二个参数,无法捕获.then()回调内部的错误。
对比示例
// 不推荐:无法捕获 then 内部的错误
promise.then(
() => { throw new Error('Oops!'); },
err => console.log('这里不会执行!')
);
// 推荐:能捕获所有前面的错误
promise
.then(() => { throw new Error('Oops!'); })
.catch(err => console.log('捕获到了:', err.message));
.catch其实是.then处理错误的一种简写方式.catch(f)调用是.then(null,f)的一种模拟
🔗 五、链式调用与状态传递
由于 .then() 和 .catch() 都返回新的 Promise,我们可以构建清晰的异步流程:
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => render(posts))
.catch(err => showError(err));
在这个链条中:
- 每一步的返回值会作为下一步的输入;
- 任何一步出错,都会跳转到最近的
.catch()。
总结:Promise 的核心心智模型
-
构造即执行:
new Promise(executor)会立即运行 executor。 -
状态单向不可逆:
pending → fulfilled或pending → rejected,仅一次。 -
.then()是观察者:根据状态决定调用哪个回调。 -
.catch()是安全网:集中处理整个链路的异常。 - 链式调用靠返回新 Promise:实现异步流程的线性表达。
JavaScript Promise 机制解析
深入理解 JavaScript Promise:运行机制、状态流转与执行顺序解析
在现代 JavaScript 开发中,Promise 已经成为异步编程的核心抽象。相比传统回调,它带来了更清晰的流程控制、更可靠的错误链路和更安全的异步表达方式。
然而,要真正用好 Promise,不仅要知道 .then() 和 .catch() 的写法,更要理解:
- Promise 为什么能“让异步看起来更同步”?
-
.then()为什么是微任务,它什么时候执行? - resolve 到底做了什么?
- Promise 与定时器、I/O 的执行顺序是什么?
本文将以 Promise 为中心,从执行机制、状态模型到微任务队列进行深度解析。
一、Promise 是什么?为何出现?
Promise 的根本目的:
用可控、可链式的方式管理异步任务,让异步逻辑更接近同步结构。
在 Promise 之前,回调模式会导致:
- 回调地狱
- 错误无法统一捕获
- 执行顺序难以推断
- 流程控制能力弱
Promise 解决了这些问题,通过:
- 明确的 状态模型(pending → fulfilled / rejected)
- 链式调用
- 微任务调度机制
- 捕获一致性(then/catch/finally)
使异步流程变得更可预测。
二、Promise 的构造与执行阶段
来看基础示例:
const p = new Promise((resolve, reject) => {
console.log(1); // 立即执行
setTimeout(() => {
console.log(2);
resolve();
}, 1000);
});
p.then(() => console.log(3));
console.log(4);
输出顺序为:
1
4
2
3
核心原因:Promise 构造函数 同步执行
-
new Promise(...)内部代码立即执行(同步) -
.then()的回调不会立即执行,而是进入 微任务队列 - 定时器是 宏任务
执行优先级:
同步任务 > 微任务 > 宏任务
执行过程:
- 输出
1 - 注册定时器(异步)
- 注册
.then()回调(微任务) - 输出
4 - 定时器回调执行,输出
2 - resolve → 将
.then()放入微任务队列 - 输出
3
这一机制奠定了 Promise 的核心价值:流程清晰且可控。
三、Promise 的状态流转与行为规则
Promise 的状态只有三种:
pendingfulfilledrejected
状态特点:
- 一旦从 pending 转为 fulfilled 或 rejected,就不可逆。(immutable)
- resolve 或 reject 只能触发状态变化一次
- then/catch 只会在状态稳定后异步执行(微任务)
示例:
resolve(1);
resolve(2); // 无效
reject('error'); // 无效
Promise 设计成只执行一次,是为了避免异步任务重复触发造成混乱。
四、为什么 Promise 属于“异步微任务”?
Promise 回调不属于普通异步,而是:
属于微任务(Microtask),优先级高于定时器、I/O 等宏任务。
微任务来源:
- Promise.then / catch / finally
- queueMicrotask
- MutationObserver
宏任务来源:
- setTimeout / setInterval
- I/O(如 fs.readFile)
- setImmediate
- UI 渲染事件
顺序:
同步 → 所有微任务 → 一个宏任务 → 微任务 → 宏任务 → 循环...
这也是为什么下面代码:
Promise.resolve().then(() => console.log('micro'));
setTimeout(() => console.log('macro'));
console.log('sync');
输出:
sync
micro
macro
五、Promise 与异步 I/O
在 Node 中,I/O 操作(如 fs.readFile)属于宏任务,因此即使放在 Promise 中,也不会立即触发 .then()。
示例:
import fs from 'fs';
const p = new Promise((resolve, reject) => {
console.log(1);
fs.readFile('./b.txt', (err, data) => {
if (err) return reject(err);
resolve(data.toString());
});
});
p.then(data => console.log(data))
.catch(err => console.log(err));
console.log(2);
执行顺序:
1
2
(文件内容)
流程:
- Promise 执行器同步执行 → 输出 1
- I/O 操作异步 → 注册回调
- 输出 2
- 读取完成 → resolve → 微任务 → then 执行
Promise 和 Node I/O 的组合能形成非常清晰、链式的文件读取逻辑。
六、Promise 为什么能“让异步看起来更同步”?
核心原因:
1. 异步结果以链式 .then() 形式呈现
结构像同步流程:
task()
.then(step2)
.then(step3)
.catch(handleError)
比回调嵌套清晰太多。
2. 异步调度由微任务队列保证
顺序确定、可预测,不受浏览器或 Node 背后调度干扰。
3. 错误管理统一
try/catch 不适用于异步,Promise 则能把错误向下传递到 catch。
七、总结:Promise 的核心要点
- 构造函数立即执行
- resolve/reject 会改变状态,并触发微任务
- then/catch 属于微任务,优先级高
- Promise 只会被解决一次,状态不可逆
- 与异步 I/O、定时器配合时,Promise 决定回调进入微任务队列,而不是宏任务
Promise 是现代 JS 异步的基础,也是 async/await 的底层支撑。