Angular模板语法全解析:从数据绑定到结构指令
很多人第一次翻开 Angular 2 项目看到模板里那堆*ngFor、[(ngModel)]、$event、?.的时候第一反应都是这真的还是 HTML 吗我在前面带过好几个从 Vue 或者 jQuery 时期转过来的同事大家几乎都要在模板语法上卡一两周才缓过来。这篇文章我就照着 Angular 2 的模板语法一条一条拆把我自己踩过的坑、面试时高频出现的点、以及老项目里最常见的写法都揉进去帮你把这一套东西从“会抄”变成“会写”。顺便也把经常被问到的 Angular 和 Vue 模板差别放进来聊一聊因为这俩框架的模板观完全不一样理解了这个差异很多用法背后的“为什么”就自然通了。1. 先建立对 Angular 模板语法的整体认知1.1 模板在 Angular 里到底扮演什么角色Angular 的模板不是传统意义上的模板字符串不是把{{name}}替换成数据就完事。Angular 编译器会把这套模板解析成一组视图定义再通过指令和绑定关系把组件实例和真实 DOM 连接起来。换句话说模板是组件类对外暴露的“视图描述”它写在 HTML 里但本质上是一套有作用域约束的 DSL。组件类里放着数据和业务方法模板负责“这些东西该怎么显示”。两者之间的桥梁就是绑定语法。你写{{ title }}就是在告诉 Angular这个地方要显示组件实例里的title属性并且当title变化时这里要跟着更新。写(click)save()就是在告诉 Angular用户点击的时候调用组件里的save方法。这个观念一定要先立住。很多人学 Angular 模板学得懵是因为总拿原生 HTML 的思维去套看到[disabled]就以为是在操作 HTML 属性看到(click)就当普通事件监听器结果一旦行为不符合预期就不知道怎么排查。实际上 Angular 模板里的方括号、圆括号、星号都是框架层面的语法糖背后是一整套编译和变更检测机制。1.2 看到[]、()、*号时脑子里要有一个翻译器Angular 模板里的符号非常统一核心就几组我建议你先把它们翻译成正常人话{{ }}插值把组件数据渲染到视图中本质上是一种单向绑定。[属性]方括号代表从组件类流向模板/ DOM 的属性绑定等号右边的表达式结果会作为值赋给目标。(事件)圆括号代表从模板/ DOM 流向组件类的事件绑定等号右边写的是组件类里的方法或赋值语句。[(属性)]方括号套圆括号代表双向绑定是上面两种写法的组合。*星号表示这是一个结构型指令它会改变 DOM 结构比如*ngIf和*ngFor。#模板引用变量给当前元素或指令起一个局部名字方便在模板内部引用。把这几个符号装进脑子里以后再去看一个 Angular 模板基本就不会觉得那是天书了。你只需要记住方括号管“输入”圆括号管“输出”星号管“结构”#管“别名”。后面所有复杂的写法都是从这几条基础规则演化出来的。1.3 模板表达式是一套“被阉割”的 JavaScript这是 Angular 模板语法里最容易踩坑的地方。模板里的表达式看着像 JavaScript但能力是受限的。你在里面不能写赋值语句不能写new不能写自增自减也不能用位运算符和分号。比如下面这些写法在 Angular 模板里都会直接报错!-- 这些都是不合法的模板表达式 -- {{ count 1 }} {{ items }} {{ item (name jack) }}模板表达式的设计目标是“返回一个值”而不是“执行一段逻辑”。所以在{{ }}里你可以写三元运算、字符串拼接、方法调用这些都没有问题但要避免写那些带副作用的语句。比如{{ user.age 1 }}没问题{{ save() }}虽然在部分场景能跑但通常不建议在插值里调用有状态变更的方法。模板语句则是另一种情况它出现在事件绑定右边比如(click)save()。模板语句更接近“动作”所以它允许赋值比如(click)count count 1是合法的。但即便允许我也建议逻辑稍微复杂一点就放到组件类的方法里去模板里只留调用不然调试的时候你会满世界找这个count到底在哪被改的。2. 插值、属性绑定与 DOM 属性的边界2.1 插值{{}}从显示数据到踩坑插值是最基础的模板语法它的作用是输出文本内容。p{{ title }}/p这种谁都会写但实际操作里有几个细节是真的要留意的。第一插值表达式里可以调用组件方法但我不推荐你在频繁更新的场景里这么做。Angular 默认的变更检测是全局的组件内任何一个异步事件发生后模板里所有绑定表达式都可能被重新求值。如果{{ formatTitle() }}这个方法内部做了复杂计算或者每调用一次就new一个对象那每次变更检测都会白白消耗性能而且很难排查。第二插值默认会对输出的内容做转义这是 Angular 内置的 XSS 防护。如果后端返给你一段富文本 HTML你直接{{ htmlContent }}是看不到效果页面上只会显示一段转义后的文本。真要渲染 HTML得用[innerHTML]htmlContent绑定并且要配合DomSanitizer标记信任来源。这个操作有安全风险实际项目里如果不是特别可信的内容源我不建议你用。第三如果你想在模板里输出一个花括号字符本身直接写{{ { }}或者{{ } }}因为 Angular 会把裸花括号当作插值的边界解析。2.2 属性绑定为什么用[]而不是{{ }}插值只能输出字符串这一点非常关键。比如你写input disabled{{ isDisabled }}当isDisabled为false的时候页面上的输入框依然会是禁用状态。为什么因为 HTML 属性disabled只要出现在标签里哪怕值是字符串false浏览器也会认为它是布尔真值。这时候正确写法是input [disabled]isDisabledAngular 把isDisabled的布尔值直接绑定到 DOM 对象的disabled属性上false就能真正让输入框可用。这里就引出了 Angular 模板里一个特别重要的概念方括号绑定的是 DOM 属性property不是 HTML 属性attribute。专栏里之前有人问过我 DOM property 和 HTML attribute 到底什么区别我用一个生活化类比解释一下。HTML attribute 是写在标签源码里的“初始配置单”比如input valuehello里的valuehello它只决定元素创建时的初始值。而 DOM property 是元素对象诞生后的“当前状态”比如用户往输入框里打字后input.value这个 property 会跟着变但 HTML 源码里的 attribute 不会变。所以遇到colspan、rowspan、aria-*这类只有 attribute 没有 property 的东西你需要写[attr.colspan]1 1这种形式。遇到value、disabled、checked这类同时存在 property 和 attribute 的东西就要想清楚你到底是希望“设置初始值”还是“控制当前状态”然后用对应的语法。2.3 class 和 style 绑定的三种姿势class 绑定是高频操作Angular 提供了三种写法我分别列一下适用场景。!-- 单个 class 开关 -- div [class.active]isActive菜单项/div !-- 整个 class 对象 -- div [ngClass]{active: isActive, error: hasError}状态盒子/div !-- 整体 class 字符串绑定 -- div [class]customClass自定义样式/div单 class 绑定适合那种“一个条件控制一个类名”的场景可读性最好。[ngClass]适合多个类名同时联动对象里的键是类名值是布尔条件。[class]是整体绑定适合类名完全由组件动态拼接的场景。style 绑定类似也有单属性绑定和对象绑定两种写法还有一个比较实用的小技巧是带单位div [style.color]textColor文字/div div [style.font-size.px]fontSize带像素单位/div div [ngStyle]{background-color: bgColor, line-height.px: lineHeight}样式对象/div[style.font-size.px]这种写法我强烈推荐因为模板里不用再写字符串拼接Angular 会自动把数值拼上单位。唯一的提醒是class 和 style 绑定里的表达式都别写太复杂涉及多个条件判断时先在组件里算好模板只负责展示这是最容易让模板保持整洁的办法。3. 事件绑定与双向绑定的原理和实战3.1 事件绑定()不是回调注册的简写事件绑定的基础写法是(click)onClick()右边可以是方法调用也可以是简单赋值。很多人以为这就是addEventListener的语法糖其实不对。Angular 的事件绑定在框架层做了很多额外处理比如事件作用域、变更检测触发、自定义事件支持等。一个很容易忽略的细节是$event这个变量在事件绑定语句里是自动可用的。对于原生 DOM 事件它代表事件对象你可以这么用button (click)onClick($event)点击/buttononClick(event: Event) { event.stopPropagation(); event.preventDefault(); console.log(event.target); }但对于组件自定义事件$event就变了。比如子组件通过Output() selected new EventEmitterItem()抛出一个值父组件模板里写(selected)handleSelected($event)这里的$event不是 DOM 事件对象而是子组件 emit 出来的那个Item实例。这是新手最容易懵的地方看到$event就把所有事件都当 DOM 事件处理结果取不到target。Angular 还支持带修饰的事件名写法最常用的是键盘事件过滤比如(keyup.enter)search()就只在按下回车时触发比自己在方法里判断event.keyCode干净很多。这点比原生addEventListener友好得多。3.2$event到底是什么能用哪些属性我们展开说一下$event。如果你绑定的是 DOM 事件$event就是浏览器原生事件对象它带target、currentTarget、keyCode、ctrlKey这些原生属性如果你绑定的是自定义组件输出事件$event就是EventEmitter.emit()传入的那个值它可以是一个布尔值、一个字符串、一个对象甚至是void。做个表格放到团队文档里比较好用绑定场景$event是什么例子原生 DOM 事件浏览器原生事件对象(click)onClick($event)键盘事件原生 KeyboardEvent(keyup.enter)search($event)自定义组件事件组件 emit 出来的业务数据(selected)onSelected($event)双向绑定事件更新后的新值(ngModelChange)name $event写组件的时候自定义事件的载荷要设计得尽量小只传必需的字段就好。不要把一个巨大的对象整个 emit 出去再让父组件自己取字段组件之间的接口越精简越容易维护。3.3[(ngModel)]语法糖拆解接下来是 Angular 模板语法里辨识度最高的双向绑定写法[(ngModel)]。它的俗名很形象叫“香蕉盒子”因为括号套方括号看起来就像一个盒子里面放了根香蕉。input [(ngModel)]name placeholder请输入姓名这行代码看起来像是框架黑魔法其实 Angular 只是把它翻译成了两条绑定。等价写法是input [ngModel]name (ngModelChange)name $event第一条方括号把name的值传给输入框第二条圆括号监听ngModelChange事件当输入框内容变化时把新值赋回给name。明白了这层拆解你在排查问题时思路就清晰了如果双向绑定不生效你要分别检查“属性绑定有没有通”和“事件有没有触发”。用[(ngModel)]有个强制要求容易被忽略如果你不是在form表单域中使用它Angular 会强制要求元素带name属性否则报错。脱离表单单独用的话要显式声明input [(ngModel)]name [ngModelOptions]{standalone: true}这行代码背后的意思是“我不在这个表单里上报这个字段请别用表单校验规则约束我”。这个选项在实际项目中很常用特别是在搜索框、独立筛选器等非表单场景中。3.4 双向绑定的进阶用法理解了语法糖之后你还可以自己创建一个支持双向绑定的自定义组件。假设你写了一个评分组件希望父组件能直接写[(rate)]score那么子组件只需要两样东西Input() rate: number 0; Output() rateChange new EventEmitternumber();在评分组件内部用户点击星标时调用this.rateChange.emit(value)这个写法之所以能生效是因为 Angular 双向绑定的约定就是属性名是rate输出事件名必须是rateChange。这套约定对任何组件都成立。实际项目中我更推荐一种做法只有当[(ngModel)]/[(自定义属性)]的语义是“单一值同步”时才用双向绑定。如果同时要绑定的字段超过两个或者更新的时候要做额外处理比如日志埋点、格式化、防抖那就拆成显式的[value]和(valueChange)把逻辑写在组件方法里别让模板承受太多。4. 内置指令实战*ngIf、*ngFor、ngSwitch与星号原理4.1*ngIf开关视图与as语法*ngIf是最常用的结构型指令作用是条件成立时渲染元素条件不成立时直接从 DOM 中移除。注意是“移除”不是隐藏。这两者的差别在实战里影响很大移除意味着节点上的事件监听器、组件实例、数据状态全部销毁重新渲染时重新创建[hidden]只是打个 CSS 样式把元素藏起来组件实例还在。div *ngIfuser p{{ user.name }}/p /div !-- 配合 async 管道把订阅结果存到局部变量里 -- div *ngIfuser$ | async as user p{{ user.name }}/p /div第二种写法是我最推荐的*ngIf用法。as user会把管道或表达式的计算结果存到模板局部变量里后面整个*ngIf块内都可以直接使用。这样既能把空值判断和渲染数据解耦又能避免同一条user$流在模板里被订阅多次。*ngIf的判断标准是真的“逻辑真值”0、空字符串、null、undefined、NaN都会被当成假值。如果业务上你的判断条件是0是否展示比如“库存剩余 0 件时优惠不显示”那要写成*ngIfstock ! 0别直接*ngIfstock否则你排查半天也找不到问题。4.2*ngFor列表渲染与trackBy*ngFor是模板里出现频率最高的指令之一基础写法是ul li *ngForlet item of items; index as i; trackBy: trackByFn {{ i }} - {{ item.name }} /li /ulindex as i把当前索引暴露成局部变量类似的还有first、last、even、odd在列表里做分割线或者序号样式时很好用。真正影响性能的是trackBy。Angular 渲染列表时默认按照对象引用来判断“这一项有没有变”。如果你从后端重新拉了一整个数组即便数据内容没变化只要数组元素不是同一个引用Angular 就会销毁旧 DOM、创建新 DOM。列表小还行几百上千条的时候就能明显感到卡顿。trackBy就是告诉 Angular你按我给的 ID 来追踪列表项而不是按对象引用。trackByFn(index: number, item: Item) { return item.id; }我强烈建议列表项里有唯一id字段的项目都用trackBy这是一个“写起来多一行、性能翻几倍”的优化。还有一个小坑是别用index当trackBy的返回值当列表发生删除、排序时用索引追踪会导致 DOM 复用混乱可能出现复选框选错、输入框内容错位这些诡异问题。4.3ngSwitch家族适合什么场景ngSwitch不是结构型指令但它和*ngSwitchCase、*ngSwitchDefault配合使用可以做到多分支条件渲染。div [ngSwitch]status p *ngSwitchCaseloading加载中.../p p *ngSwitchCasesuccess加载成功/p p *ngSwitchDefault加载失败/p /div它和连续*ngIf的区别在于ngSwitch是对同一个变量做多重匹配语义更清晰后续加分支只需要加一个*ngSwitchCase。要注意ngSwitch的匹配规则是严格全等数字1和字符串1不会互相匹配这个和很多人写if时的宽松习惯不一样接口返回的类型要提前确认好。4.4 为什么星号是指令不是 HTML 属性你可能会疑惑*ngIf这个星号到底是什么意思。它不是 HTML 语法而是 Angular 自己定义的语法糖。div *ngIfshow内容/div会被 Angular 展开成ng-template [ngIf]show div内容/div /ng-templateng-template本身不会渲染任何东西它只是定义了一块“模板片段”由ngIf指令决定是否把这段片段插入到视图中。有了这个理解你就能解释一些诡异规则了同一个元素上不能同时写两个结构型指令比如div *ngIfa *ngForlet item of items是违法的因为一个元素展开后只能对应一个ng-template的宿主位置。遇到需要同时满足多个结构条件的情况可以用ng-container包一层。ng-container不会生成真实 DOM 节点非常适合做纯粹的包装容器ng-container *ngIfvisible div *ngForlet item of items{{ item.name }}/div /ng-container这段模板里ng-container负责外层条件判断div负责内层循环两者各司其职也不会多出无意义的 DOM 层级。这个写法在真实项目里非常常见我至今写列表页条件渲染时都优先用它。5. 模板引用变量、安全导航操作符与管道5.1 模板引用变量作用域没你想象的那么宽模板引用变量用#声明可以用来引用一个 DOM 元素、一个组件实例、一个指令实例甚至通过exportAs引用指令暴露出来的公开 API。input #phone placeholder手机号 button (click)callPhone(phone.value)拨打/button这里phone指向的是原生 input 元素所以模板里可以直接取phone.value。如果想拿到指令实例最常见的是拿 ngModel 的表单状态input #namengModel [(ngModel)]user.name required p *ngIfname.invalid name.touched姓名不能为空/p#namengModel语法表示把这个模板变量绑定到ngModel指令实例上这样name.invalid、name.touched这些状态就能直接在模板里访问。但注意模板引用变量的作用域是有边界的。它只能在声明的那个模板视图范围内使用如果在*ngIf内部声明了变量那么到ng-template之外就访问不到了。相反如果在父元素上声明子元素可以访问到。实际项目里最常见的错误是有人在*ngFor里声明了一个变量然后在循环外面想引用它结果拿不到这时候只能把需要的值提前存到组件属性里。5.2 安全导航操作符?.处理空值要适可而止后端接口返回的数据经常存在嵌套为空的情况比如user.detail.address.city如果user为null直接访问user.detail就会在模板里抛异常。Angular 提供了安全导航操作符?.p{{ user?.detail?.address?.city }}/p当user为null或undefined时整个表达式会短路返回undefined页面显示空字符串不会报错。这个特性在模板里非常实用但我见过不少同事用上头了把整个模板写成一长串?.这样反而掩盖了真正的数据异常。我的建议是只对“可能缺失”的层级使用?.。如果user是必然存在的只是detail可能为空那就写user.detail?.address。如果user、detail、address都可能没有说明接口数据结构本身不稳定更应该在后端或服务层做数据归一化而不是靠模板到处兜底。还有一个容易混淆的!非空断言操作符比如user!.name它的作用是告诉 TypeScript 编译器“我保证user不是空值”帮你通过类型检查。但它不会在运行时做任何保护如果运行时user真的是null照样报错。所以!只能用于你确定不会为空的场景不要拿它当?.的替代品。5.3 管道格式化利器与性能分水岭管道是用一个竖线|对模板数据进行格式化处理比如日期、货币、百分比、大小写转换等。p{{ birthday | date: yyyy-MM-dd }}/p p{{ price | currency: CNY }}/p p{{ content | slice: 0:100 }}.../pAngular 内置了一批常见管道基本覆盖日常需求。如果你想自己定义管道代码也很简单Pipe({ name: phoneMask }) export class PhoneMaskPipe implements PipeTransform { transform(value: string): string { if (!value || value.length ! 11) { return value; } return value.replace(/(\d{3})\d{4}(\d{4})/, $1****$2); } }管道一个容易被忽略的性能特性是“纯管道”和“非纯管道”的区别。默认情况下管道是纯管道pure: true只有输入参数的引用发生变化时管道才会重新执行。这意味着{{ user.name | uppercase }}在每次变更检测时只要user.name的字符串引用没变管道就不会重复计算性能很好。反过来非纯管道pure: false在每一次变更检测时都会执行哪怕输入完全没有变化。Angular 内置的async管道就是非纯管道因为它需要监听流实时更新。自己在写管道时能保持纯管道就保持纯管道这能帮你避开很多不必要的性能损耗。5.4 从async管道说开去async管道是 Angular 处理异步数据的核心工具它接收一个Observable或Promise自动订阅拿到值后渲染到视图并且在组件销毁时自动退订。这样一来你就不用在组件里手动管理订阅和退订代码简洁不少。p{{ user$ | async }}/p但async管道用起来不是没有代价。每写一个| async都会创建一次独立的订阅如果你在模板里同一个流用了两遍就会订阅两次。和*ngIf配合时我最推荐前面说的as语法div *ngIfuser$ | async as user p{{ user.name }}/p p{{ user.email }}/p /div这样一条流只订阅一次变量名user还能复用。如果流在做网络请求这种写法能避免同一个接口被发两次这是实际项目中特别实用的优化点。6. 常见问题与排查技巧实录6.1 模板表达式里的常见报错我整理了这几年被问得最多、也是最容易卡住新手的几类模板报错建议收藏对照排查。报错信息原因与解决Cant bind to ngModel since it isnt a known property当前模块没有导入FormsModule在NgModule的 imports 里补上即可。ngForOf is not a known property*ngFor拼写错误或者当前模块没有引入CommonModule。Property xxx does not exist on type YourComponent模板表达式引用了组件中不存在的属性或方法检查组件类定义。ExpressionChangedAfterItHasBeenCheckedError模板绑定值在变更检测的第二次检查中发生了变化常见于ngAfterViewInit里改数据、子组件回调里改父组件数据检查生命周期里是否有数据赋值操作。Cannot read property xxx of null访问了空对象的属性用?.处理或保证数据初始化非空。ExpressionChangedAfterItHasBeenCheckedError这个报错我单独说一下。Angular 开发模式下一次变更检测后会再额外跑一遍检查确保数据稳定。如果第一轮结束后的值第二轮检查时发现变了就会抛这个错。最常见的触发场景是在ngAfterViewInit里写this.isLoading false因为视图初始化完成后你又改了数据Angular 认为这可能导致视图和状态不一致。解决思路一般是把数据更新放到下一个异步周期里比如setTimeout或Promise.resolve()或者重新审视这种“视图初始化后又改数据”的设计是否必要。6.2 列表不更新、class 不生效的排查思路很多人在*ngFor渲染之后发现列表不更新第一反应是去改模板其实问题往往出在数据变更检测上。Angular 组件默认是Default变更检测策略大多数情况下数据变了视图都会更新。但如果你给组件加了OnPush策略Angular 就只在输入引用变化、事件触发、Observable 触发等少数情况才检查组件。OnPush策略下直接对数组做push、对对象做属性修改视图不会更新。你需要创建新的数组或对象// 不生效 this.items.push(newItem); // 生效 this.items [...this.items, newItem];class 不生效则要检查绑定目标。[class.active]condition是给 class 列表追加或移除active[ngClass]是批量处理。不要在一个高层级元素上用[class]整体绑定。因为[class]绑定的是一个整体字符串它和静态 class 的共存行为在早期版本里容易产生预期之外的覆盖我建议统一用[ngClass]或单 class 绑定语义最清晰。6.3 一个可以抄进项目的模板规范项目里模板写多了以后我总结了一套自己的模板约束规则现在分享出来模板里不写超过两个三元运算符嵌套的表达式超过就提取成组件方法或 getter。模板是给人看的不是用来炫技的。一个表达式如果超过一行还读不明白说明这段逻辑应该出现在组件类里。不把同一个复杂表达式复制粘贴到多处的模板位置改用管道、getter 或计算属性。重复本身就是坏味道何况 Angular 模板里的表达式还会被多次求值。列表渲染统一写trackBy结构指令统一使用ng-container包裹避免多余 DOM 层级。这样虽然代码会多一点但性能问题和样式问题都会少很多。模板里不直接调用返回新对象的方法比如{{ getConfig() }}如果在方法里return { ... }每次变更检测都会生成新对象即使逻辑结果没变Angular 也会认为引用变了导致不必要更新。改成预计算好的组件属性或者用纯管道性能会稳定得多。7. 面试视角Angular 模板和 Vue 模板的区别7.1 模板写法的表层差异每次面试我都会被问“Angular 和 Vue 有什么区别”模板语法是最直观的一个切入点。Angular 用方括号[]绑定属性圆括号()绑定事件[()]做双向绑定Vue 用:或v-bind:绑定属性或v-on:绑定事件v-model做双向绑定。两者功能上高度对应但 Angular 更强调“语法符号的统一性”Vue 则更贴近原生 HTML 的写法。结构型指令的差别更明显。Angular 用*ngIf、*ngFor这种星号语法Vue 用v-if、v-for作为普通 HTML 属性。Angular 的星号底层是ng-template的语法糖Vue 的v-if/v-for虽然也是指令但在普通属性上做条件渲染初学者更容易理解。模板引用变量方面Angular 用#nameVue 用refnameAngular 的临时变量作用域更局部Vue 的 ref 更多用于获取元素或组件实例。管道 / 过滤器的差异也值得一提Angular 至今保留管道机制Vue 2 有过滤器Vue 3 已经把它移除了官方建议用计算属性或方法替代这背后是两个框架对“视图转换逻辑”归属地不同的理解。7.2 变更检测机制带来的底层差异模板语法的表层差异其实根植于底层机制不同。Angular 默认的变更检测是全局性的它会在每次异步事件点击、请求、定时器发生后对组件树做一轮检查把模板表达式重新求值一遍对比值和上一次是否相同不同就更新 DOM。配合 Zone.jsAngular 能捕获到很广泛的异步操作但也因此对模板表达式的纯度要求很高。Vue 用的是响应式依赖追踪模板在渲染过程中会记录它访问了哪些响应式数据当这些数据变化时只更新依赖这些数据的组件和模板片段。所以 Vue 模板里写方法调用时影响范围比 Angular 更局部这也让 Vue 新手写代码时没那么敏感不太容易因为“方法被重复调用”而踩坑。我这里列一个差异表对比项AngularVue属性绑定[property]expr:propertyexpr或v-bind:property事件绑定(event)handlereventhandler或v-on:event双向绑定[(ngModel)]v-model条件渲染*ngIfv-if列表渲染*ngFortrackByv-for:key格式化管道|Vue 3 用计算属性或方法变更检测默认全组件树检查基于响应式依赖追踪模板表达式限制较强不能访问全局、不能写语句同样限制全局访问但模板更贴近 JS 环境7.3 模板能力边界与团队心智负担从团队角度看Angular 模板是一门更“重”的 DSL学习曲线陡但几个核心语法符号就能通吃绝大多数场景规则明确适合大项目和规范化团队。Vue 模板上手快写法灵活但复杂业务场景里你经常要判断“这条逻辑是放模板、放计算属性还是放 watch”反而需要团队自己定规范。面试的时候回答“Angular 和 Vue 的区别”千万别只停在“Angular 用[]和()Vue 用:和”这种表层。更好的思路是顺着模板差异讲到编译模型Angular 的模板会经过编译器处理在构建期就绑定关系和变更检测函数Vue 则通过虚拟 DOM 和响应式系统在运行时完成更新。前者更像是声明式“画地图”后者像是“按需导航”没有绝对好坏但能看出你对框架本质是否理解到位。我个人的体会是Angular 模板语法本质上就那几块拼图插值负责输出方括号负责输入圆括号负责输出事件星号负责结构管道负责格式化引用变量负责局部别名。把这几个映射关系记牢再复杂的老项目模板也能一眼看明白它到底在干什么。如果你正在学或者正在面试建议动手把常见业务页面用这套规则全部写一遍比刷十篇解析文章都管用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →