Angularjs 双向绑定机制解析

文章转自:http://www.2cto.com/kf/201408/327594.html

AngularJs 的元素与模型双向绑定依赖于循环检测它们之间的值,这种做法叫做脏检测,这几天研究了一下其源码,将 Angular 的实现分享一下。

首先看看如何将 Model 的变更更新到 UI

Angular 的 Model 是一个 Scope 的类型,每个 Scope 都归属于一个 Directive 对象,比如 $rootScope 就归属于 ng-app。

从 ng-app 往下,每个 Directive 创建的 Scope 都会一层一层链接下去,形成一个以 $rootScope 为根的链表,注意 Scope 还有同级的概念,形容更贴切我觉得应该是一棵树。

我们大概看一下 Scope 都有哪些成员:

function Scope() {

this.$id = nextUid();

// 依次为: 阶段、父 Scope、Watch 函数集、下一个同级 Scope、上一个同级 Scope、首个子级 Scope、最后一个子级 Scope

this.$$phase = this.$parent = this.$$watchers =

this.$$nextSibling = this.$$prevSibling =

this.$$childHead = this.$$childTail = null;

// 重写 this 属性以便支持原型链

this[‘this‘] = this.$root =  this;

this.$$destroyed = false;

// 以当前 Scope 为上下文的异步求值队列,也就是一堆 Angular 表达式

this.$$asyncQueue = [];

this.$$postDigestQueue = [];

this.$$listeners = {};

this.$$listenerCount = {};

this.$$isolateBindings = {};

}

Scope.$digest,这是 Angular 提供的从 Model 更新到 UI 的接口,你从哪个 Scope 调用,那它就会从这个 Scope 开始遍历,通知模型更改给各个 watch 函数,

来看看 $digest 的源码:

$digest: function() {

var watch, value, last,

watchers,

asyncQueue = this.$$asyncQueue,

postDigestQueue = this.$$postDigestQueue,

length,

dirty, ttl = TTL,

next, current, target = this,

watchLog = [],

logIdx, logMsg, asyncTask;

// 标识阶段,防止多次进入

beginPhase(‘$digest‘);

// 最后一个检测到脏值的 watch 函数

lastDirtyWatch = null;

// 开始脏检测,只要还有脏值或异步队列不为空就会一直循环

do {

dirty = false;

// 当前遍历到的 Scope

current = target;

// 处理异步队列中所有任务, 这个队列由 scope.$evalAsync 方法输入

while(asyncQueue.length) {

try {

asyncTask = asyncQueue.shift();

asyncTask.scope.$eval(asyncTask.expression);

} catch (e) {

clearPhase();

$exceptionHandler(e);

}

lastDirtyWatch = null;

}

traverseScopesLoop:

do {

// 取出当前 Scope 的所有 watch 函数

if ((watchers = current.$$watchers)) {

length = watchers.length;

while (length--) {

try {

watch = watchers[length];

if (watch) {

// 1.取 watch 函数的运算新值,直接与 watch 函数最后一次值比较

// 2.如果比较失败则尝试调用 watch 函数的 equal 函数,如果没有 equal 函数则直接比较新旧值是否都是 number

if ((value = watch.get(current)) !== (last = watch.last) &&

!(watch.eq

? equals(value, last)

: (typeof value == ‘number‘ && typeof last == ‘number‘

&& isNaN(value) && isNaN(last)))) {

// 检测到值改变,设置一些标识

dirty = true;

lastDirtyWatch = watch;

watch.last = watch.eq ? copy(value, null) : value;

// 调用 watch 函数的变更通知函数, 也就是说各个 directive 从这里更新 UI

watch.fn(value, ((last === initWatchVal) ? value : last), current);

// 当 digest 调用次数大于 5 的时候(默认10),记录下来以便开发人员分析。

if (ttl < 5) {

logIdx = 4 - ttl;

if (!watchLog[logIdx]) watchLog[logIdx] = [];

logMsg = (isFunction(watch.exp))

? ‘fn: ‘ + (watch.exp.name || watch.exp.toString())

: watch.exp;

logMsg += ‘; newVal: ‘ + toJson(value) + ‘; oldVal: ‘ + toJson(last);

watchLog[logIdx].push(logMsg);

}

} else if (watch === lastDirtyWatch) {

// If the most recently dirty watcher is now clean, short circuit since the remaining watchers

// have already been tested.

dirty = false;

break traverseScopesLoop;

}

}

} catch (e) {

clearPhase();

$exceptionHandler(e);

}

}

}

// 恕我理解不能,下边这三句是卖萌吗

// Insanity Warning: scope depth-first traversal

// yes, this code is a bit crazy, but it works and we have tests to prove it!

// this piece should be kept in sync with the traversal in $broadcast

// 没有子级 Scope,也没有同级 Scope

if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {

// 又判断一遍不知道为什么,不过这个时候 next === undefined 了,也就退出当前 Scope 的 watch 遍历了

while(current !== target && !(next = current.$$nextSibling)) {

current = current.$parent;

}

}

} while ((current = next));

// 当 TTL 用完,依旧有未处理的脏值和异步队列则抛出异常

if((dirty || asyncQueue.length) && !(ttl--)) {

clearPhase();

throw $rootScopeMinErr(‘infdig‘,

‘{0} $digest() iterations reached. Aborting!\n‘ +

‘Watchers fired in the last 5 iterations: {1}‘,

TTL, toJson(watchLog));

}

} while (dirty || asyncQueue.length);

// 退出 digest 阶段,允许其他人调用

clearPhase();

while(postDigestQueue.length) {

try {

postDigestQueue.shift()();

} catch (e) {

$exceptionHandler(e);

}

}

}

虽然看起来很长,但是很容易理解,默认从 $rootScope 开始遍历,对每个 watch 函数求值比较,出现新值则调用通知函数,由通知函数更新 UI,我们来看看 ng-model 是怎么注册通知函数的:

$scope.$watch(function ngModelWatch() {

var value = ngModelGet($scope);

// 如果 ng-model 当前记录的 modelValue 不等于 Scope 的最新值

if (ctrl.$modelValue !== value) {

var formatters = ctrl.$formatters,

idx = formatters.length;

// 使用格式化器格式新值,比如 number,email 之类

ctrl.$modelValue = value;

while(idx--) {

value = formatters[idx](value);

}

// 将新值更新到 UI

if (ctrl.$viewValue !== value) {

ctrl.$viewValue = value;

ctrl.$render();

}

}

return value;

});

那么 UI 更改如何更新到 Model 呢

很简单,靠 Directive 编译时绑定的事件,比如 ng-model 绑定到一个输入框的时候事件代码如下:

var ngEventDirectives = {};

forEach(

‘click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste‘.split(‘ ‘),

function(name) {

var directiveName = directiveNormalize(‘ng-‘ + name);

ngEventDirectives[directiveName] = [‘$parse‘, function($parse) {

return {

compile: function($element, attr) {

var fn = $parse(attr[directiveName]);

return function(scope, element, attr) {

// 触发以上指定的事件,就将元素的 scope 和 event 对象一起发送给 direcive

element.on(lowercase(name), function(event) {

scope.$apply(function() {

fn(scope, {$event:event});

});

});

};

}

};

}];

}

);

Directive 接收到输入事件后根据需要再去 Update Model 就好啦。

相信经过以上研究应该对 Angular 的绑定机制相当了解了吧,现在可别跟人家说起脏检测就觉得是一个 while(true) 一直在求值效率好低什么的,跟你平时用事件没啥两样,多了几次循环而已。

最后注意一点就是平时你通常不需要手动调用 scope.$digest,特别是当你的代码在一个 $digest 中被回调的时候,因为已经进入了 digest 阶段所以你再调用则会抛出异常。

我们只在没有 Scope 上下文的代码里边需要调用 digest,因为此时你对 UI 或 Model 的更改 Angular 并不知情。

时间: 2024-10-12 13:04:31

Angularjs 双向绑定机制解析的相关文章

Angular数据双向绑定机制

数据的双向绑定 Angular实现了双向绑定机制.所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面. 一个最简单的示例就是这样: <div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter=counter+1">increase</butto

深入学习AngularJS中数据的双向绑定机制

来自:http://www.jb51.net/article/80454.htm Angular JS (Angular.JS) 是一组用来开发Web页面的框架.模板以及数据绑定和丰富UI组件.它支持整个开发进程,提供web应用的架构,无需进行手工DOM操作. AngularJS很小,只有60K,兼容主流浏览器,与 jQuery 配合良好.双向数据绑定可能是AngularJS最酷最实用的特性,将MVC的原理展现地淋漓尽致. AngularJS的工作原理是:HTML模板将会被浏览器解析到DOM中,

AngularJS双向绑定,手动实施观察

实现这样的一个需求:页面中某个地方显示某个文本框的值经过计算得到的结果,而且是文本框值每次变化显示的计算结果也跟着动态变化. 在controller中可以声明一个对象,它的一个字段用来存储初始值: $scope.funding = {startingEstimate:0}; 以上,声明了一个funding对象,它的startingEstimate字段用来存储初始值,初始值这里是0. 在controller中还应该声明一个函数,用来把根据初始值计算得到的结果赋值给funding对象的另外一个字段.

angularjs双向绑定

1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> 6 <ti

浅谈AngularJs双向绑定

双向绑定的三个重要方法: $scope.$apply() $scope.$digest() $scope.$watch() 一.$scope.$watch() $watch作用就是在$scope上添加一个监听器,当监听的数据发生变化的时候,监听器就会收到提示,并执行某些操作.当给$watch指定如下两个函数,就可以创建一个监听器: 一个表达式,用于指定所关注的那部分数据,比如说“user.firstName”. 一个监听函数,用于在数据变更的时候接受提示. 为了实现$watch,我们需要存储监听

mvvm双向绑定机制的原理和代码实现

mvvm框架的双向绑定,即当对象改变时,自动改变相关的dom元素的值,反之,当dom元素改变时,能自动更新对象的值,当然dom元素一般是指可输出的input元素. 1. 首先实现单向绑定,在指定对象的属性值发生改变时触发callback函数. 2. 单向绑定可采用ES5新增的defineProperty实现(或defineProperties),用了ES5注定就不支持IE9以下了,为了防止递归死循环问题,原有属性需要剪切到一个私有属性中保存. 3. 循环调用defineProperty定义闭包时

玩转angularJs——通过自定义ng-model,不仅仅只是input可以有双向绑定

angularJs双向绑定特性在开发中很方便很实用,但是由于ng-model一般只能挂在input上,因此我们需要自定义ng-model来在div等元素上使用该标签. 自定义指令: 1 //自定义ngModel的属性 2 .directive('contenteditable', ['$window', function() { 3 return { 4 restrict: 'A', 5 require: '?ngModel', // 此指令所代替的函数 6 link: function(sco

Angular数据双向绑定

Angular数据双向绑定 AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀的前端JS框架,已经被用于Google的多款产品当中.AngularJS有着诸多特性,最为核心的是:MVVM.模块化.自动化双向数据绑定.语义化标签.依赖注入等等. 一.什么是数据双向绑定 Angular实现了双向绑定机制.所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面. 一个最简单的示例就是这样: <div ng-control

数据的双向绑定 Angular JS

接触AngularJS许了,时常问自己一些问题,如果是我实现它,会在哪些方面选择跟它相同的道路,哪些方面不同.为此,记录了一些思考,给自己回顾,也供他人参考. 初步大致有以下几个方面: 数据双向绑定 视图模型的继承关系 模块和依赖注入的设计 待定 数据的双向绑定 Angular实现了双向绑定机制.所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面. 一个最简单的示例就是这样: <div ng-controller="CounterCtrl"> &