Angular 双向绑定的二三事
Angular 双向绑定
利用一个例子来看看破坏双向绑定
// parent component html
<child [val]='val'/>
<button (onClick)="val=1">Reset</button>
// child component html
<span>{{val}}</span>
<button type="button" (onClick)="val=val+1">increase</button>
parent component 传递一个val给child component, 同时child component 内部有个button ,点击会加1,那么现在进行如下操作,首先,val 的值是1, 然后,我们点击 child component 内部的button, 最后,我们点击parent component内部的button reset. 那么val 值在两个组件中的值为多少,显示为多少。 大家猜猜,结果看下图。
步骤 | val in parent | val in child | val in display |
---|---|---|---|
初始 | 1 | 1 | 1 |
点击btn in child | 1 | 2 | 2 |
点击btn in parent | 1 | 2 | 2 |
为什么,Angular 怎么不工作了 | |||
先思考这几个问题 |
- Angular 是如何发现组件的差别,从而触发更新的
Angular 在每个组件内部(具体放在哪里的还得看源码,但是肯定是跟组件对象instance 关联),缓存了所有的Input, 每次在检查组件是否要更新时,都会用最新的Input 更缓存的Input 进行比较,找到差异,差异在ngOnChanges() hook 函数内体现。有两个问题,- 比较什么,
以上面的例子为例,假设我们正在对parent component 进行changeDection, 首先,我们比较的东西是<child [val]='val'/>
里面的 ='val' 计算的结果。所以,在parent component 内部肯定缓存了它之前的值。比较之前的值跟最新的值得差异,从而决定是否将哪些Input 更新到Child component 的,同时也会刷新其在parent component 的缓存。所以缓存的数据结构肯定类似于{componentInstance:{...allInputs}}, 如果发现差异,只会将差异更新到childComponent, 我们可以通过在child component 内部的ngOnChanges(changes:SimpleChanges)
拿到需要更新的Inputs。 - 怎么比较?
我猜跟React 里面类似吧,说白了就是对象比较引用,primitive type 直接值比较
- 比较什么,
那我们来分析一下上面例子。
步骤 | val cache of child in parent | val in child |
---|---|---|
初始化后 | 1 | 1 |
点击increase in child | 1 | 2 |
点击reset in parent | 1 | 2 |
在我们点击increase in child 后,在parent component 进行changedetection 时,val 在parent 内部,已经缓存的值都是1, 所以,对于parent component 而言,child component 的Input 没有变化,当我们点击reset 时同样的,val在parent 内部还是1,跟缓存的值一样,所以,child component Input 还是没有变化,Angular不会将1赋值给child component, 但是child component 内部 val 值一直是2, 这就是为什么Angular 不工作的原因。 |
怎么解决这个问题?
问题的根源是在parent 内部,child Input val 缓存的值跟child 内部实际的val 值不一致。那么我们有两个思路,
- 第一种,我们只要告诉Angular 或者,让Angular感知到val 变化了,看如下代码
<button (onClick)="val=3;setTimeout(()=>val=1,0)">Reset</button>
我们先把val设置成一个第三状态值,然后,异步再设置成我们想要的值,先设置成第三状态值,是为了区别于缓存的1,然后Angular 更新值缓存,接着后面的setTimeout,把值再设置成我们要的值,Angular 再次介入,更新。这是一种hack的方式。
- 第二种方法,我们利用双向绑定,也就是child 的内部不会直接更新val 的值,而是把更新的值通知parent ,由parent 来更新,然后在changedetection 里面,child 拿到更新后的值,更新DOM, 如果有同学熟悉React 的话,这就是controlled component 跟uncontrolled component。Angular 里面提供了语法糖,不用像React 那么累。看代码
// parent component
<child [(val)]='val'/>
// child component ts
@Input()
val:any;
@Output()
valChange:EventEmitter<any>=new EventEmitter<any>()
increase(){
this.valChange.emit(this.val+1);
}
<button type="button" (onClick)="increase()">increase</button>
当点击child 里面的increase button 时,child component 并不直接修改val,而是把修改后的值通知parent,parent利用语法糖,自动更新val ,在随后的changedetection中,把更新后的值赋给child.
this.valueChange.emit()是不是实时的? 答案是:是的
如果Input 不是一个primitive type,而是一个对象,会怎么样,如果是对象一些就简单多了,因为对象是地址引用的比较,所以只有引用不变,Angular 一律认为没有改变。
这里强调一点,尽管Angular 发现一些组件的Input 没有发生变化,Angular 还是会递归的对其内部的组件做ChangeDetection,以及Dom的刷新,ChangeDetection 的目的是发现差异,并且将差异传递给目标组件,如果目标是native html element, 那么就是更新Dom,如果是component,direcrive, 那么就更新Input,然后递归ChangeDetection.(不管目标组件有没有Input 更新,都会做,当然有特殊,例如Onpush,等)