2.4 模板与数据绑定 数据绑定是把组件类的数据接到模板、把模板事件接回类 的语法体系:插值、属性绑定、事件绑定与双向绑定。站在变更检测主线上,绑定表达式就是被采样的探针——每轮检查重算一次,新旧值不同才写 DOM。理解绑定的求值时机,就理解了 Angular 性能话题的一半。 上节明确了"模板重新读取字段"是更新的第三段,本节把模板本身讲透:四种绑定的语法、求值方向,以及模板表达式写法的性能红线。 学习目标 正确使用四种绑定语法并说明各自的数据流向。 解释属性绑定与插值在目标为 DOM 属性时的等价关系。 写出安全的双向绑定,说明香蕉语法糖的展开形式。 列出模板表达式的三条禁令(调用重方法、赋值、new),并说明原因。 一、四种绑定一张图 属性绑定与插值的等价性值得点破: 本质是 的简写。
数据绑定是把组件类的数据接到模板、把模板事件接回类 的语法体系:插值、属性绑定、事件绑定与双向绑定。站在变更检测主线上,绑定表达式就是被采样的探针——每轮检查重算一次,新旧值不同才写 DOM。理解绑定的求值时机,就理解了 Angular 性能话题的一半。
上节明确了"模板重新读取字段"是更新的第三段,本节把模板本身讲透:四种绑定的语法、求值方向,以及模板表达式写法的性能红线。
<!-- 组件模板:订单搜索框 --> <div class="search"> <!-- 1 插值:单向出,类 → 文本 --> <h3>订单检索(共 {{ total }} 条)</h3> <!-- 2 属性绑定:单向出,类 → 元素属性/组件输入/指令输入 --> <input [value]="keyword" [placeholder]="hint" [disabled]="querying"> <!-- 3 事件绑定:单向进,模板 → 类方法 --> <button (click)="search()" [disabled]="querying">查询</button> <!-- 4 双向绑定:进出都有,语法糖等价于 [value]="keyword" + (valueChange)="keyword = $event" --> <input [(ngModel)]="keyword" name="kw"> </div>
属性绑定与插值的等价性值得点破:{{ keyword }} 本质是 [textContent]="keyword" 的简写。也就是说,插值没有特殊通道,它就是一次属性绑定,检查时同样走"求值 → 比对 → 写入"流程。
香蕉语法 [(ngModel)] 的展开形式务必亲手写一遍,双向绑定失灵时全靠它排错——十有八九是输出事件名对不上(框架约定输入属性名加 Change 后缀构成输出名)。
绑定表达式只在变更检测运行时求值,但"每轮都求值"意味着成本与绑定数量成正比。看一组对照:
@Component({ selector: 'app-cost-demo', template: ` <!-- 差:每轮检查都调用排序,列表大时每轮白付一次排序成本 --> <li *ngFor="let o of sortByAmount(orders)">{{ o.orderNo }}</li> <!-- 好:数据变化时在类里排好,检查只读字段 --> <li *ngFor="let o of sortedOrders">{{ o.orderNo }}</li> <!-- 差:每轮 new 一个新数组/新对象,引用永不相等,浪费比对 --> <app-order-list [orders]="[...orders]"></app-order-list> <!-- 好:传既有引用,引用不变则子组件输入判定为"没变" --> <app-order-list [orders]="orders"></app-order-list> ` }) export class CostDemoComponent { orders = [ { orderNo: 'A-1', amount: 199 }, { orderNo: 'A-2', amount: 88 } ]; sortedOrders = [...this.orders].sort((a, b) => b.amount - a.amount); sortByAmount(list: { orderNo: string; amount: number }[]) { return [...list].sort((a, b) => b.amount - a.amount); } }
由此得出模板表达式三条禁令:不调用有副作用或重计算的方法;不做赋值;不创建新对象字面量当输入传。禁令的共同根源就一条——表达式每轮检查都会重算,你写的不是"执行一次"的代码,是"每轮执行一次"的代码。
💡 直觉:把模板表达式当成数据库视图——必须无副作用、可重复求值、足够廉价。
| 语法 | 方向 | 目标 | 求值时机 |
|---|---|---|---|
{{ expr }} |
类→视图 | 文本内容 | 每轮检查 |
[prop]="expr" |
类→视图 | 元素属性、组件/指令输入 | 每轮检查 |
(event)="stmt" |
视图→类 | DOM 事件、组件输出 | 事件发生时 |
[(x)]="field" |
双向 | 输入+同名输出 | 两者各按各的时机 |
#ref / @ViewChild |
视图→类 | 元素/组件引用 | 视图初始化后 |

背景:运营后台订单搜索,要求输入停顿 300 毫秒再触发查询,避免每敲一键打一次接口。
操作:表单控件的 valueChanges 流上做防抖(RxJS 细节在 2.10 节),结果写入组件字段:
import { Component } from '@angular/core'; import { FormControl } from '@angular/forms'; import { debounceTime, filter } from 'rxjs/operators'; @Component({ selector: 'app-order-search', template: ` <input [formControl]="kw" placeholder="输入单号或关键词"> <p *ngIf="lastQuery">最近查询:{{ lastQuery }}({{ resultCount }} 条结果)</p> ` }) export class OrderSearchComponent { kw = new FormControl(''); lastQuery = ''; resultCount = 0; constructor() { this.kw.valueChanges.pipe( debounceTime(300), // 停顿 300ms 才放行 filter(v => (v ?? '').length >= 2) // 至少两个字符 ).subscribe(v => { this.lastQuery = v ?? ''; this.resultCount = 42; // 实际场景换成 HTTP 调用 // 订阅回调属于异步事件,结束后一轮检查自然到来 }); } }
结果:连续快速输入时界面不闪、请求不发;停顿后一次更新。
解读:防抖把高频数据变更合并成低频,检查轮数随之下降——这是"减少被检查次数"思路的第一次亮相,第 3 章性能一节会把它系统化。
变式:把 debounceTime 换成 throttleTime(节流,固定间隔放行一次),适合滚动加载类"宁多勿漏"的场景;两种合并策略的选择标准是用户对中间态的容忍度。
下一节把模板的表达力扩展出去:指令与管道。