Angular—都2019了,你还对双向数据绑定念念不忘

原文: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 这个指令,相信你肯定有思路。

  1. 肯定要把输入属性 ngModel 和input元素的value值关联起来。

2. input的值发生变化后需要使用 ngModelChange 把它发送出来,那ngModelChange肯定是一个EventEmitter。

3. 在赋值的时候直接用的是$event,而不是$event.target.value。这也很容易,要内部实现时取出inputEvent对象的值传递给 ngModelChange 就Ok了。

输入+输出===双向绑定

现在,我们只需要使用简写写法把它们合起来,这就是‘双向绑定’

<input [(ngModel)]="name" />

为什么这样写组件中的数据会被修改?肯定是Angular内部帮你做了啊,要不怎么叫简写定法呢?这些小事框架都不帮忙,要框架何用?当然这只是开个玩笑,如果你愿意的话可以看下源码。对于实现来说需要记住的是,输入属性名称加一个‘Change’后缀,把它定义成EventEmitter就可以了。

自定义双向绑定

按照上面的思路,实现一个双向绑定的步骤:

  1. 定义一个输入属性(如: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](/2019-03-0920-15-37_2019-03-0920_17_441552133910640.gif)

可见所谓的双向绑定只不过是在输入和输出的基础上的语法糖。