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