这篇文章源于今天在项目中定位的1个bug,情况是这样子的:任务大类和任务小类是2个存在级联关系的select,当任务大类变化的时候会级联刷新任务小类。
页面刚进来的时候,如果任务大类的默认选中值是空(place holder),任务小类select内容也是空(只有1个place holder),显示效果图如下,这样是没有问题的。
但是当页面刚进来的时候,任务大类不是空的时候,任务小类select数据没有加载,效果图如下,显然这是问题。
我们自定义的select控件,大致代码如下:
Nf.mspl.form.dataField.SimpleSelect = function(id){
// 控件的配置信息
var optionsJson = JSON.parse($.trim($E(id).attr("json")));
// angularJS的controller,当页面bootstrap之后才会被调用
this.ngController(id, function($scope, $element, $ionicModal) {
"$scope:nomunge, $element:nomunge, $ionicModal:nomunge";
self.$scope = $scope;
$scope.listItems = [];
// 从服务端加载数据
$scope.loadListFromService=function(parentValue){
// start for loadData
Spl.MessageProcessor.loadData({
serviceId : optionsJson.serviceId,
data : {"parentValue" : parentValue},
success : function(json) {
refreshScopeList(json);
},
error: function() {
console.error("invoke service["+optionsJson.serviceId+"] error.");
}
});
}
$scope.refreshScopeList = function(json){
var tempForScope = [];
for (var i = 0; i < json.length; i++)
{
tempForScope.push({"value":json[i].value, "text":json[i].text});
}
// refresh data in scope
$scope.listItems = tempForScope;
}
// 没有父select 需要加载这个最顶级select的数据
if(optionsJson.serviceId && !optionsJson.parentSelectName)
{
$scope.loadListFromService("");
}
// init child select
// this is a child select,but its parent select default value is not empty.Thus need to load child select.
if(parentSelectName)
{
var $parent_select = CurrentPage().getPageOutestJQuery().find("*[name='"+parentSelectName+"']");
// 获取父select的默认选中的值
var parentVal = $parent_select.val();
console.log("begin to load child select.parentVal="+parentVal);
if(parentVal)
{
$scope.loadListFromService(parentVal);
}
}
});
}
发现问题其实很简单,我们有一行日志"begin to load child select.parentVal",当任务大类默认选中值不是空的时候,这个时候任务小类也需要加载数据,但是在子select中获取到的父select的选中值是空。问题就出现在这里:初始化子select(任务小类)的时候,无法获取到父select(任务大类)的选中值。
有2种可能会导致上面的问题:1.获取到的$parent_select是错误的或者是没有获取到 2.$parent_select是正确的,但是默认选中值不对。只需要使用$parent_select.prop("outerHTML") 打印出html即可。最终结果大致如下:
<select id="nf3" name="task_category">
<!-- ngRepeat: item in listItems -->
</select>
到这里问题很明显了:虽然任务大类选择框最终显示是正确的,但是我们给$scope.listItems赋值之后,拿不到select下的<option>(这些option是通过angularJS的ng-repeat指令动态生成的)。可以这么理解:angularJS数据双向绑定是异步的,需要时间的。我们在给$scope中变量赋值之后,angular刷新界面dom是需要时间的。
下面这段代码模拟上面的效果:
<html>
<head>
<script src="jquery-1.11.1.min.js"></script>
<script src="angular.js"></script>
<script>
var rootMoudle = angular.module('module', []);
rootMoudle.controller("root_controller",function($scope){
// 设置select默认选中的option
$scope.selectVal = 2;
// 加载数据,生成option
$scope.listItems = load();
setTimeout(function(){
console.log("delay");
showSelectContent();
},200);
console.log("immediately");
showSelectContent();
});
function showSelectContent()
{
console.log("content=" + $("#aty").html());
console.log("value="+$("#aty").val());
}
$(function(){
angular.bootstrap($("#root"),["module"]);
})
// 模拟通过ajax加载服务端返回的数据
function load()
{
return [{"value":1,"text":"a1"},
{"value":2,"text":"b1"},
{"value":3,"text":"c1"}];
}
</script>
</head>
<body id="root" ng-controller="root_controller">
<select id="aty">
<option ng-repeat="item in listItems" ng-selected="item.value == selectVal" value="{{item.value}}">{{item.text}}</option>
</select>
</body>
</html>
上面的输出结果的确可以证实我们的猜想:我们给$scope.listItems赋值之后,angularJS的双向绑定特性会将变化刷新到html页面上,但是之后的js代码是无法立刻获取到界面最新的dom,因为刷新dom是需要时间的,所以我们延时200ms之后就没有问题了。
可以看到同时使用angularJS和jQuery的时候容易出现这种顺序问题,jQuery的核心是操作界面上的dom,angularjs核心是操作scope中的数据。angularJS会根据scope中数据的变化,自动刷新dom(对dom进行增、删、改)。显然这种刷新是需要时间的,正是因为如此,才会导致jquery无法正确、及时的获取最新的dom。
当然,不要写出这些依赖于顺序的代码,如果我们使用了angularJS,就应该按照angularJS的理念去编写代码。网上有很多这种案例,按照传统jQuery的想法去编写angularJS会遇到各种问题或者困难。
可以看看“ Think in AngularJS:对比jQuery和AngularJS的不同思维模式”这篇文章。