#5 watch机制,digest process详讲

watcher是Angular提供的一种对变量的观察机制,如果存在数据的绑定等,则在不同的作用域中都存在相应的 watch list, 这些被观察的变量将在 digest process (后面会详讲)中得到更新,这也称之为 脏值检查

watch 和 watch listener

$scope (和$rootScope) 对象上存在 $watch 函数,我们可以手动的添加watch listener, 当被观察的变量被检测到发生变化时,变量被更新,然后angularjs自动调用 watch listener.

// 观察 'a' 变量的变化
// 后面的回调函数就是 watch listener, 2个参数: newValue oldValue
$scope.$watch('a', function(newValue, oldValue) {
  if (newValue != oldValue) {
    $scope.b = $scope.a * 2
  }
})

watches的类型

1.$watch - Reference watch 引用类型的观测

$watch 是一种引用类型的观察,只有当引用的地址发生变化时才触发,上面的代码使用原始类型,当值变化时,相应的引用也会发生变化。下面例子中 obj 是一个对象,只有 obj 引用的地址发生变化时,才会调用watch listener, 因此当 a, b, c的值发生变化时,obj引用的地址并不会发生改变,因此不会调用watch listener,这一点要注意:

$scope.obj = {
  a: 1,
  b: 2,
  c: 4
}
// 引用观察
$scope.$watch('obj', function(newValue, oldValue) {
  if ($newValue != oldValue) {
    $scope.obj.c = $scope.obj.a * $scope.obj.b; 
  }
});

$watch with 'true' - Equality watch

同上面的例子,如果后面再添加一个 'true', 则观察的类型将变为相等性观察(也可称为deep watch)。这样对象的成员发生变化时,会自动调用watch listener

$scope.obj = {
  a: 1,
  b: 2,
  c: 4
}
// 相等性观察
$scope.$watch('obj', function(newValue, oldValue) {
  if ($newValue != oldValue) {
    $scope.obj.c = $scope.obj.a * $scope.obj.b; 
  }
}, true);

2.$watchGroup - Reference watch for multiple variables

$watchGroup 对多个变量同时进行监测,这可以说是一种简便的写法

$scope.a =1;
$scope.b = 2;
$scope.c = 4;

// 相等性观察
$scope.$watchGroup(['a', 'b'], function(newValue, oldValue) {
  if ($newValue != oldValue) {
    $scope.obj.c = $scope.obj.a * $scope.obj.b; 
  }
});

3.$watchCollection - Collection Watch

这个是对集合元素的观测:

  • 用于观测数组
  • 检测数组的变化(比如添加或删除元素)
  • 不检测数组item的变化(比如数组的元素为对象,对象中的内容发生变化,这并不再监察范围内, 如果希望监察这种变化,可以添加 true, 同$watch)
$scope.emps = [
  { empId: 1001, empName: 'James'},
  { empId: 1002, empName: 'Kobe'},
  { empId: 1003, empName: 'Durant'},
  { empId: 1004, empName: 'Jordan'}
];

// 如果emps数组增加元素或删除元素,则会调用watch listener
// 如果 '$scope.emps[2].empName="Harden"' 并不会调用watch listener
$scope.$watchCollection('emps', function(newValue, oldValue) {
  //...
})

如果希望 '$scope.emps[2].empName="Harden"' 内容的变化调用watch listener, 则可以添加 true, 变为相等性监察

$scope.$watchCollection('emps', function(newValue, oldValue) {
  //...
}, true)

示例:

// html
<div ng-controller="myCtrl">
  a = {{a}} <input ng-model="a" ng-change="updateC()" />
</div>

// js
app.controller('myCtrl', function($scope) {
  $scope.a = 10;
  $scope.c = 1;

  $scope.updateC = function() {
    $scope.c = $scope.a * 2
  }

  $scope.watch('c', function(newValue, oldValue) {
    if (newValue != oldValue) {
      console.log('C updated to ' + newValue)
    }
  })
})

// 当我们改变 'a' 的值的时候,比如10, 'ng-change' 将调用 'updateC()'函数
// 从而改变c的值
// 然后c被检测到发生变化,watch listener调用
// 控制台显示 'C updated to 20'

值得注意的是, ng-change 指令的用法, 当a的值发生变化时,将自动的调用其绑定的函数

digest process/loop

通过上面的watch,我们知道,作用域中的可能存在watchers, 这些watcher都存在各个作用域中的 watch list中,如下图:

![Uploading digest in simple words_926951.jpg . . .]

digest process 简单的来讲就是: 负责遍历整个watch list,查看变化(也称之为 dirty-checking(脏值检查)),有如下特点:

  • 被观察的变量有变化,则更新变量,如果存在watch listener, 则执行watch listener
  • 追踪变化,告知Angular更新DOM
  • Digest Process 完成之后更新DOM
  • Digest Process 运行在 Angular Context 中,这是angularjs运行时环境,这个环境建立在Javascript Context

如下图:

digest in simple words.jpg

举个例子: watch list中的 a 变量,检测到发生了变化:

  • AngularJS 进行第1次遍历,发现a有变化,更新a的值
  • a如果存在watch listener, 则执行这个函数
  • AngularJS再次进行遍历,查看是否又有变化,如果没有变化,则Digest process完成,更新DOM;如果又发现变化,则再次进行遍历,直到没有任何变化

如下图所示:


digest process - cycle.jpg

从图中可以看出:

  • AngularJS Digest process 至少遍历2次, 最多遍历10次(超过10次抛出错误)

我们可以使用 $rootScope.watch的方法来查看初始次数:

$rootScope.watch(function() {
  console.log('Digest process start');
})

// 在控制台中我们可以看到
Digest process start
Digest process start

// 这个过程执行了2次

digest process 完整过程

digest process并不会定时的触发,而是通过下图中的一些条件引起而触发:


digest process.jpg

从图中可以看出,其中几个触发条件为(在AngularJS context中):

  • DOM 事件(ng-click等)
  • Ajax 请求($http等)
  • Timer 回调($timeout $interval)
  • 浏览器地址发生变化
  • 手动的调用($apply $digest)

Angularjs 也正是通过上面这些情形当作桥梁和浏览器产生关联的

和angular不相关的DOM事件,比如 onclick, 并不会触发digest process,而 ng-click 则会。

Angular context位于Javascript Context 中,浏览器的一些行为会影响到Angular context中的一些触发条件,从而触发digest process

$apply && $digest

这2个方法用于手动触发digest process:

  • 主要用于当作用域变量在Angular Context 之外(比如使用jquery 事件, jquery ajax等)被修改,而 UI 需要刷新数据绑定
  • $apply 本质上是调用 $digest
  • 使用方法: $scope.$apply() | $scope.$digest()
apply 和 digest.jpg

$apply

这个启动digest process的特点:

  • 永远从 RootScope 开始启动digest process
  • ng-click, $timeout, $http(ajax) 等操作,背后都会调用$apply

$digest

这个和$apply的区别:

  • 当前作用域 (包括子作用域和嵌套作用域) 启动 digest process
  • 不从RootScope或父作用域启动
  • 也能够从RootScope开始东东digest process(这就相当于$apply了)

示例:

// html
<div ng-controller="MyCtrl" id="sum">
  a: {{a}} <input ng-model="a" />
  b: {{b}} <input ng-model="b" />
  <hr />
  <button ng-click="getSum()">ng-click求和</button> // 这里使用ng-click,自动触发digest process
  <button ng-click="btnClick()">外部事件求和</button> // 这里使用外部DOM事件求和, 需要手动的触发digest process
  <hr />
  总和为: {{s}}
</div>

// js

app.controller('MyCtrl', function($scope) {
  $scope.a = 10;
  $scope.b = 10;
  $scope.s = 0;
  
  $scope.getSum = function() {
    $scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
  }
})

// Angular context 外部逻辑
var btnClick = function() {
  var sumDiv = document.getElementById('sum');
  // 找出该作用域 这里的 '$scope' 可以为任何值
  var $scope = angular.element(sumDiv).scope();
  $scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
} 

仅仅这样写,点击 'ng-click求和' 将得到正确的值,但是点击 '外部事件求和' 按钮,虽然s的值会更新,但是DOM并未更新,这是我们需要使用 $scope.$apply() 手动的更新

var btnClick = function() {
  var sumDiv = document.getElementById('sum');
  var $scope = angular.element(sumDiv).scope();
  $scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);

  $scope.$apply(); // 手动的启动digest process
} 

$scope.$apply() 也可以添加一个函数,上面可以写为:

var btnClick = function() {
  var sumDiv = document.getElementById('sum');
  var $scope = angular.element(sumDiv).scope();

  // 使用函数的形式
  $scope.$apply(function() {
    $scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
  });
} 

总结

这一章主要有如下内容:

  • AngularJS的监察机制:watch list, watch 的分类: $watch, $watchGroup, $watchCollection
  • digest process的机制
  • ng-change 指令,当绑定的变量发生变化时, 调用相应的函数
  • Angular Context 的概念
  • 触发digest process的条件
  • 手动触发digest process: $apply(), $digest(), 两者之间的差异主要在于digest process开始的地方: $apply()从rootScope开始,而$digest()从当前scope开始,这对某些情况是有差别的
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容