Angularjs 数据的双向绑定和controller通信

数据的双向绑定

Angular实现了双向绑定机制。所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面。

angular并不存在定时脏检测,而是触发指定函数进入angular的digest流程:
- DOM事件,如输入文本,点击按钮等
- XHR响应事件($http)
- 浏览器Location变更($location)
- Timer事件($timeout, $interval)
- 执行 $digest() 和 $apply()
digest循环就是遍历$watch(), 查看是否有变量发生改变,如果本轮循环有变量变化那就会进行第二轮循环,直至变量不再改变,然后再更新DOM上的变量值

一个最简单的示例就是这样:

<div ng-controller="CounterCtrl">
    <span ng-bind="counter"></span>
    <button ng-click="counter=counter+1">increase</button>
</div>
function CounterCtrl($scope) {
    $scope.counter = 1;
}

这个例子很简单,每当点击一次按钮,界面上的数字就增加一。

绑定数据是怎样生效的

初学AngularJS的人可能会踩到这样的坑,假设有一个指令:

var app = angular.module("test", []);

app.directive("myclick", function() {
    return function (scope, element, attr) {
        element.on("click", function() {
            scope.counter++;
        });
    };
});

app.controller("CounterCtrl", function($scope) {
    $scope.counter = 0;
});
<body ng-app="test">
    <div ng-controller="CounterCtrl">
        <button myclick>increase</button>
        <span ng-bind="counter"></span>
    </div>
</body>

这个时候,点击按钮,界面上的数字并不会增加。很多人会感到迷惑,因为他查看调试器,发现数据确实已经增加了,Angular不是双向绑定吗,为什么数据变化了,界面没有跟着刷新?

试试在scope.counter++;这句之后加一句scope.digest();再看看是不是好了?

为什么要这么做呢,什么情况下要这么做呢?我们发现第一个例子中并没有digest,而且,如果你写了digest,它还会抛出异常,说正在做其他的digest,这是怎么回事?

我们先想想,假如没有AngularJS,我们想要自己实现这么个功能,应该怎样?

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>two-way binding</title>
    </head>
    <body onload="init()">
        <button ng-click="inc">
            increase 1
        </button>
        <button ng-click="inc2">
            increase 2
        </button>
        <span style="color:red" ng-bind="counter"></span>
        <span style="color:blue" ng-bind="counter"></span>
        <span style="color:green" ng-bind="counter"></span>

        <script type="text/javascript">
            /* 数据模型区开始 */
            var counter = 0;

            function inc() {
                counter++;
            }

            function inc2() {
                counter+=2;
            }
            /* 数据模型区结束 */

            /* 绑定关系区开始 */
            function init() {
                bind();
            }

            function bind() {
                var list = document.querySelectorAll("[ng-click]");
                for (var i=0; i<list.length; i++) {
                    list[i].onclick = (function(index) {
                        return function() {
                            window[list[index].getAttribute("ng-click")]();
                            apply();
                        };
                    })(i);
                }
            }

            function apply() {
                var list = document.querySelectorAll("[ng-bind='counter']");
                for (var i=0; i<list.length; i++) {
                    list[i].innerHTML = counter;
                }
            }
            /* 绑定关系区结束 */
        </script>
    </body>
</html>

可以看到,在这么一个简单的例子中,我们做了一些双向绑定的事情。从两个按钮的点击到数据的变更,这个很好理解,但我们没有直接使用DOM的onclick方法,而是搞了一个ng-click,然后在bind里面把这个ng-click对应的函数拿出来,绑定到onclick的事件处理函数中。为什么要这样呢?因为数据虽然变更了,但是还没有往界面上填充,我们需要在此做一些附加操作。

从另外一个方面看,当数据变更的时候,需要把这个变更应用到界面上,也就是那三个span里。但由于Angular使用的是脏检测,意味着当改变数据之后,你自己要做一些事情来触发脏检测,然后再应用到这个数据对应的DOM元素上。问题就在于,怎样触发脏检测?什么时候触发?

我们知道,一些基于setter的框架,它可以在给数据设值的时候,对DOM元素上的绑定变量作重新赋值。脏检测的机制没有这个阶段,它没有任何途径在数据变更之后立即得到通知,所以只能在每个事件入口中手动调用apply(),把数据的变更应用到界面上。在真正的Angular实现中,这里先进行脏检测,确定数据有变化了,然后才对界面设值。

所以,我们在ng-click里面封装真正的click,最重要的作用是为了在之后追加一次apply(),把数据的变更应用到界面上去。

那么,为什么在ng-click里面调用$digest的话,会报错呢?因为Angular的设计,同一时间只允许一个$digest运行,而ng-click这种内置指令已经触发了$digest,当前的还没有走完,所以就出错了。

$digest$apply

在Angular中,有$apply$digest两个函数,我们刚才是通过$digest来让这个数据应用到界面上。但这个时候,也可以不用$digest,而是使用$apply,效果是一样的,那么,它们的差异是什么呢?

最直接的差异是,$apply可以带参数,它可以接受一个函数,然后在应用数据之后,调用这个函数。所以,一般在集成非Angular框架的代码时,可以把代码写在这个里面调用。

var app = angular.module("test", []);

app.directive("myclick", function() {
    return function (scope, element, attr) {
        element.on("click", function() {
            scope.counter++;
            scope.$apply(function() {
                scope.counter++;
            });
        });
    };
});

app.controller("CounterCtrl", function($scope) {
    $scope.counter = 0;
});

除此之外,还有别的区别吗?

在简单的数据模型中,这两者没有本质差别,但是当有层次结构的时候,就不一样了。考虑到有两层作用域,我们可以在父作用域上调用这两个函数,也可以在子作用域上调用,这个时候就能看到差别了。

对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例:

var app = angular.module("test", []);

app.directive("increasea", function() {
    return function (scope, element, attr) {
        element.on("click", function() {
            scope.a++;
            scope.$digest();
        });
    };
});

app.directive("increaseb", function() {
    return function (scope, element, attr) {
        element.on("click", function() {
            scope.b++;
            scope.$digest();    //这个换成$apply即可
        });
    };
});

app.controller("OuterCtrl", ["$scope", function($scope) {
    $scope.a = 1;

    $scope.$watch("a", function(newVal) {
        console.log("a:" + newVal);
    });

    $scope.$on("test", function(evt) {
        $scope.a++;
    });
}]);

app.controller("InnerCtrl", ["$scope", function($scope) {
    $scope.b = 2;

    $scope.$watch("b", function(newVal) {
        console.log("b:" + newVal);
        $scope.$emit("test", newVal);
    });
}]);
<div ng-app="test">
    <div ng-controller="OuterCtrl">
        <div ng-controller="InnerCtrl">
            <button increaseb>increase b</button>
            <span ng-bind="b"></span>
        </div>
        <button increasea>increase a</button>
        <span ng-bind="a"></span>
    </div>
</div>

这时候,我们就能看出差别了,在increase b按钮上点击,这时候,a跟b的值其实都已经变化了,但是界面上的a没有更新,直到点击一次increase a,这时候刚才对a的累加才会一次更新上来。怎么解决这个问题呢?只需在increaseb这个指令的实现中,把$digest换成$apply即可。

当调用$digest的时候,只触发当前作用域和它的子作用域上的监控,但是当调用$apply的时候,会触发作用域树上的所有监控。

因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控。

从另外一个角度,我们也可以看到,为什么调用外部框架的时候,是推荐放在$apply中,因为只有这个地方才是对所有数据变更都应用的地方,如果用$digest,有可能临时丢失数据变更。

1、更改作用域为何会刷新视图?(location变化)
这里写图片描述
这里写图片描述
2、在事件中更改作用域,为何会刷新视图?(ng-click)
<button ng-click="click()"></button>
这里写图片描述
3、在视图中更改数据,为何会影响作用域。(on-change)
<input type="text" ng-model="name" />
这里写图片描述
其中,有三个概念:
watch digest():脏检查
$apply:提供上下文执行回调函数

$apply伪代码:

var $apply=function(exp){
 $eval(exp);
 $rootScope.$digest();
}

转自 https://www.zhihu.com/question/23275373

controller通信

作用域

ng-controller ng-repeat ng-include ng-switch都会创建新的作用域, 子作用域可以直接用父作用域中的引用类型变量, 也可以通过$scope.$parent.name访问父作用域中的基本数据类型的变量,如果直接通过$scope.name访问父作用域中的变量是达不到效果的,而是会在当前作用域下创建一个同名变量,所以在ng-repeat中的item如果是继承父作用域的变量必须是个引用类型,否则会在ng-repeat作用域下新建变量,达不到与父作用域变量绑定的效果。$scope.$$childHead指向第一个子作用域,$scope.$$childTail指向最后一个子作用域,$scope.$$nextSibling指向下一个相邻作用域,$scope.$$prevSibling指向上一个相邻作用域

注入服务

angular.module('demo')
    .factory('Data', function(){
        return {
            name: 'htf'
        };
    })
.controller('childCtrl1', ['$scope', 'Data', function($scope, Data){
    $scope.data = Data;
}])

.controller('childCtrl2', ['$scope', 'Data', function($scope, Data){
    $scope.data = Data;
}])

基于事件

当我们在创建新的控制器时,angularJS会帮我们生成并传递一个新的$scope对象给这个controller,在angularJS应用中的任何一个部分,都有父级作用域的存在,顶级就是ng-app所在的层级,它的父级作用域就是$rootScope。

每个$scope的$root指向$rootScope, $scope.$parent指向父级作用域。

Angularjs在scope中为我们提供了冒泡和隧道机制,$broadcast会把事件广播给所有子controller,而$emit则会将事件冒泡传递给父controller,$on则是angularjs的事件注册函数,有了这一些我们就能很快的以angularjs的方式去解决angularjs controller之间的通信,要注意的是子scope通过 $emit 发出的事件名不能与父scope用 $broadcast 的事件名一样,防止进入死循环

//HTML
<div ng-app="app" ng-controller="parentCtr">
     <div ng-controller="childCtr1">name :
         <input ng-model="name" type="text" ng-change="change(name);" />
     </div>
     <div ng-controller="childCtr2">Ctr1 name:
         <input ng-model="ctr1Name" />
     </div>
 </div>
//JS
angular.module("app", []).controller("parentCtr",

function ($scope) {
    $scope.$on("Ctr1NameChange",

    function (event, msg) {
        console.log("parent", msg);
        $scope.$broadcast("Ctr1NameChangeFromParrent", msg);
    });
}).controller("childCtr1", function ($scope) {
    $scope.change = function (name) {
        console.log("childCtr1", name);
        $scope.$emit("Ctr1NameChange", name);
    };
}).controller("childCtr2", function ($scope) {
    $scope.$on("Ctr1NameChangeFromParrent",

    function (event, msg) {
        console.log("childCtr2", msg);
        $scope.ctr1Name = msg;
    });
});

兄弟通信可以通过父作用域通信,也可以在根作用域广播事件,$rootScope.$broadcast(“functionname”,”value”), 子作用域监听事件

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值