이 글은 주로 초보자, 이제 막 Angular를 접하기 시작했고 데이터 바인딩이 어떻게 작동하는지 이해하고 싶은 사람들을 위해 작성되었습니다. 이미 Angular에 대해 많이 알고 있다면 직접 소스 코드를 읽어보는 것이 좋습니다.
Angular 사용자는 모두 데이터 바인딩이 어떻게 구현되는지 알고 싶어합니다. 다양한 단어를 볼 수 있습니다: $watch, $apply, $digest, dirty-checking... 그게 무엇인가요? 어떻게 작동하나요? 여기서는 이러한 질문에 답하고 싶습니다. 사실 공식 문서에 답변이 나와 있지만 여전히 이를 결합하고 싶지만 설명하기 위해 간단한 방법을 사용합니다. 암호.
처음부터 시작해 보겠습니다.
브라우저 이벤트 루프 및 Angular.js 확장
브라우저는 항상 사용자 상호 작용과 같은 이벤트를 기다리고 있습니다. 버튼을 클릭하거나 입력란에 무엇인가를 입력하면 자바스크립트 인터프리터에서 해당 이벤트의 콜백 함수가 실행되며, 콜백 함수가 완료되면 브라우저는 DOM에서 동작하게 됩니다. 그에 따라 변경하십시오. Angular는 이 이벤트 루프를 확장하여 때때로 앵귤러 컨텍스트라고 불리는 실행 환경을 생성합니다(이것이 중요한 개념이라는 것을 기억하세요). 컨텍스트가 무엇이고 어떻게 작동하는지 설명하려면 몇 가지 개념을 더 설명해야 합니다.
$watch 대기열($watch 목록)
UI에 무언가를 바인딩할 때마다 $watch 대기열에 $watch를 삽입합니다. $watch가 모니터링하는 모델의 변화를 감지할 수 있는 것이라고 상상해 보세요. 예를 들어 다음 코드는
index.html
User: <input type="text" ng-model="user" /> Password: <input type="password" ng-model="pass" />
여기에는 On으로 바인딩된 $scope.user가 있습니다. 첫 번째 입력 상자에는 두 번째 입력 상자에 바인딩된 $scope.pass도 있습니다. 그런 다음 $watch 목록에 두 개의 $watch
를 추가하고 아래 예를 계속 살펴봅니다. 🎜>controllers.js
app.controller('MainCtrl', function($scope) { $scope.foo = "Foo"; $scope.world = "World"; });
index.html
Hello, {{ World }}
여기서 $scope에 두 가지를 추가했지만 UI에는 하나만 바인딩되어 있으므로 $watch는 하나만 생성됩니다.
다시 살펴보기 아래 예시:
controllers.js
app.controller('MainCtrl', function($scope) { $scope.people = [...]; });
index.html
<ul> <li ng-repeat="person in people"> {{person.name}} - {{person.age}} </li> </ul>
얼마나 많은 $watch가 생성되나요? 여기? 사람마다 2개(이름 1개, 나이 1개)가 있고 ng-repeat가 1개 있으므로 총 10명이면 (2 * 10) + 1이므로 $시계가 21개가 됩니다. 따라서 UI에 바인딩된 모든 데이터는 $watch를 생성합니다. 네, $watch는 언제 생성되었나요? 템플릿이 로드되면, 즉 연결 단계(Angular는 컴파일 단계와 연결 단계로 구분됩니다---번역가의 메모)에서 Angular 인터프리터는 각 지시문을 찾아 필요한 각 $watch를 생성합니다. 좋은 것 같지만 다음은 무엇입니까?
$digest 루프
앞서 언급한 확장 이벤트 루프를 기억하시나요? $digest 루프는 브라우저가 Angular 컨텍스트에서 처리할 수 있는 이벤트를 수신할 때 트리거됩니다. 이 루프는 두 개의 작은 루프로 구성됩니다. 하나는 evalAsync 대기열을 처리하고 다른 하나는 이 블로그 게시물의 주제이기도 한 $watch 대기열을 처리합니다. 이것은 무엇을 다루나요? $digest는 $watch를 반복하여 다음과 같이 묻습니다.
안녕 $watch, 당신의 가치는 무엇인가요?
9시입니다.그럼 바뀌었나요?
아니요, 선생님.
(이 변수는 변하지 않았으니 다음 변수)
당신은 어떤가요, 당신의 가치는 무엇인가요?
신고하세요 푸입니다.
지금 뭔가 달라진 게 있나요?
변경되었습니다. 방금 Bar였습니다.
(좋습니다. 업데이트해야 할 DOM이 있습니다.)
$watch 대기열이 확인될 때까지 계속 질문하세요.
이를 더티 검사라고 합니다. 이제 모든 $watch가 확인되었으므로 질문해야 합니다. $watch가 업데이트되었습니까? 적어도 하나가 업데이트된 경우 모든 $watches가 변경되지 않을 때까지 루프가 다시 트리거됩니다. 이렇게 하면 각 모델이 다시 변경되지 않습니다. 루프가 10회를 초과하면 무한 루프를 방지하기 위해 예외가 발생한다는 점을 기억하세요. $digest 루프가 끝나면 그에 따라 DOM이 변경됩니다.
예: Controllers.js
app.controller('MainCtrl', function() { $scope.name = "Foo"; $scope.changeFoo = function() { $scope.name = "Bar"; } });
index.html
{{ name }} <button ng-click="changeFoo()">Change the name</button>
여기에는 ng-click이 $watch를 생성하지 않기 때문에 $watch가 있습니다(함수는 변경되지 않음).
버튼을 누릅니다
브라우저는 이벤트를 수신하고 각도 컨텍스트로 들어갑니다(이유는 나중에 설명합니다).
$digest 루프가 실행되기 시작하여 각 $watch가 변경되었는지 쿼리합니다.
$scope.name을 모니터링하는 $watch가 변경 사항을 보고하므로 또 다른 $digest 주기가 강제 실행됩니다.
변경 사항이 감지되지 않은 새로운 $digest 루프입니다.
브라우저가 제어권을 되찾고 $scope.name의 새 값에 해당하는 DOM 부분을 업데이트합니다.
여기서 매우 중요한 점(그리고 많은 사람들이 겪는 고충)은 각도 컨텍스트에 들어가는 모든 이벤트가 $digest 루프를 실행한다는 것입니다. 즉, 문자를 입력할 때마다 루프가 모든 $watch의 전체 페이지를 확인하세요.
$apply를 통해 각도 컨텍스트 입력
어떤 이벤트가 각도 컨텍스트에 들어가고 어떤 이벤트가 들어가지 않을지는 누가 결정합니까? $신청하세요!
如果当事件触发时,你调用$apply,它会进入angular context,如果没有调用就不会进入。现在你可能会问:刚才的例子里我也没有调用$apply啊,为什么?Angular为了做了!因此你点击带有ng-click的元素时,时间就会被封装到一个$apply调用。如果你有一个ng-model="foo"的输入框,然后你敲一个f,事件就会这样调用$apply("foo = 'f';")。
Angular什么时候不会自动为我们$apply呢?
这是Angular新手共同的痛处。为什么我的jQuery不会更新我绑定的东西呢?因为jQuery没有调用$apply,事件没有进入angular context,$digest循环永远没有执行。
我们来看一个有趣的例子:
假设我们有下面这个directive和controller
app.js
app.directive('clickable', function() { return { restrict: "E", scope: { foo: '=', bar: '=' }, template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>', link: function(scope, element, attrs) { element.bind('click', function() { scope.foo++; scope.bar++; }); } } }); app.controller('MainCtrl', function($scope) { $scope.foo = 0; $scope.bar = 0; });
它将foo和bar从controller里绑定到一个list里面,每次点击这个元素的时候,foo和bar都会自增1。
那我们点击元素的时候会发生什么呢?我们能看到更新吗?答案是否定的。因为点击事件是一个没有封装到$apply里面的常见的事件,这意味着我们会失去我们的计数吗?不会
真正的结果是:$scope确实改变了,但是没有强制$digest循环,监视foo 和bar的$watch没有执行。也就是说如果我们自己执行一次$apply那么这些$watch就会看见这些变化,然后根据需要更新DOM。
试试看吧:http://jsbin.com/opimat/2/
如果我们点击这个directive(蓝色区域),我们看不到任何变化,但是我们点击按钮时,点击数就更新了。如刚才说的,在这个directive上点击时我们不会触发$digest循环,但是当按钮被点击时,ng-click会调用$apply,然后就会执行$digest循环,于是所有的$watch都会被检查,当然就包括我们的foo和bar的$watch了。
现在你在想那并不是你想要的,你想要的是点击蓝色区域的时候就更新点击数。很简单,执行一下$apply就可以了:
element.bind('click', function() { scope.foo++; scope.bar++; scope.$apply(); });
$apply是我们的$scope(或者是direcvie里的link函数中的scope)的一个函数,调用它会强制一次$digest循环(除非当前正在执行循环,这种情况下会抛出一个异常,这是我们不需要在那里执行$apply的标志)。
试试看:http://jsbin.com/opimat/3/edit
有用啦!但是有一种更好的使用$apply的方法:
element.bind('click', function() { scope.$apply(function() { scope.foo++; scope.bar++; }); })
有什么不一样的?差别就是在第一个版本中,我们是在angular context的外面更新的数据,如果有发生错误,Angular永远不知道。很明显在这个像个小玩具的例子里面不会出什么大错,但是想象一下我们如果有个alert框显示错误给用户,然后我们有个第三方的库进行一个网络调用然后失败了,如果我们不把它封装进$apply里面,Angular永远不会知道失败了,alert框就永远不会弹出来了。
因此,如果你想使用一个jQuery插件,并且要执行$digest循环来更新你的DOM的话,要确保你调用了$apply。
有时候我想多说一句的是有些人在不得不调用$apply时会“感觉不妙”,因为他们会觉得他们做错了什么。其实不是这样的,Angular不是什么魔术师,他也不知道第三方库想要更新绑定的数据。
使用$watch来监视你自己的东西
你已经知道了我们设置的任何绑定都有一个它自己的$watch,当需要时更新DOM,但是我们如果要自定义自己的watches呢?简单
来看个例子:
app.js
app.controller('MainCtrl', function($scope) { $scope.name = "Angular"; $scope.updated = -1; $scope.$watch('name', function() { $scope.updated++; }); });
index.html
<body ng-controller="MainCtrl"> <input ng-model="name" /> Name updated: {{updated}} times. </body>
这就是我们创造一个新的$watch的方法。第一个参数是一个字符串或者函数,在这里是只是一个字符串,就是我们要监视的变量的名字,在这里,$scope.name(注意我们只需要用name)。第二个参数是当$watch说我监视的表达式发生变化后要执行的。我们要知道的第一件事就是当controller执行到这个$watch时,它会立即执行一次,因此我们设置updated为-1。
试试看:http://jsbin.com/ucaxan/1/edit
例子2:
app.js
app.controller('MainCtrl', function($scope) { $scope.name = "Angular"; $scope.updated = 0; $scope.$watch('name', function(newValue, oldValue) { if (newValue === oldValue) { return; } // AKA first run $scope.updated++; }); });
index.html
<body ng-controller="MainCtrl"> <input ng-model="name" /> Name updated: {{updated}} times. </body>
watch的第二个参数接受两个参数,新值和旧值。我们可以用他们来略过第一次的执行。通常你不需要略过第一次执行,但在这个例子里面你是需要的。灵活点嘛少年。
例子3:
app.js
app.controller('MainCtrl', function($scope) { $scope.user = { name: "Fox" }; $scope.updated = 0; $scope.$watch('user', function(newValue, oldValue) { if (newValue === oldValue) { return; } $scope.updated++; }); });
index.html
<body ng-controller="MainCtrl"> <input ng-model="user.name" /> Name updated: {{updated}} times. </body>
我们想要监视$scope.user对象里的任何变化,和以前一样这里只是用一个对象来代替前面的字符串。
试试看:http://jsbin.com/ucaxan/3/edit
呃?没用,为啥?因为$watch默认是比较两个对象所引用的是否相同,在例子1和2里面,每次更改$scope.name都会创建一个新的基本变量,因此$watch会执行,因为对这个变量的引用已经改变了。在上面的例子里,我们在监视$scope.user,当我们改变$scope.user.name时,对$scope.user的引用是不会改变的,我们只是每次创建了一个新的$scope.user.name,但是$scope.user永远是一样的。
例子4:
app.js
app.controller('MainCtrl', function($scope) { $scope.user = { name: "Fox" }; $scope.updated = 0; $scope.$watch('user', function(newValue, oldValue) { if (newValue === oldValue) { return; } $scope.updated++; }, true); });
index.html
<body ng-controller="MainCtrl"> <input ng-model="user.name" /> Name updated: {{updated}} times. </body>
试试看:http://jsbin.com/ucaxan/4/edit
现在有用了吧!因为我们对$watch加入了第三个参数,它是一个bool类型的参数,表示的是我们比较的是对象的值而不是引用。由于当我们更新$scope.user.name时$scope.user也会改变,所以能够正确触发。
关于$watch还有很多tips&tricks,但是这些都是基础。
总结
好吧,我希望你们已经学会了在Angular中数据绑定是如何工作的。我猜想你的第一印象是dirty-checking很慢,好吧,其实是不对的。它像闪电般快。但是,是的,如果你在一个模版里有2000-3000个watch,它会开始变慢。但是我觉得如果你达到这个数量级,就可以找个用户体验专家咨询一下了
无论如何,随着ECMAScript6的到来,在Angular未来的版本里我们将会有Object.observe那样会极大改善$digest循环的速度。同时未来的文章也会涉及一些tips&tricks。
另一方面,这个主题并不容易,如果你发现我落下了什么重要的东西或者有什么东西完全错了,请指正(原文是在GITHUB上PR 或报告issue)
希望本文所述对大家AngularJS程序设计有所帮助。