Angularjs与Angular2双向绑定原理

1、angularjs(1.x)数据双向绑定原理

AngularJs 为 scope 模型上设置了一个 监听队列,用来监听数据变化并更新 view 。每次绑定一个东西到 view(html) 上时 AngularJs 就会往 watchers 队列里插入一条watch,用来检测它监视的 model 里是否有变化的东西。当浏览器接收到可以被 angular context 处理的事件时,digest 循环就会触发。digest 会遍历所有的 $watch。从而更新DOM。

watch 这有点类似于我们的观察者模式,在当前作用域scope下,我们创建一个监控器watchers和一个监听器watch,watchers 负责管理所有的watch,当我们每次绑定到UI上的时候就自动创建一个watch,并把它放到watchers。

注:即便我们在scope上添加了两个变量,但是只有一个绑定在了UI上,因此在这里只生成了一个watch。

digest 当浏览器接收到可以被angular context处理的事件时,digest循环就会触发。digest将会遍历我们的watch,如果watch没有变化,这个循环检测就将停止,如果有至少一个更新过,这个循环就会再次触发,直到所有的watch都没有变化。这样就能够保证每个model都已经不会再变化。这就是脏检查(Dirty Checking)机制。

applyapply 我们可以直接理解为刷新UI。如果当事件触发时,你调用apply,它会进入angular context,如果没有调用就不会进入,之后的digest检测机制就不会触发。

总结:

  • 只有在scope变量绑定到页面上,才会创建watch
  • $apply决定事件是否可以进入angular context
  • $digest 循环检查model时最少两次,最多10次(多于10次抛出异常,防止无限检查)
  • AngularJs自带的指令已经实现了$apply,所以不需要我们额外的编写
  • 在自定义指令时,建议使用带function参数的$apply

2、Angular2中的双向绑定原理

双向绑定,也是Angular2的核心概念之一,Angular2的双向绑定是这样的:

  • data=>view:数据绑定,模板语法是 []
  • view=>data:事件绑定,模板语法是 ()
  • Angular其实并没有一个双向绑定的实现,他的双向绑定就是数据绑定+事件绑定,模板语法是 [()] 。

Angular2官方给的例子:

<!--value是数据绑定,input是事件绑定-->
<input [value]="currentHero.name"   
       (input)="currentHero.name=$event.target.value"
       >
<!--等价-->
<input [(ngModel)]="currentHero.name">

上面是input空间的双向绑定语法,很清楚的说明了双向绑定与两个单向绑定的关系。这里没有使用ngModule语法,ngModule语法内部实现与这个差不多。

事件绑定

  1. 用户操作出发DOM事件通知
  2. Angular监听到了通知,然后执行模板语法,上面的例子就是将input控件的输入值赋给了currentHero.name

数据绑定

由于js语言并没有属性变化通知的机制,所以angular也不知道谁发生了变化,在什么时候变了。Angular的变化机制是:

image

上面的例子中input的数据绑定过程如下:

  1. 代码修改了currentHero.name的值。
  2. 触发整个组件树的变化检查。
  3. input显示了修改后的值。
数据何时变化

主要入下集中情况可能改变数据:

  • 用户输入操作,比如点击,提交等。
  • 请求服务端数据。
  • 定时事件,比如setTimeoutsetInterval

这几点有个共同点,就是他们都是异步的。也就是说,所有的异步操作是可能导致数据变化的根源因素。

如何通知变化

在Angularjs中是由代码$scope.$apply()或者$scope.$digest触发,而Angular2接入了ZoneJS,由它监听了Angular所有的异步事件。ZoneJS重写了所有的异步API(所谓的猴子补丁,MonkeyPath)。ZoneJS会通知Angular可能有数据发生变化,需要检测更新。

变化检测原理 -- 脏检查

所谓脏检查就是存储所有变量的值,每当可能有变量发生变化需要检查时,就将所有变量的旧值跟新值进行比较,不相等就说明检测到变化,需要更新对应的视图。

AngularJS与Angular2变化检测的区别

Angularjs的变化检测机制也是脏检查,而Angular2的变化检测性能比Angularjs提升了很多。

Angular2

Angular的核心是组件化,组件的嵌套会使得最终形成一棵组件树。Angular的变化检测可以分组件进行,每个组件都有对应的变化检测器ChangeDetector。可想而知,这些变化检测器也会构成一棵树。

另外,Angular的数据流是自顶而下的,从父组件到子组件单向流动。单向数据流向保证了高效、可预测的变化检测,尽管检查了负组件之后,子组件可能会改变父组件的数据使得父组件需要再次被检查,这是不被推荐的数据处理方式。在开发模式下,Angular会进行二次检查,如果出现上述情况,二次检查就会报错:ExpressionChangedAfterItHasBeenCheckedError。而在生产环境中,脏检查只会执行一次。

Angularjs

相比之下,Angularjs采用的是双向数据流,错综复杂的数据流使得他不得不多次检查,使得数据最终趋向稳定。理论上,数据永远不可能稳定,Angularjs的策略是,脏检查超过10次就认定程序有问题。

image

变化检测优化

优化策略

有2个思路:

  1. OnPush策略:我知道我没变,别查我。
  2. 手动控制刷新:我变了,只查我。

变化检测策略 OnPush

Angular还让开发者拥有制定变化策略的能力。

export enum ChangeDetectionStrategy { 
  OnPush, // 表示变化检测对象的状态为`CheckOnce` 
  Default, // 表示变化检测对象的状态为`CheckAlways`
}

ChangeDetectionStrategy可以看到,Angular有两种变化检测策略。Default是Angular默认的变化检测策略,也就是脏检查(只要有值发生变化,就全部检查)。开发者可以根据场景来设置更加高效的变化检测方式:OnPushOnPush策略,就是只有当输入数据的引用发生变化或者有事件触发时,组件进行变化检测。

@Component({
  template: `
    <h2>{{vData.name}}</h2>
    <span>{{vData.email}}</span>
  `,
  // 设置该组件的变化检测策略为onPush
  changeDetection: ChangeDetectionStrategy.OnPush
})
class VCardCmp {
  @Input() vData;
}

比如上面这个例子,当vData的属性值发生变化的时候,这个组件不会发生变化检测,只有当vData重新赋值的时候才会。一般,只接受输入的木偶子组件(dumb components)比较适合采用onPush策略。

那什么时候只要对象的属性值发生变化,整个对象的引用就变了呢?不可变对象(Immutable Object)。当组件中的输入对象是不变量时,可采用onPush变化检测策略,减少变化检测的频率。换个角度来说,为了更加智能地执行变化检测,可以在只接受输入的子组件中采用onPush策略。

手动控制变化检测

Angular不仅可以让开发者设置变化检测策略,还可以让开发者获取变化检测对象引用ChangeDetectorRef,手动去操作变化检测。变化检测对象引用给开发者提供的方法有以下几种:

  • markForCheck():将检查组件的所有父组件所有子组件,即使设置了变化检测策略为onPush
  • detach():将变化检测对象脱离检测对象树,不再进行变化检查;结合detectChanges可实现局部变化检测。(采用onPush策略之后的组件detach()无效)
  • detectChanges():将检测该组件及其子组件,结合detach可实现局部检测。
  • checkNoChanges(): 检测该组件及其子组件,如果有变化存在则报错,用于开发阶段二次验证变化已经完成。
  • reattach():将脱离的变化检测对象重新链接到变化检测树上。

那么,如果是Observable的话,它会订阅所有的变量变化,只要在订阅回调函数中手动触发变化检测即可实现最小成本的检测(仍采用onPush变化检测策略)。举个例子:

@Component({
  template: '{{counter}}',
  changeDetection: ChangeDetectionStrategy.OnPush
})
class CartBadgeCmp {

  @Input() addItemStream:Observable<any>;
  counter = 0;

  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++;        // 数据模型发生变化
      this.cd.markForCheck(); // 手动触发检测
    })
  }
}

另外,当数据模型变化太过频繁,我们可自定义变化检测的时机。举个例子:

@Component({
  template: `{{counter}}
  <input type="check" (click)="toggle()">`, 
})
class CartBadgeCmp { 
  counter = 0;
  detectEnabled = false;

  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    // 每10毫秒增加1
    setInterval(()=>{this.counter++}, 10);
  }

  toggle(){
      if( this.detectEnabled ){
          this.cd.reattach();  // 链接上变化检测树
      }
      else{
          this.cd.detach(); // 脱离变化检测树
      }
  }
}

Angular2与Angularjs比较

Angular2与Angularjs都采用变化检测机制,前者优于后者主要体现在:

  • 单项数据流动
  • 以组件为单位维度独立进行检测
  • 生产环境只进行一次检查
  • 可自定义的变化检测策略:DefaultonPush
  • 可自定义的变化检测操作:markForcheck()detectChanges()detach()reattach()checkNoChanges()
  • 代码实现上的优化,据说采用了VM friendly的代码。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335