angularjs双向绑定后,发生了什么事情?是什么可以让view层和controller层进行绑定的?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jnshu_it/article/details/84258671

这里是修真院前端小课堂,每篇分享文从

【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】

八个方面深度解析前端知识/技能,本篇分享的是:

【angularjs双向绑定后,发生了什么事情?是什么可以让view层和controller层进行绑定的?    】

1.1数据绑定
我们看到的网站页面中,是由数据和设计两部分组合而成。将设计转换成浏览器能理解的语言,便是html和css主要做的工作。 而将数据显示在页面上,并且有一定的交互效果(比如点击等用户操作及对应的页面反应)则是js主要完成的工作。 很多时候我们不可能每次更新数据便刷新页面(get请求),而是通过向后端请求相关数据,并通过无刷新加载的方式进行更新页面(post请求)。 那么数据进行更新后,页面上相应的位置也能自动做出对应的修改,便是数据绑定。

在以前的开发模式中,这一步一般通过jq操作DOM结构,从而进行更新页面。但这样带来的是大量的代码和大量的操作。 如果能在开始的时候,便已经确定好从后端获取的数据到页面上需要进行的操作,当数据发生改变,页面的相关内容也自动发生变化,这样便能极大地方便前端工程师的开发。 在新的框架中(angualr,react,vue等),通过对数据的监视,发现变化便根据已经写好的规则进行修改页面,便实现了数据绑定。 可以看出,数据绑定是M(model,数据)通过VM(model-view,数据与页面之间的变换规则)向V(view)的一个修改。

而双向绑定则是增加了一条反向的路。在用户操作页面(比如在Input中输入值)的时候,数据能及时发生变化,并且根据数据的变化, 页面的另一处也做出对应的修改。有一个常见的例子就是淘宝中的购物车,在商品数量发生变化的时候,商品价格也能及时变化。这样便实现了V——M——VM——V的一个双向绑定。

ANGULARJS双向绑定后,发生了什么事情? 什么让VIEW层和CONTROLLER层进行绑定的Angular双向绑定通过watch,digest和apply实现的

watch序列 watch监控model中是否有变化,会记录last值,也就是改变后的值,每一个model都会增加一个watch到watch队列中。 传递给$watch()的第二个参数称为监听器函数,当aModel的值发生变化时,它就被调用。我们很容易理解,当aModel的值发生改变, 这个监听器就会被调用来更新HTML中的表达式。但是,还有一个很重要的问题!Angular是怎么判断什么时候调用这个监听器函数的呢? 换句话说,AngularJS是如何知道aModel值是何时发生改变的,从而它可以调用相应的监听器函数呢?它是否定期运行一个函数来检查 scope 模型的值是否已经改变了?好,这就是 $digest 循环的步骤。

在 $digest 周期中,watcher 会被触发。当一个 watcher 被触发时,AngularJS将评估 scope 模型,如果它发生了变化,则调用相应的监听器函数。 那么,我们的下一个问题是,这个$digest 循环是何时开始的。 $digest循环是在什么时候以各种方式开始的? 当浏览器接收到可以被 angular context 处理的事件时,digest循环就会触发,,遍历所有的watch,最后更新 dom。 假设你通过ng-click指令在处理程序函数中更改了一个scope模型。在这种情况下,AngularJS会通过调用 $digest() 自动触发一个 $digest 循环。当 $digest 循环开始的时候,它就会触发每一个 watcher。 这些 watcher 会检查scope模型的当前值是否与上次计算得到的值不同。如果不同,则执行相应的监听器函数。 因此,如果在视图中有任何表达式,它们将被更新。除了ng-click之外,还有其他一些内置的指令/服务可以让你更改模型(例如ng-model、$timeout 等),并自动触发一个 $digest 循环。

例子

click 时会产生一次更新的操作(至少触发两次 $digest 循环)  •按下按钮 •浏览器接收到一个事件,进入到 angular context •digest循环开始执行,查询每个watch 是否变化  •由于监视 scope.val的 scope.val的watch 报告了变化,因此强制再执行一次 $digest 循环  •新的 $digest 循环未检测到变化 •浏览器拿回控制器,更新 $scope. val.新值对应的 dom 到目前为止还不错!但是,这里有一个小问题。在上面的例子中,Angular并不直接调用 $digest()。 相反,它调用 $scope.$apply(),而 $scope.$apply() 又会调用 $rootScope.$digest()。

因此,一个 $digest 循环开始于 $rootScope,随后会访问所有的child scopes,并在此过程中调用child scopes中的watchers。 现在,假设你将一个ng-click指令附加到一个按钮,并将一个函数名传递给它。当单击按钮时,AngularJS将函数调用包装在 $scope.$apply() 中。因此,你的函数照常执行,更改模型(如果有的话),并开始一个 $digest 循环来确保你的更改反映在视图中。 注意:$scope.$apply() 自动调用 $rootScope.$digest()。$apply() 函数有两种形式。第一种接受一个函数作为参数,执行这个函数,并触发一个 $digest 循环。第二种则不需要任何参数,在调用时只触发一个 $digest 循环。

$APPLY

进行数据变化检查的实际上是$digest函数,但是我们往往不是直接使用$digest,而是使用$apply, $apply接收表达式或者函数作为参数后调用$digest来更新绑定部门以及监控器。实际上,Angular几乎在所有提供的代码中添加了$apply, 如ng-click,初始controller,$http的回调操作,在这,你并不需要亲自调用 $apply,而且重复的调用会引起错误。因此,当你运行了一个新阶段,并且这部分并不属于Angular库的情况下才需要使用$apply。 这有一段关于setTimeout的代码,在经过了2000毫秒的延迟之后,代码进入执行了一个新的阶段,但是Angular并不知道数据有更新, 因此更新并不会被显示。我们应该用angular JS提供的timeout方法,这样它就会被自动用 timeout方法,这样它就会被自动用apply方法包起来了

什么时候用$APPLY()

那我们到底什么时候需要去调用apply()方法呢?情况非常少,实际上几乎我们所有的代码都包在scope.apply()里面, 像ng−click,controller的初始化,http的回调函数等。在这些情况下,我们不需要自己调用,实际上我们也不能自己调用,否则在apply()方法里面再调用 apply()方法会抛出错误。如果我们需要在一个新的执行序列中运行代码时才真正需要用到它, 而且当且仅当这个新的执行序列不是被angular JS的库的方法创建的,这个时候我们需要将代码用 scope.apply()包起来。

$DIGEST 循环会运行多少次?

$digest 循环的上限是 10 次(超过 10次后抛出一个异常,防止无限循环)。 $digest 循环不会只运行一次。在当前的一次循环结束后, 它会再执行一次循环用来检查是否有 models 发生了变化。 这就是脏检查(Dirty Checking),它用来处理在 listener 函数被执行时可能引起的 model 变化。 因此 digest循环会持续运行直到model不再发生变化,或者 digest循环会持续运行直到model不再发生变化,或者digest 循环的次数达到了 10 次(超过 10 次后抛出一个异常,防止无限循环)。 当 $digest 循环结束时,DOM 相应地变化。

脏检查如何被触发

angular 会在可能触发 UI 变更的时候进行脏检查:这句话并不准确。实际上脏检查是digest执行的, 另一个更常用的用于触发脏检查的函数apply——其实就是 $digest 的一个简单封装(还做了一些抓异常的工作)。 通常写代码时我们无需主动调用 apply或 apply或digest 是因为 angular 在外部对我们的回调函数做了包装。 例如常用的 ng-click,这是一个指令(Directive),$digest过程的逻辑就是检查watcher列表中的每一项,看当前值与上次的值是否相同, 如果不同则调用listener回调函数。这就是dirty-checking的核心逻辑

在 AngularJS 中使用 $watch注意事项?

我们在实际运用中常常不只是对一个原始类型的属性进行监视,如果你还记得Javascript中的六种基本类型, 你一定会记得原始类型(数字,字符串)和引用类型的区别。对于原始类型,如果我们使用了一个赋值操作, 则这个原始类型变量会“真正的”被进行一次复制,然而对于引用类型,在进行赋值时,仅仅时将赋值的变量指向了这个引用类型。 在AngularJS的$watch方法中,对两者的操作也有不同之处。原始类型,就像我们上面例子中提到的$rootScope, 没有什么特别之处,然而如果要对一个引用类型,尤其是在实际运用中常见的对象数组进行监视时,情况就不一样了。

$watch在对待原始类型和引用类型会有不同的处理方式,这就要首先说一说$watch函数的第三个参数。 在前面的例子中,我们知道,$watch函数有接收两个参数,第一个参数是需要监视的对象,第二个参数是在监视对象发生变化时需要调用的函数, 实际上$watch还有第三个参数,它在默认情况下是false。在默认情况下,即不显式指明第三个参数或者将其指明为false时, 我们进行的监视叫做“引用监视”。引用监视的原词的“reference watch”,它的意思是只要监视的对象引用没有发生变化, 就不算它发生了变化。具体来说,在上面的例子中,只要是items的引用没有发生变化,就算items中的一些属性发生了变化, $watch也会当做没有看见。那么在什么时候算是引用发生了变化呢?比如说将一个新的数组newItems赋值给items,此时$watch才会站出来

相反,如果我们将$watch的第三个变量设置为true,那么此时我们进行的监视叫做“全等监视”, 原词是“equality watch”。此时,$watch就像是一个醋意十足的恋人,只要看他的对象有一点风吹草动,马上就跳出来, 既然全等监视这么好,那么我们为什么不直接用全等监视呢?当然,任何事情都有好的坏的两个方面,全等监视固然是好, 但是它在运行时需要先遍历整个监视对象,然后在每次$digest之前使用angular.copy()将整个对象深拷贝一遍 然后在运行之后用angular.equal()将前后的对象进行对比,上面的例子中因为items比较简单,因此可能性能上不会有什么差别, 但是到了实际生产时,我们要面对的数据千千万万,可能因为全等监视这一个设置就会消耗大量的资源,让应用停滞不前。 因此这就需要我们在使用时进行权衡,究竟应该使用哪一种监视方式。

脏检查慢吗?

说实话脏检查效率是不高,但是也谈不上有多慢。简单的数字或字符串比较能有多慢呢?十几个表达式的脏检查可以直接忽略不计; 上百个也可以接受;成百上千个就有很大问题了。绑定大量表达式时请注意所绑定的表达式效率。建议注意一下几点: •表达式(以及表达式所调用的函数)中少写太过复杂的逻辑 •不要连接太长的 filter(往往 filter 里都会遍历并且生成新数组) •不要访问 DOM 元素。 1、使用单次绑定减少绑定表达式数量 单次绑定(One-time binding 是 Angular 1.3 就引入的一种特殊的表达式, 它以 :: 开头,当脏检查发现这种表达式的值不为 undefined 时就认为此表达式已经稳定,并取消对此表达式的监视。 这是一种行之有效的减少绑定表达式数量的方法,与ng-repeat 连用效果更佳,但过度使用也容易引发 bug。 2、善用 ng-if 减少绑定表达式的数量

问题一:为什么例五里面自定义指令需要调用$apply方法?是不是自定义指令都不具备$apply方法?

答:不是。不是因为自定义指令本身不具有$apply方法,因为element.on(“click”,function{})这实际上是一个jQuery方法,而jQuery是不具备$apply方法的。如果我们把scope.b++使用一个angular方法去触发,是不用调用$apply就可以触发的。

问题二:前文提到的循环十次会抛出异常,是指什么?

是指在一个ng-指令(本身已经自带$apply方法)内部再次调用$apply方法,就会抛出异常。这实际上是angular的保护机制。

问题三:dirty-checking这么复杂,会不会速度很慢?

不会的,实际上运行的很快。而且在ES6普及后,angular的未来版本会加入Object.observe,$digest循环的速度会更快。

PPT链接 视频链接

更多内容,可以加入IT交流群565734203与大家一起讨论交流

这里是技能树·IT修真院:https://www.jnshu.com,初学者转行到互联网的聚集地

猜你喜欢

转载自blog.csdn.net/jnshu_it/article/details/84258671
今日推荐