原文:https://zhuanlan.zhihu.com/p/58787662
双向数据绑定是AngularJs的一大卖点,当初问世时开发人员无不惊讶,“Wow, it's so crazy"。但是用过AngularJs的,都对它又爱又恨,爱的是它确实给开发提供了一定的便利,恨的是基于‘脏检查’的变更检测机制会随着watch的数据量的增加拖慢应用运行的速度。于是乎,goolge在2016年推出了angular彻底改变了检测机制,这次并没有大力吆喝双向数据绑定,但仍会有人习惯的问一句,“有没双向数据绑定?”。如果你只是随口一问,我会告诉你,有。如果你仍然“死缠烂打”的追问倒底有没有,我会告诉你,**没有**。
像AngularJs中一样使用双向绑定
在AngularJs中,双向数据绑定的写法:
<input ng-model="name" />
// controller.js
...
$scope.name = 'John';
...
Angular中的写法:
<input [(ngModel)]="name" />
// component.ts
...
name = 'John';
...
写法上略有不同,目的和实现的效果却是一样的,当js或ts文件中的name值发生变化时,html模板中的值会发生改变,反之,当用户在input中输入值的时候,js或ts文件中name的值也会发生相应的改变,这就是让很多人念念不忘的双向数据绑定。
AngularJs接下来会设置$watch,进入digest循环,然后循环检测等等,背后发生的一切各位看官有兴趣自行google,这里就不再赘述。你肯定会关心的是,Angular不是明明实现了双向绑定吗,为什么文章开头会说,没有?已经2019了,该忘的东西还是忘了吧,这不是喜新厌旧,应该是与时俱进。
Angular中的’双向数据绑定‘
没有黑魔法
Angular努力拥抱web标准,不创造新名词,也不使用什么黑魔法,那么双向绑定是如何实现的呢?事实上通过属性绑定和事件,这并不难做到。
<input [value]="name" (input)="name = $event.target.value" />
// component.ts
...
name = 'John';
...
上面这段代码中,组件中的属性绑定到了input元素的value属性,自然input的初始值就应该是’John‘。input元素上会产生input事件,通过监听这个事件把name重新赋值。
与其关心双向绑定等黑魔法(实际还算不上黑魔法),倒不如去关心‘输入和输出’。
模板上[]的语法代表了输入,html元素或组件通过这种语法接收输入值。
模板上()的语法代表了输出,html元素通过事件或者组件通过EventEmitter向外输出值。
$event可以视作获取输出的关键字,不同场景下代表的对象是不同的,上面这段代码中由于是监听了input事件,所以它代表的就是 InputEvent,通过属性查询我们获取到了事件上传递的值。
照葫芦画瓢
上面代码现在看起来和之前使用的‘双向绑定’不太一样,但是这只不过是表象。
<input [ngModel]="name" (ngModelChange)="name = $event" />
ts代码没什么变化,这里就省略了。
依然是有输入,有输出,只不过属性名称由value变成了ngModel,事件名称由input变成了ngModelChange。
在不看源码的情况下,如果是让你去实现 ngModel 这个指令,相信你肯定有思路。
- 肯定要把输入属性 ngModel 和input元素的value值关联起来。
2. input的值发生变化后需要使用 ngModelChange 把它发送出来,那ngModelChange肯定是一个EventEmitter。
3. 在赋值的时候直接用的是$event,而不是$event.target.value。这也很容易,要内部实现时取出inputEvent对象的值传递给 ngModelChange 就Ok了。
输入+输出===双向绑定
现在,我们只需要使用简写写法把它们合起来,这就是‘双向绑定’
<input [(ngModel)]="name" />
为什么这样写组件中的数据会被修改?肯定是Angular内部帮你做了啊,要不怎么叫简写定法呢?这些小事框架都不帮忙,要框架何用?当然这只是开个玩笑,如果你愿意的话可以看下源码。对于实现来说需要记住的是,输入属性名称加一个‘Change’后缀,把它定义成EventEmitter就可以了。
自定义双向绑定
按照上面的思路,实现一个双向绑定的步骤:
- 定义一个输入属性(如:name)。
2. 定义一个输出属性,名称就是输入属性名加‘Change’后缀(如:nameChange)。
3. 确保nameChange输出最新的值。
name.component.ts
@Component({
selector: 'name',
template: `
<button (click)="addPrefix()">add prefix</button>
My name is: {{ name }}
<button (click)="addSuffix()">add suffix</button>
`,
})
export class NameComponent {
@Input() name: string;
@Output() nameChange: EventEmitter<string> = new EventEmitter();
addPrefix() {
this.name = '* ' + this.name;
this.nameChange.emit(this.name); // 记得输出新的值
}
addSuffix() {
this.name = this.name + ' *';
this.nameChange.emit(this.name); // 记得输出新的值
}
}
在其它组件中使用这个组件:
app.component.ts
Component({
selector: 'my-app',
template: `<name [(name)]="name" (nameChange)="log()"></name>`,
})
export class AppComponent {
name = 'Angular';
log() {
console.log(this.name);
}
}
注意app组件中的log方法并没有接收参数,而是直接log出组件上name属性的值,这里是为了说明当name的值在子组件中被修改以后,angular帮助我们把 AppComponent 上name的值进行了修改。
下面是输出结果:
![2019-03-0920-15-37_2019-03-0920_17_441552133910640.gif](http://assets.hijavascript.com/2019-03-0920-15-37_2019-03-0920_17_441552133910640.gif)
可见所谓的双向绑定只不过是在输入和输出的基础上的语法糖。