Angular源码解析1——脏值查询的基本原理

数据双向绑定

在VUE中,通过ES5提供的getter和setter实现单向数据流。而在Angular中通过所谓的脏值查询实现了双向数据绑定,即View层和ViewModel层的双向数据流。具体是如何做到的呢。

基本原理

首先我们要了解订阅-发布模式(原理见我的另一篇博文EVENT事件库的原理)。Angular的脏值查询在此基础上实现。
这里我们先构建作用域构造函数,并设置事件池

1
2
3
function Scope () {
this.$$watchers=[];
}

数据变动的检测基于$scope作用域两个内置方法$watch和$digest。$watch类似事件库里的on方法,用于在事件池(在Angular中是作用域中的$$watchers属性)中存放事件名和对应回调函数。
类似这样

1
2
3
4
Scope.prototype.$watch=function (watchFn,listenerFn) {
var watcher={watchFn:watchFn,listenerFn:listenerFn};
this.$$watchers.push(watcher);
}

  • watchFn 为监控函数,返回所监控的数据的值
  • listenerFn 为监听函数,当数据发生变化后作出行为
    一个监控name值的简单监控函数
    1
    2
    3
    4
    //监控作用域中name属性的值
    function (scope) {
    return scope.name;
    }

$digest类似事件库中的emit方法,用来执行所在作用域中事件池($$watchers数组)中保存的方法对应的回调函数(即数据的监听函数),并把执行后新的值保存在$watch中。这样我们再次调用$digest,就能比较新值与旧值的变化。
类似这样

1
2
3
4
5
6
7
8
9
10
11
12
13
Scope.prototype.$digest=function () {
var self=this;
this.$$watchers.forEach(function (watcher) {
var newVal=watcher.watchFn(self);
var oldVal=watcher.last;
if (newVal!==oldVal) {
//如果发现值变化了就调用监听函数
watcher.listener(newVal,oldVal,self);
}
//每次调用都会更新数据
watch.last=newVal;
})
}

这就是Angular作用域的本质:添加监听器,在$digest中运行他们。
这也揭示了Angular作用域的性能特性:

  1. 在作用域中添加数据并不会带来性能折扣。Angular并不会遍历作用域的数据,只会遍历绑定在$$watchers中的事件。
  2. $digest会调用每个监控函数。因此,最好关注监听器的数量,还有每个独立的监控函数或者表达式的性能。

存在的问题

现在我们的脏值检查还存在很多问题,其中比较重要的一个应用场景咱们就没有实现——当监听函数自身也修改作用域上的属性。如果这个发生了,另外有个监听器在监控被修改的属性,有可能在同一个digest里面检测不到这个变动。