当前位置:首页 > 日记 > 正文

深入理解AngularJs-scope的脏检查(一)

深入理解AngularJs-scope的脏检查(一)

进入正文前的说明:本文中的示例代码并非AngularJs源码,而是来自书籍<<Build Your Own AngularJs>>, 这本书的作者仅依赖jquery和lodash一步一步构建出AngularJs的各核心模块,对全面理解AngularJs有非常巨大的帮助。若有正在使用AngulaJs攻城拔寨并且希望完全掌握手中武器的小伙伴,相信能对你理解AngularJs带来莫大帮助,感谢作者。

在这篇文章中,希望能让您理清楚以下几项与scope相关的功能:

1.dirty-checking(脏检测)核心机制,主要包括:$watch 和 $digest;

2.几种不同的触发$digest循环的方式:$eval, $apply, $evalAsync, $applyAsync;

3.scope的继承机制以及isolated scope;

4.依赖于scope的事件循环:$on, $broadcast, $emit.

现在开始我们的第一部分:scope和dirty-checking

dirty-checking(脏检测)原理简述:scope通过$watch方法向this.$$watchers数组中添加watcher对象(包含watchFn, listenerFn, valueEq, last 四个属性)。每当$digest循环被触发时,它会遍历$$watchers数组,执行watcher中的watchFn,获取当前scope上某属性的值(一个watcher对应scope上一个被监听属性),然后去同watcher中的last(上一次的值)做比较,若两值不相等,就执行listenerFn。

function Scope() {  this.$$watchers = []; // 监听器数组  this.$$lastDirtyWatch = null; // 每次digest循环的最后一个脏的watcher, 用于优化digest循环  this.$$asyncQueue = []; // scope上的异步队列  this.$$applyAsyncQueue = []; // scope上的异步apply队列  this.$$applyAsyncId = null; //异步apply信息  this.$$postDigestQueue = []; // postDigest执行队列  this.$$phase = null; // 储存scope上正在做什么,值有:digest/apply/null  this.$root = this; // rootScope  this.$$listeners = {}; // 存储包含自定义事件键值对的对象  this.$$children = []; // 存储当前scope的儿子Scope,以便$digest循环递归}

实际上scope就是一个普通的javascript对象,一个类构造函数,可以通过new进行实例化。根据脏检测的原理,接下来,我们一起看看scope的$watch方法的实现。

/* $watch方法:向watchers数组中添加watcher对象,以便对应调用 */Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {  var self = this;  watchFn = $parse(watchFn);  // watchDelegate: 针对watch expression是常量和 one-time-binding的情况,进行优化。在第一次初始化之后删除watch  if(watchFn.$$watchDelegate) {    return watchFn.$$watchDelegate(self, listenerFn, valueEq, watchFn);  }  var watcher = {    watchFn: watchFn,    listenerFn: listenerFn || function() {},    valueEq: !!valueEq,    last: initWatchVal  };  this.$$watchers.unshift(watcher);  this.$root.$$lastDirtyWatch = null;  return function() {    var index = self.$$watchers.indexOf(watcher);    if(index >= 0) {      self.$$watchers.splice(index, 1);      self.$root.$$lastDirtyWatch = null;    }  };};

$watch方法的参数:

watchFn-监视表达式,在使用$watch时,通常是传入一个expression, 经过$parse服务处理后返回一个监视函数,提供动态访问scope上属性值的功能,可以看作 function() { return scope.someValue; }。

listenerFn-监听函数,当$digest循环dirty时(即scope上$$watchers数组中有watcher监测到属性值变化时),执行的回调函数。

valueEq-是否全等监视,布尔值,valueEq默认为false,此时$watch对监视对象进行“引用监视”,如果被监视的表达式是原始数据类型,$watch能够发现改变。如果被监视的表达式是引用类型,由于引用类型的赋值只是将被赋值变量指向当前引用,故$watch认为没有改变。若需要对引用类型进行监视,则需要将valueEq设置为true,这是$watch会对被监视对象进行“全等监视”,在每次比较前会用angular.copy()对被监视对象进行深拷贝,然后用angular.equal()进行比对。虽然“全等监视”能够监视到所有改变,但如果被监视对象很大,性能肯定会大打折扣。所以应该根据实际情况来使用valueEq。

从代码中能够看出,$watch的功能其实非常简单,就是构造watcher对象,并将watcher对象插入到scope.$$watchers数组中,然后返回一个销毁当前watcher的函数。

接下来进入到脏检测最核心的部分:$digest循环

《Build your own AngularJs》的作者将$digest分成了两个函数:$digestOnce 和 $digest。这虽然不用与框架源码,但能够使代码更易理解。两个函数实际上分别对应了$digest的内层循环和外层循环。代码如下:

内层循环

Scope.prototype.$$digestOnce = function() {      var dirty;      var continueLoop = true;      var self = this;      this.$$everyScope(function(scope) {        var newValue, oldValue;        _.forEachRight(scope.$$watchers, function(watcher) {          try {            if(watcher) {              newValue = watcher.watchFn(scope);              oldValue = watcher.last;              if(!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) {                scope.$root.$$lastDirtyWatch = watcher;                watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);                                watcher.listenerFn(newValue,                  (oldValue === initWatchVal? newValue : oldValue), scope);                dirty = true;              } else if(scope.$root.$$lastDirtyWatch === watcher) {                continueLoop = false;                return false;              }            }          } catch(e) {            console.error(e);          }        });        return continueLoop;      });      return dirty;    };

代码中,$$everyScope是递归childScope执行回调函数的工具方法,后面会贴出。

$digestOnce的核心逻辑就在$$everyScope方法的循环体内,即遍历scope.$$watchers, 比对新旧值,根据比对结果确定是否执行listenerFn,并向listenerFn中传入newValue, oldValue, scope供开发者获取。

示例代码第18行,watcher.last的赋值证实了上文提到的$watch的第三个参数valueEq的作用。

示例代码第23行,由于$digest循环会一直运行直到没有dirty watcher时,故单次$digest循环通过缓存最后一个dirty的watcher,在下一次$digest循环时如果遇到$$lastDirtyWatcher就停止当前循环。这样做减少了遍历watcher的数量,优化了性能。

 外层循环

在我们的示例中,外层循环即由 $digest来控制。$digest函数主要由do while循环体内调用$digestOnce进行脏检测 以及 对其他一些异步操作的处理组成。代码如下:

// digest循环的外循环,保持循环直到没有脏值为止    Scope.prototype.$digest = function() {      var ttl = TTL;      var dirty;      this.$root.$$lastDirtyWatch = null;      this.$beginPhase('$digest');      if(this.$root.$$applyAsyncId) {        clearTimeout(this.$root.$$applyAsyncId);        this.$$flushApplyAsync();      }      do {        while (this.$$asyncQueue.length) {          try {            var asyncTask = this.$$asyncQueue.shift();            asyncTask.scope.$eval(asyncTask.expression);          } catch(e) {            console.error(e);          }        }        dirty = this.$$digestOnce();        if((dirty || this.$$asyncQueue.length) && !(ttl--)) {          this.$clearPhase();          throw TTL + ' digest iterations reached';        }      } while (dirty || this.$$asyncQueue.length);      this.$clearPhase();      while(this.$$postDigestQueue.length) {        try {          this.$$postDigestQueue.shift()();        } catch(e) {          console.error(e);        }      }    };

在这一节中我们的主要关注点是脏检测,异步任务相关的$$applyAsync,$$flushApplyAsync,$$asyncQueue,$$postDigestQueue之后再做分析。

示例代码第24行,调用$$digestOnce,并把返回值赋值给dirty。在do while循环中,只要dirty为true,那么循环就会一直执行下去,直到dirty的值为 false。这就是脏检测机制的外层循环的实现,是不是觉得其实很简单呢,嘿嘿。

设想一下,某些值可能会在listenerFn中持续被改变并且,无法稳定下来,那势必会出现死循环。为了解决这个问题,AngularJs使用 TTL(time to live)来对循环次数进行控制,超过最大次数,就会throw错误 并 告诉开发者循环可能永远不会稳定。

现在我们把注意力移到代码第26行的 if 代码块上,不难看出,这里是对最大$digest循环次数进行了限制,每执行一次do while循环的循环体,TTL就会自减1。当TTL值为0,再进行循环就会报错。当然咯,这个TTL的值也是能够进行配置的。

现在,相信小伙伴们对$digest循环已经比较清楚了吧~简单来说,dirty-checking就是依赖缓存在scope上的$$watchers和$digest循环来对值进行监听的。有了$digest,当然还需要有手段去触发它咯。

接下来,我们将进入第二部分:触发$digest循环 和 异步任务处理 

$eval

说到触发$digest循环,大部分同学都会想到$apply。要说$apply就需要先说说$eval。

$eval使我们能够在scope的context中执行一段表达式,并允许传入locals object对当前scope context进行修改。

tip:$parse服务能够接受一个表达式或者函数作为参数,经过处理返回一个函数供开发者调用。这个函数有两个参数context object(通常就是scope),locals object(本地对象,常用来覆盖context中的属性)。

 Scope.prototype.$eval = function(expr, locals) {   return $parse(expr)(this, locals); };

$apply

$apply 方法接收一个expression或者function作为参数,$apply通过$eval函数执行传入的expression 或 function。最终从$rootScope上触发$digest循环。

$apply 被认为是 使AngularJs与第三方库混合使用最标准的方式。初学者朋友刚开始都会遇到用第三方库修改了scope上的属性或者被watch的属性,但并没有触发$digest循环,导致双向绑定失效的问题。此时,$apply就是解决这种情况的良药!

Scope.prototype.$apply = function(expr) {  try {    this.$beginPhase('$apply');    return this.$eval(expr);  } finally {    this.$clearPhase();    this.$root.$digest();  }};

$apply本质上,就是用$eval执行了一段表达式,再调用rootScope的$digest方法。

有时候,当我们能够确定我们不需要从rootScope开始进行$digest循环时,我可以调用scope.digest() 来代替 $apply,这样能够带来性能的提升。

 $evalAsync

$evalAsync 用于延迟执行一段表达式。通常我们更习惯使用$timeout服务来进行代码的延迟执行,但$timeout会将执行控制权交给浏览器,如果浏览器同时还需要执行诸如 ui渲染/事件控制/ajax 等任务时,我们代码延迟执行的时机就会变得非常不可控。

我们来看看$evalAsync是如何让代码延迟执行的时机变得严格,可控的。

Scope.prototype.$evalAsync = function(expr) {  var self = this;  if(!self.$$phase && !self.$$asyncQueue.length) {    setTimeout(function() {      if(self.$$asyncQueue.length) {        self.$root.$digest();      }    }, 0);  }  this.$$asyncQueue.push({    scope: this,    expression: expr  });};

$evalAsync方法的主要功能是从代码第11行开始,向$$asyncQueeu中添加对象。$$asyncQueue队列的执行是在$digest的do while循环中进行的。

while (this.$$asyncQueue.length) {  try {    var asyncTask = this.$$asyncQueue.shift();    asyncTask.scope.$eval(asyncTask.expression);  } catch(e) {    console.error(e);  }}

$evalAsync的代码会在正在运行的$digest循环中被执行,如果当前没有正在运行的$digest循环,会自己延迟触发一个$digest循环来执行延迟代码。

 $applyAsync

$applyAsync用于合并短时间内多次$digest循环,优化应用性能。

在日常开发工作中,常常会遇到要短时间内接收若干http响应,同时触发多次$digest循环的情况。使用$applyAsync可合并若干次$digest,优化性能。

/* 这个方法用于 知道需要在短时间内多次使用$apply的情况,  能够对短时间内多次$digest循环进行合并,  是针对$digest循环的优化策略  */Scope.prototype.$applyAsync = function(expr) {  var self = this;  self.$$applyAsyncQueue.push(function() {    self.$eval(expr);  });  if(self.$root.$$applyAsyncId === null) {    self.$root.$$applyAsyncId = setTimeout(function() {      self.$apply(_.bind(self.$$flushApplyAsync, self));    }, 0);  }};

$$postDigest

$$postDigest方法提供了在下一次digest循环后执行代码的方式,这个方法的前缀是"$$",是一个AngularJs内部方法,应用开发极少用到。

此方法不自主触发$digest循环,而是在别处产生$digest循环之后执行。

/* $$postDigest 用于在下一次digest循环后执行函数队列    不同于applyAsync 和 evalAsync, 它不触发digest循环   */ Scope.prototype.$$postDigest = function(fn) {   this.$$postDigestQueue.push(fn); };

到这里,我们对脏检测的原理,即它的工作机制就了解的差不多了。希望这些知识能够帮助你更好的应用AngularJs来开发,能够更轻松地定位错误。

下一章,我会继续为大家介绍文章开头提到的另外两处scope相关的特性。篇幅较长,感谢您的耐心阅读~也希望大家多多支持。

相关文章

AI三步制作超漂亮的悖反空间效果

AI三步制作超漂亮的悖反空间效果

空间,三步,效果,漂亮,电脑软件,这篇教程教的AI学习者们使用AI快速三步制作悖反空间效果,所以悖反空间其实就是不同透视角度物体的重合。这样制作出来的效果会更加吸引人的眼球和引发人们细看的兴趣。好了,我们先来看看最终的效果图:具体的制作…

详解PHP防止直接访问.php 文件的实

详解PHP防止直接访问.php 文件的实

文件,方法,直接访问,详解,电脑软件,详解PHP防止直接访问.php 文件的实现方法为了保证我们用 PHP 写的 API 的安全性要禁止除了接口外的访问方式.比如我们的项目为 example, 其下有文件夹 dir1、有个接口文件 api.php. 结构为: 输入图片说明…

微信小程序 生命周期函数详解

微信小程序 生命周期函数详解

周期函数,详解,生命,程序,电脑软件,微信小程序 生命周期函数小程序中 判断当前首页是从其他页面返回,还是由入口打开由于小程序的数据在我们退出小程序时并没有得到释放,因此再次点击开来数据依然没有变成初始化 解决方法:在小程序 data 数据…

2017扣扣个性签名搞笑的

2017扣扣个性签名搞笑的

个性签名,搞笑,最新版,热门,经典,  2017年扣扣个性签名档,有哪些搞笑的话语呢?下面是小编给大家带来的扣扣个性签名搞笑的,希望你喜欢。2017扣扣个性签名搞笑的【经典版】1) 能不打就别打,一旦打起来,就给我死命磕2) 我可以爱你爱到撕心裂肺,…

fireworks简单几步制作layer

fireworks简单几步制作layer

几步,简单,电脑软件,fireworks,layer,在使用fireworks的时候,想要使用fireworks做layer,如何制作呢?下面讲解具体的制作方法!最终效果:步骤:1、先打开ps&ldquo;文件&rdquo;&rarr;&rdquo;新建&rdquo;,然后点击工具栏的&ldquo;椭圆选框工具&rdquo;2…

在wps2016表格中内容如何快速填充

在wps2016表格中内容如何快速填充

填充,表格,快速,内容,电脑软件,  在WPS2016表格内容快速填充,好像和Excel的快速填充有点不一样了,那么如果你不知道如何表格内容快速填充,可以学习一下。以下是小编为您带来的关于wps2016表格内容快速填充,希望对您有所帮助。wps2016表格内容…

怎么把ppt转换成视频

怎么把ppt转换成视频

转换成,视频,电脑软件,ppt,在我们使用PPT2013时, 怎么把ppt转换成视频?下面小编教你一招最简单实用的方法,希望对大家有所帮助。PPT 2013怎么把ppt转换成视频1、首先 ,使用ppt2013打开你要转化的ppt文件,点击右上角的&ldquo;文件&rdquo;选项,如…

如何在Excel中设置单元格只能输入

如何在Excel中设置单元格只能输入

输入,设置,单元格,如何在,电脑软件,如何能让输入的单元格只能输入正值?正值就是大于0的数据吗?是只能输入数值?经过确认后,希望能限制单元格只能输入数值,而且是正数。Excel果然是强大的,这种问题分分钟搞定。与大家分享一下。场景:适合公司人…

详解vue父子组件间传值 | props

详解vue父子组件间传值 | props

传值,组件,父子,详解,电脑软件,先定义一个子组件,在组件中注册props<template> <div> <div>{{message}}(子组件)</div> </div></template><script>export default { props: { message: String //定义传值的类型<br> }}</script><s…

位图怎么转换成矢量图

位图怎么转换成矢量图

矢量图,位图,转换成,电脑软件,我们在做设计的过程中,有时候用到的素材是一张尺寸较小的图片,而又找不到更好的图片时,会给设计过程带来不便,如果图片是位图文件,任意放大或者缩小都会模糊不清,要想获得良好的视觉体验,就必须转换成矢量图,那么有哪…

easyui datagrid 表格中操作栏 按

easyui datagrid 表格中操作栏 按

显示,解决方法,操作,表格,按钮图标,jQuery EasyUI动态添加控件或者ajax加载页面后不能自动渲染解决办法:使用解析器Parser(解析器)$.parser.parse(); // 解析所有页面$.parser.parse('#cc'); // 解析指定节点以上这篇easyui datagrid 表格…

怎么用ppt制作各式各样形状的用ppt

怎么用ppt制作各式各样形状的用ppt

方法,步骤,各式各样,形状,图片,  怎么用ppt制作各式各样形状的图片?PPT课件是应用最广泛的多媒体教学资源,它能够有效的将文字、图形、音频、视频、动画等多种元素融合在一起;下面小编就教你用ppt制作各式各样形状的图片的方法。用ppt制…