前端开发框架总结之Angular实用技巧(二)
上文讲了Angular自定义指令,今天我们再来介绍下Angular数据绑定相关的知识。很多都是个人理解,大家选择性参考吧。
-
数据绑定、监听、页面刷新
想要理解Angular数据双向绑定原理,那就要去看ng-model指令的实现过程。在实际项目中我们要理解两个重要变量$viewValue,$modelValue和两个个重要方法$render,$setViewValue。其中$viewValue是与页面相关的变量(和页面值不一定相等),$modelValue是与数据模型相关的变量(不一定相等),$render方法是把$viewValue的值同步到页面元素上,利用页面元素事件的监听会触发$setViewValue方法把页面的值同步到$viewValue,$modelValue,然后是同步到ctrl中的scope的属性。在自定义指令中我们可以使用require:‘?^ngModel’的方式获取到指令所在元素的ngModelCtrl对象。还有元素属性获取,自定义校验,页面刷新方法重写等方式在下面的例子中贴出。
一个页面加载大致过程如下(只写一些我关注的阶段):
HTML页面加载-----> HTML中的DOM元素加载-----> js加载------->根据页面内容生成相应的watcher------>controler加载----->记录controler中的watcher------->自定义指令link方法----->$scope的$apply方法------>scope的$digest方法----->遍历所有watcher--->-------->根据规则,对所有watcher的回调函数调用。------->ngModelwatch中会对$scope中的变量,$modelValue,$viewValue进行同步,然后调用ngModel的render方法,把值同步到页面。(其他类型的绑定比如ng-bind,{{}},他们的同步方式类似,只是在最后一步的watcher回调中走了不同的listener。)。
对于双向绑定还有把页面元素的值同步到$scope变量中的过程。这个是由页面元素的时间触发的,比如input的输入等,这个会触发ngModel的$setViewValue方法。把页面的值通过给viewValue,modelValue,进而同步给$scope中的变量。
为了监听一些$scope变量的变化,我们可以使用$watch方法,注意:如果监听的变量是一个对象,那么我们只改变对象的某个属性是不会触发watch方法的,因为对象的引用并没有发生变化,如果想要触发watch,就要在添加watch的时候指明需要深度比较。另外watch方法想要触发,一定是变量值改变之后,触发了digest数据同步,才会触发watch方法的。
最后来讲一下,主动触发把$scope值同步到页面的几种方式。这里会用到4个函数。$apply(),$digest(),$applyAsync(),$evalAsync();
$apply:遍历所有的scope的watcher,把他们的数据同步至页面。本质上最终调用的还是$digest。
$digest:遍历当前scope中的watcher,把他们的数据同步至页面。
$applyAsync:跟apply功能相同。只不过是他的“异步方法”,把发同步的过程延迟触发。在http请求频繁的页面可以使用$httpProvider.useApplyAsync(true).这样可以把页面刷新合并,不会频繁的触发apply方法。
$evalAsync:跟apply的功能也类似,只不过如果一次apply触发的遍历如果还没有结束的话,使用$evalAsync会把这次要更新的内容在本次遍历中进行执行,而不是等上一个apply循环执行完成之后再发起一遍遍历了。
真正理解了以上4中方法的意思,就可以在合适的场景中使用合适的方法,进而提高页面效率。
另外比如ng-click等指令、包括$http方法的回调完成后,框架中都是会主动调用digest或者apply的,所以一般不需要我们手动触发,但如果元素的click事件是我们手动绑定的,那么就是不会触发同步操作的。$timeout方法也会触发digest过程的。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>angular-test</title>
</head>
<body ng-app="ExampleApp">
<div ng-controller="testCtrl">
<input ng-model="inputValue" test-render name="testInput">
<button id="btn" >手动绑定的点击事件</button>
<button ng-click="change()" >指令绑定的点击事件</button>
</div>
<script src="jquery-3.2.1.min.js"></script>
<script src="angular.js"></script>
<script>
var app = angular.module('ExampleApp', []);
app.controller('testCtrl', function ($scope,$timeout) {
$scope.inputValue = '初值';
$scope.$watch('inputValue',function (newVal, oldVal) {
console.log("watch:inputValue,newVal:" + newVal + ",oldVal:" + oldVal );
});
$scope.$watch('obj',function (newVal, oldVal) {
console.log("watch:obj,newVal:" + newVal + ",oldVal:" + oldVal );
},true);
$scope.obj = new Object();
$('#btn').on('click',btnClick);
$scope.change = function () {
$scope.obj.name = 'test';
$scope.inputValue = '修改后的input值';
}
function btnClick() {
$scope.obj.name = 'test';
$scope.inputValue = '修改后的input值';
//$timeout方法是不需要调用apply的,它自己会主动触发。
/*$timeout(function () {
$scope.obj.name = 'test';
$scope.inputValue = '修改后的input值';
});*/
//$scope.$apply();
//$scope.$applyAsync();
//$scope.$evalAsync();
$scope.$digest();
}
});
app.directive('testRender',function () {
return {
restrict: 'A',
require:'?^ngModel',
link:function (scope,element,attrs,ngModel) {
//获取地址应属性所在元素的属性
console.log('获取属性值name:' + attrs.name);
console.log('获取属性值ngModel:' + scope.$eval(attrs.ngModel));
//可以主动设置viewValue,但是必须手动调用render方法刷新界面。
//ngModel.$setViewValue('主动设置的input');
//可以重写render方法,对render过程进行拦截。
/*ngModel.$render = function () {
console.log("这是我的刷新逻辑");
};*/
//手动触发的刷新。
//ngModel.$render();
}
}
});
</script>
</body>
</html>