深入了解angularjs中的$digest与$apply方法,从区别聊到使用优化
壹 ❀ 引
如果有人问,在angularjs中修改模型数据为何视图会同步更新呢,我想大多数人一定会回答脏检查(Dirty Checking)相关概念。没错,在angularjs中作用域(scope)作为链接控制器(controller)与视图(view)之间的桥梁,除了绑定数据监听事件外,一旦有数据发生改变,scope还兼顾了脏检测更新视图的职责,这是我们宏观的理解。
这就引发了一系列的问题,以点击事件为例,为什么在angularjs中用原生click事件达不到更新视图的效果?ng-click与原生click有何区别?ng-click触发后angularjs又是怎么让视图更新的呢?$digest和$apply这两个眼熟的方法究竟有何作用,两者有什么区别?如果你对于这些问题感兴趣,不妨静下心来读一读本文,那么本文开始。
贰 ❀ angularjs的数据绑定
现在有个需求,当我们点击按钮时需要更新视图中的文本信息,当然不通过angularjs我们使用原生js做法也能轻易实现,像这样:
<div>我的名字是:<span class="name"></span></div>
<button class="btn">click me</button>
let btn = document.querySelector('.btn');
let name = document.querySelector('.name');
btn.onclick = function () {
name.innerHTML = '听风是风';
};
但这样做就有两个问题,第一我们不得不操作DOM,第二不便于复用,如果我们希望点击后将name字段更新到DOM不同层级的各种地方,此时获取DOM就尤为复杂了。
而angularjs便提供了一种有效的解决方法---数据绑定,它将我们需要更新的name字段抽离成了一份数据,在使用时你不用关心这份数据与DOM结构的内在联系,你要考虑的仅仅是在何处放置这份数据而已。
同样还是上面的需求,我们使用angularjs就可以这么做:
<body ng-controller="myCtrl as vm">
<div>我的名字是:<span>{{vm.name}}</span></div>
<button class="btn" ng-click="vm.sayName()">click me</button>
</body>
angular.module('myApp', [])
.controller('myCtrl', function () {
let vm = this;
vm.name = '';
vm.sayName = function () {
vm.name = '听风是风';
};
});
我们仔细对比这两种实现,js是click事件作为媒介找到对应的DOM并操作DOM,而angularjs通过click事件操作的却只是数据,前者操作DOM后者操作数据。突然想起了事件驱动与数据驱动的概念,也有那么点意思了。
这就比较神奇了,当我们点击按钮,name的值发生了改变,同时视图上也同步进行了刷新,angularjs是如何感知变化,又是怎么通过到视图的呢?这就得介绍$digest循环了。
叁 ❀ 神器的$digest
angularjs的事件循环又称为$digest循环,循环过程中包含了数据的脏检测,准确来说angularjs的脏检测功能是由scope上的$digest()方法实现,这里我们先理解$digest与脏检测的关系。
angularjs中的$digest循环主要包含了$watch列表与$evalAsync列表两个部分,$evalAsync列表先不分析,看到$watch列表大家是不是有点想法了呢?
没错,这里的$watch列表就是一个包含了多个$watch监听的数组,在scope中以$$watchers字段表示。$watch大家都不会陌生,监听一份数据,如果发生改变则执行对应回调,而angularjs便是利用$watch监听了我们需要交互的每份数据,只要发生改变,底层将通知视图进行更新。
说到这大家就纳闷了,我编程中明明没加$watch,哪来的呢?其实在angularjs使用中,无论是表达式{{}}还是ng-bind,凡是与视图上与数据交互的地方angualrjs都会帮你去注册watch监听,我们来看个简单的例子:
<body ng-app="myApp">
<div ng-controller="myCtrl as vm">
纯路人,{{vm.name}}非常{{vm.describe}}
<div ng-bind="vm.age"></div>
</div>
</body>
angular.module('myApp', [])
.controller('myCtrl', function () {
var vm = this;
vm.name = "听风是风";
vm.describe = '帅';
vm.age = 26;
});
在这个例子中,我们定义了三个属性,并在视图上与之绑定,查看当前控制器scope属性下的$$watchers属性(这个需要谷歌插件才能查看,插件名 ng-inspect for AngularJS),可以看到类型为Array,数组中包含的三个监听器分别对应我们前面定义的数据。一旦数据发生变化,$watch回调负责更新视图。
一个新的问题就是,angularjs的$watch又是如何感知哪些值发生了变化呢,这就像ng-click能执行代码是依赖了点击行为,毕竟总不能用定时器一直监听吧。
关于这一点就又回到了我们前面提到的$digest循环上了,angularjs在每次调用$scope.$digest()方法都会发起$digest循环,在循环中angularjs会触发$$watchers中的每一个$watch(脏检测),有了触发源$watch要做的就是新旧值对比,以及发生变化后的相应操作了。
OK,到这里我们明白了调用$socpe.$digest()会触发$digest循环,在循环中又会触发所有$watch进行数据对比,也就是我们说的脏检查,以及在数据变更后对视图进行更新。
那么$apply与$digest又有什么联系呢?我们接着说。
肆 ❀ $apply与$digest
在angularjs开发中,大家一定有过这样的经历,如果一段断码明明修改了数据但视图没变化,用$scope.$apply方法包裹代码就能解决该问题。比如我们在前文中click与ng-click的例子,这是为什么?
不卖关子,click之所以无法触发视图更新,这是因为click绑定函数中的函数作用域已经脱离了angularjs的上下文,angularjs的$digest循环无法感知脱离angularjs作用域的数据变化(你变了我不知道)。
使用$apply就能让angularjs执行脏检查的本质其实就是$apply也触发了$digest循环,准确来说,执行$scope.$apply后会调用$rootscope.$digest,所以只要使用了$apply方法,angularjs都 会从根作用域开始遍历每个作用域中的每个$warchers。
相比之下像angularjs中内置的事件比如ng-click都内置了$apply用于触发$digest循环,如果我们依旧使用$apply,angularjs反而会报错告诉你已经启动了$apply,一个简单的例子:
<button class="btn" ng-click="vm.sayName()">click me</button>
angular.module('myApp', [])
.controller('myCtrl', function ($scope) {
let vm = this;
vm.name = '';
vm.sayName = function () {
$scope.$apply(function () {
vm.name = '听风是风';
});
};
});
OK,打到这里我们知道了$apply方法执行会调用$rootscope.$digest方法,从而启动全新的$digest循环,对所有作用域中的数据进行脏检查。
伍 ❀ 何时使用$apply
在angularjs controller中的任何地方都属于angularjs的上下文,在这个上下文中直接修改变量都不需要$apply,但如果你在普通函数以及非angularjs提供的回调函数中修改变量,此时都需要结合$apply来通知angularjs进行额外的脏检查。举几个例子:
1.普通事件绑定的函数内修改数据需要使用$apply,文章开头已有举例。
2.普通定时器回调中修改数据需要使用$apply,一般推荐使用angularjs封装的定时器,比如$timeout:
//angularjs定时器
$timeout(() => vm.name = '听风是风', 1000);
//普通定时器
setTimeout(() => {
$scope.$apply(() => vm.name = '时间跳跃');
}, 2000);
在上文中我们已经说了,如果调用了$apply 等同于$rootscope.$digest,这样性能其实是不太好的,特别是存在多个scope的情况下,我们往往更喜欢只检测当前作用域的数据变化。
更加优化的做法是在当前作用域调用$digest,像这样:
setTimeout(() => {
$scope.$apply(() => vm.name = '时间跳跃');
}, 1000); setTimeout(function () {
vm.name = '听风是风';
$scope.$digest();
}, 2000);
陆 ❀ 总
那么到这里,我们详细介绍了$digest与$apply方法的区别,在介绍$digest循环后了解到$digest是由$apply触发,从而也解释了ng-click与普通click的区别。在介绍了$apply后,我们简单提及了使用$apply的场景,我们知道它会让angualrjs从根作用域开始脏检测,代价较大,因此推荐使用$digest可代替。那么到这里,本文结束。
参考
理解Angular中的$apply()以及$digest()
angularjs权威指南
深入了解angularjs中的$digest与$apply方法,从区别聊到使用优化的更多相关文章
- angularjs 中的$digest和$apply区别
$digest和$apply 在Angular中,有$apply和$digest两个函数,我们刚才是通过$digest来让这个数据应用到界面上.但这个时候,也可以不用$digest,而是使用$appl ...
- AngularJS中的digest循环$apply
欢迎大家指导与讨论 : ) 前言 Angular会拓展这个标准的浏览器流程,创建一个Angular上下文.这个Angular上下文指的是运行在Angular事件循环内的特定代码,该Angular事件循 ...
- js中的call和apply方法的区别
一.call和apply的说明 1.call,apply都属于Function.prototype的一个方法,它是JavaScript引擎内在实现的,因为属于Function.prototype,所以 ...
- 转发: JS中的call()和apply()方法和区别 --小白变色记
一.方法定义: apply:调用一个对象的一个方法,用另一个对象替换当前对象.例如:B.apply(A, arguments);即A对象应用B对象的方法. call:调用一个对象的一个方法,用另一个对 ...
- angularjs中provider,factory,service的区别和用法
angularjs中provider,factory,service的区别和用法 都能提供service,但是又有差别 service 第一次被注入时实例化,只实例化一次,整个应用的生命周期中是个单例 ...
- JS中的call()和apply()方法(转)
转自:http://uule.iteye.com/blog/1158829 JS中的call()和apply()方法 博客分类: JS 1.方法定义 call方法: 语法:call([thisOb ...
- 我总结的call()与apply()方法的区别
[call()与apply()的区别]在ECMAScript中每一个函数都是function类型(是javascript的基本引用类型)的实例,具有一定的属性和方法.call()和apply()则是这 ...
- 正则表达式中的exec和match方法的区别
正则表达式中的exec和match方法的区别 字符串的正则方法有:match().replace().search().split() 正则对象的方法有:exec().test() 1.match m ...
- IL角度理解C#中字段,属性与方法的区别
IL角度理解C#中字段,属性与方法的区别 1.字段,属性与方法的区别 字段的本质是变量,直接在类或者结构体中声明.类或者结构体中会有实例字段,静态字段等(静态字段可实现内存共享功能,比如数学上的pi就 ...
随机推荐
- C# Properties文件夹 Bin 目录 Bin 目录
Properties文件夹 定义你程序集的属性 项目属性文件夹 一般只有一个 AssemblyInfo.cs 类文件,用于保存程序集的信息,如名称,版本等,这些信息一般与项目属性面板中的数据对应,不需 ...
- socket实现ftp上传下载
socket实现ftp文件的上传和下载 server端代码: import socket import json import struct import os soc = socket.socket ...
- 小白的springboot之路(七)、事务支持
0-前言 事务管理对于企业级应用来说必不可少,用来确保数据的完整性和一致性: 1-开启事务 spring boot支持编程式事务和声明式事务,用声明式事务即可: spring boot开启事务非常简单 ...
- MySQL必知必会(使用子查询)
SELECT cust_name, cust_contact FROM customers WHERE cust_id IN (SELECT cust_id FROM orders #单独写多个分句, ...
- Vue全家桶高仿小米商城
大家好,我是河畔一角,时隔半年再次给大家带来一门重量级的实战课程:<Vue全家桶高仿小米商城>,现在很多公司都在参与到商城的构建体系当中,因此掌握一套商城的标准开发体系非常重要:商城的开始 ...
- 分析Crash文件
应用在没有发布前,可以通过打印log很方便的查看错误信息.但是发布后,就需要根据Crash文件来定位了. 将手机连接电脑,通过XCode获取Crash文件.Window ->Devices -& ...
- 一台电脑如何管理多个ssh key
目录 一.生成ssh key 1.1 生成密钥(必须) 1.2 设置路径 (可选) 1.3 指定密语字符串(可选) 二.设置ssh key的代理 2.1. 首先查看代理 2.2. 添加私钥 三.添加公 ...
- ubuntu下安装gcc
在ubuntu下安装gcc 第一次写blog,多多包涵! gcc安装步骤 废话不多说,gcc安装步骤如下: 1. sudo apt update 2. sudo apt install build-e ...
- 使用 RMI 实现方法的远程调用
RMI 介绍 RMI 指的是远程方法调用 (Remote Method Invocation).它是一种机制,能够让在某个 Java虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法.可以用此 ...
- Cannot forward after response has been committed问题的解决
Cannot forward after response has been committed问题解决及分析 通过TOMCAT把系统启动,可以正常登陆门户,登陆进去选择子系统的时候点击登陆的时候,可 ...