NativeScript 数据绑定 #
数据绑定概述 #
数据绑定是 NativeScript 的核心特性之一,它实现了数据与视图的自动同步。
text
┌─────────────────────────────────────────────────────────────┐
│ 数据绑定模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 单向绑定 (One-way) │
│ ┌─────────┐ ┌─────────┐ │
│ │ Model │ ─────────────> │ View │ │
│ └─────────┘ └─────────┘ │
│ │
│ 双向绑定 (Two-way) │
│ ┌─────────┐ ┌─────────┐ │
│ │ Model │ <───────────> │ View │ │
│ └─────────┘ └─────────┘ │
│ │
│ 事件绑定 (Event) │
│ ┌─────────┐ ┌─────────┐ │
│ │ View │ ─────────────> │ Handler │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
基本绑定语法 #
单向绑定 #
使用双花括号 {{ }} 进行绑定:
xml
<Label text="{{ message }}" />
<Image src="{{ avatar }}" />
<Label visibility="{{ isVisible ? 'visible' : 'collapsed' }}" />
双向绑定 #
对于输入组件,双向绑定会自动更新数据:
xml
<TextField text="{{ name }}" />
<Switch checked="{{ isEnabled }}" />
<Slider value="{{ volume }}" />
表达式绑定 #
xml
<!-- 字符串拼接 -->
<Label text="{{ 'Hello, ' + name }}" />
<!-- 条件表达式 -->
<Label text="{{ isActive ? 'Active' : 'Inactive' }}" />
<!-- 算术运算 -->
<Label text="{{ price * quantity }}" />
<!-- 方法调用 -->
<Label text="{{ formatPrice(price) }}" />
Observable 对象 #
创建 Observable #
typescript
import { Observable } from '@nativescript/core';
class UserViewModel extends Observable {
private _name: string = '';
private _email: string = '';
get name(): string {
return this._name;
}
set name(value: string) {
if (this._name !== value) {
this._name = value;
this.notifyPropertyChange('name', value);
}
}
get email(): string {
return this._email;
}
set email(value: string) {
if (this._email !== value) {
this._email = value;
this.notifyPropertyChange('email', value);
}
}
}
使用 Observable #
typescript
// main-page.ts
import { Page } from '@nativescript/core';
export function onNavigatingTo(args) {
const page = args.object as Page;
page.bindingContext = new UserViewModel();
}
xml
<!-- main-page.xml -->
<GridLayout>
<StackLayout>
<Label text="{{ name }}" />
<Label text="{{ email }}" />
<TextField text="{{ name }}" hint="Enter name" />
<TextField text="{{ email }}" hint="Enter email" />
</StackLayout>
</GridLayout>
简化写法 #
typescript
import { Observable, fromObject } from '@nativescript/core';
// 使用 fromObject 快速创建
const viewModel = fromObject({
name: 'John',
email: 'john@example.com',
isActive: true
});
page.bindingContext = viewModel;
ObservableArray #
创建 ObservableArray #
typescript
import { ObservableArray } from '@nativescript/core';
class ListViewModel extends Observable {
items: ObservableArray<Item>;
constructor() {
super();
this.items = new ObservableArray([
{ id: 1, title: 'Item 1', completed: false },
{ id: 2, title: 'Item 2', completed: true },
{ id: 3, title: 'Item 3', completed: false }
]);
}
addItem(title: string) {
this.items.push({
id: this.items.length + 1,
title: title,
completed: false
});
}
removeItem(index: number) {
this.items.splice(index, 1);
}
toggleItem(index: number) {
const item = this.items.getItem(index);
item.completed = !item.completed;
this.items.setItem(index, item);
}
}
绑定到 ListView #
xml
<ListView items="{{ items }}" itemTap="onItemTap">
<ListView.itemTemplate>
<GridLayout columns="auto, *, auto">
<Switch checked="{{ completed }}" col="0" />
<Label text="{{ title }}" col="1" />
<Button text="Delete" tap="onDelete" col="2" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
数组操作 #
typescript
// 添加元素
items.push(newItem);
items.unshift(newItem);
// 删除元素
items.pop();
items.shift();
items.splice(index, 1);
// 替换元素
items.setItem(index, newItem);
// 批量操作
items.push(...newItems);
// 清空数组
items.splice(0, items.length);
// 获取元素
const item = items.getItem(index);
绑定上下文 #
页面绑定上下文 #
typescript
export function onNavigatingTo(args) {
const page = args.object as Page;
page.bindingContext = new MainViewModel();
}
组件绑定上下文 #
xml
<GridLayout>
<StackLayout bindingContext="{{ user }}">
<Label text="{{ name }}" />
<Label text="{{ email }}" />
</StackLayout>
</GridLayout>
嵌套绑定上下文 #
typescript
const viewModel = fromObject({
user: {
name: 'John',
address: {
city: 'New York',
country: 'USA'
}
}
});
xml
<GridLayout>
<Label text="{{ user.name }}" />
<Label text="{{ user.address.city }}" />
<Label text="{{ user.address.country }}" />
</GridLayout>
MVVM 模式 #
Model #
typescript
// models/user.model.ts
export interface User {
id: number;
name: string;
email: string;
avatar: string;
}
export interface Product {
id: number;
name: string;
price: number;
image: string;
}
ViewModel #
typescript
// viewmodels/home.viewmodel.ts
import { Observable } from '@nativescript/core';
import { UserService } from '../services/user.service';
export class HomeViewModel extends Observable {
private _user: User;
private _products: ObservableArray<Product>;
private _isLoading: boolean = false;
constructor(private userService: UserService) {
super();
this.loadUser();
this.loadProducts();
}
get user(): User {
return this._user;
}
set user(value: User) {
this._user = value;
this.notifyPropertyChange('user', value);
}
get products(): ObservableArray<Product> {
return this._products;
}
get isLoading(): boolean {
return this._isLoading;
}
set isLoading(value: boolean) {
this._isLoading = value;
this.notifyPropertyChange('isLoading', value);
}
async loadUser() {
this.isLoading = true;
try {
this.user = await this.userService.getCurrentUser();
} finally {
this.isLoading = false;
}
}
async loadProducts() {
this.products = new ObservableArray(
await this.userService.getProducts()
);
}
onRefresh() {
this.loadUser();
this.loadProducts();
}
}
View #
xml
<!-- pages/home/home-page.xml -->
<Page navigatingTo="onNavigatingTo">
<GridLayout rows="auto, *, auto">
<!-- Header -->
<StackLayout row="0" class="header">
<Label text="{{ user.name }}" class="user-name" />
<Label text="{{ user.email }}" class="user-email" />
</StackLayout>
<!-- Content -->
<ListView items="{{ products }}" row="1">
<ListView.itemTemplate>
<GridLayout columns="auto, *">
<Image src="{{ image }}" col="0" width="80" height="80" />
<StackLayout col="1">
<Label text="{{ name }}" class="product-name" />
<Label text="{{ '$' + price }}" class="product-price" />
</StackLayout>
</GridLayout>
</ListView.itemTemplate>
</ListView>
<!-- Loading -->
<ActivityIndicator busy="{{ isLoading }}" row="1" />
<!-- Footer -->
<Button text="Refresh" tap="onRefresh" row="2" />
</GridLayout>
</Page>
typescript
// pages/home/home-page.ts
import { Page } from '@nativescript/core';
import { HomeViewModel } from './home.viewmodel';
export function onNavigatingTo(args) {
const page = args.object as Page;
page.bindingContext = new HomeViewModel();
}
export function onRefresh(args) {
const page = args.object.page;
const viewModel = page.bindingContext as HomeViewModel;
viewModel.onRefresh();
}
计算属性 #
实现 Getter #
typescript
class OrderViewModel extends Observable {
private _items: ObservableArray<Item> = new ObservableArray();
get items(): ObservableArray<Item> {
return this._items;
}
get subtotal(): number {
return this._items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
get tax(): number {
return this.subtotal * 0.1;
}
get total(): number {
return this.subtotal + this.tax;
}
addItem(item: Item) {
this._items.push(item);
this.notifyPropertyChange('subtotal', this.subtotal);
this.notifyPropertyChange('tax', this.tax);
this.notifyPropertyChange('total', this.total);
}
}
xml
<GridLayout>
<StackLayout>
<ListView items="{{ items }}">
<!-- ... -->
</ListView>
<Label text="{{ 'Subtotal: $' + subtotal }}" />
<Label text="{{ 'Tax: $' + tax }}" />
<Label text="{{ 'Total: $' + total }}" class="total" />
</StackLayout>
</GridLayout>
绑定转换器 #
创建转换器 #
typescript
// converters/value.converter.ts
export class ValueConverter {
static price(value: number): string {
return '$' + value.toFixed(2);
}
static date(value: Date): string {
return new Date(value).toLocaleDateString();
}
static uppercase(value: string): string {
return value.toUpperCase();
}
static booleanToVisibility(value: boolean): string {
return value ? 'visible' : 'collapsed';
}
}
使用转换器 #
typescript
// 在 ViewModel 中添加方法
class ViewModel extends Observable {
formatPrice(value: number): string {
return '$' + value.toFixed(2);
}
formatDate(value: Date): string {
return new Date(value).toLocaleDateString();
}
}
xml
<Label text="{{ formatPrice(price) }}" />
<Label text="{{ formatDate(createdAt) }}" />
绑定事件 #
事件绑定语法 #
xml
<Button text="Click" tap="{{ onTap }}" />
<TextField textChange="{{ onTextChange }}" />
在 ViewModel 中处理事件 #
typescript
class ViewModel extends Observable {
onTap(args: EventData) {
const button = args.object as Button;
console.log('Button tapped');
}
onTextChange(args: EventData) {
const textField = args.object as TextField;
console.log('Text changed:', textField.text);
}
}
双向绑定实现 #
自定义双向绑定 #
typescript
import { Observable, PropertyChangeData } from '@nativescript/core';
class FormViewModel extends Observable {
private _formData = {
name: '',
email: '',
phone: ''
};
constructor() {
super();
// 监听属性变化
this.on(Observable.propertyChangeEvent, (data: PropertyChangeData) => {
console.log(`${data.propertyName} changed to ${data.value}`);
});
}
get name(): string {
return this._formData.name;
}
set name(value: string) {
if (this._formData.name !== value) {
this._formData.name = value;
this.notifyPropertyChange('name', value);
this.validateForm();
}
}
get email(): string {
return this._formData.email;
}
set email(value: string) {
if (this._formData.email !== value) {
this._formData.email = value;
this.notifyPropertyChange('email', value);
this.validateForm();
}
}
get isValid(): boolean {
return this._formData.name.length > 0 &&
this._formData.email.includes('@');
}
private validateForm() {
this.notifyPropertyChange('isValid', this.isValid);
}
}
性能优化 #
批量更新 #
typescript
// 不推荐:多次触发更新
this.set('name', 'John');
this.set('email', 'john@example.com');
this.set('phone', '1234567890');
// 推荐:批量更新
this.notifyPropertyChange('user', {
name: 'John',
email: 'john@example.com',
phone: '1234567890'
});
延迟更新 #
typescript
import { setTimeout } from '@nativescript/core';
class SearchViewModel extends Observable {
private searchTimeout: number;
onSearchTextChange(args: EventData) {
const textField = args.object as TextField;
// 清除之前的定时器
clearTimeout(this.searchTimeout);
// 延迟搜索
this.searchTimeout = setTimeout(() => {
this.search(textField.text);
}, 300);
}
}
最佳实践 #
ViewModel 设计原则 #
text
┌─────────────────────────────────────────────────────────────┐
│ ViewModel 设计原则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 单一职责 │
│ 每个 ViewModel 只负责一个页面/组件 │
│ │
│ 2. 可测试性 │
│ ViewModel 不依赖 UI 组件 │
│ │
│ 3. 状态管理 │
│ 使用 Observable 管理状态 │
│ │
│ 4. 异步处理 │
│ 使用 async/await 处理异步操作 │
│ │
│ 5. 错误处理 │
│ 统一的错误处理机制 │
│ │
└─────────────────────────────────────────────────────────────┘
下一步 #
现在你已经掌握了数据绑定,接下来学习 样式主题,了解如何美化你的应用界面!
最后更新:2026-03-29