├── .gitignore ├── LICENSE ├── README.md ├── chapter1 ├── Checking-for-Dirty-Values.md ├── Destroying-A-Watch.md ├── Getting-Notified-of-Digests.md ├── Giving-Up-On-An-Unstable-Digest.md ├── Handling-Exceptions.md ├── Initializing-Watch-Values.md ├── Keeping-The-Digest-Going-While-Is-Stays-Dirty.md ├── NaNs.md ├── Scope-Objects.md ├── Short-Circuiting-The-Digest-When-The-Last-Watch-Is-Clean.md ├── Value-Based-Dirty-Checking.md └── Watching-Object-Properties-$watch-$digest.md ├── chapter11 ├── Array-Style-Dependency-Annotation.md ├── Binding-this-in-Injected-Functions.md ├── Dependency-Annotation-from-Function-Arguments.md ├── Dependency-Injection.md ├── Getting-A-Registered-Module.md ├── Initializing-The-Global-Just-Once.md ├── Instantiating-Objects-with-Dependency-Injection.md ├── Integrating-Annotation-with-Invocation.md ├── Providing-Locals-to-Injected-Functions.md ├── Registering-A-Constant.md ├── Registering-A-Module.md ├── Rejecting-Non-String-DI-Tokens.md ├── Requiring-Other-Modules.md ├── Strict-Mode.md ├── The-Injector.md ├── The-angular-Global.md └── The-module-Method.md ├── chapter12 ├── Circular-Dependencies.md ├── Injecting-Dependencies-To-The-$get-Method.md ├── Lazy-Instantiation-of-Dependencies.md ├── Making-Sure-Everything-Is-A-Singleton.md ├── Provider-Constructors.md ├── The Simplest-Possible-Provider-An-Object-with-A-$get-Method.md ├── Two-Injectors-The-Provider-Injector-and-The-Instance-Injector.md └── Unshifting-Constants-in-The-Invoke-Queue.md ├── chapter16 └── Creating-The-$compile-Provider.md ├── chapter2 ├── $apply-Integrating-External-Code-With-The-Digest-Cycle.md ├── $eval-Evaluating-Code-In-The-Context-of-A-Scope.md ├── $evalAsync-Deferred-Execution.md └── Scheduling-$evalAsync-from-Watch-Functions.md ├── chapter3 ├── Attribute-Shadowing.md ├── Destroying-Scope.md ├── Digesting-The-Whole-Tree-from-$apply-$evalAsync-and-$applyAsync.md ├── Isolated-Scopes.md ├── Making-A-Child-Scope.md ├── Recursive-Digestion.md ├── Separated-Watches.md ├── Substituting-The-Parent-Scope.md └── The-Root-Scope.md ├── chapter4 ├── Array-Like-Objects.md ├── Dealing-with-Objects-that-Have-A-length.md ├── Detecting-New-Array.md ├── Detecting-New-Objects.md ├── Detecting-New-Or-Removed-Items-in-Arrays.md ├── Detecting-New-Or-Replaced-Attributes-in-Objects.md ├── Detecting-Non-Collection-Changes.md ├── Detecting-Removed-Attributes-in-Objects.md ├── Detecting-Replaced-Or-Reordered-Items-in-Arrays.md ├── Handing-The-Old-Collection-Value-To-Listeners.md ├── Preventing-Unnecessary-Object-Iteration.md └── Setting-Up-The-Infrastructure.md └── chapter5 ├── Additional-Listener-Arguments.md ├── Broadcasting-Down-The-Scope-Hierarchy.md ├── Broadcasting-Scope-Removal.md ├── Dealing-with-Duplication.md ├── Deregistering-Event-Listeners.md ├── Disabling-Listeners-On-Destroyed-Scopes.md ├── Emitting-Up-The-Scope-Hierarchy.md ├── Event-Objects.md ├── Handling-Exceptions.md ├── Including-The-Current-And-Target-Scopes-in-The-Event-Object.md ├── Preventing-Default-Event-Behavior.md ├── Publish-Subscribe-Messaging.md ├── Registering-Event-Listeners-$on.md ├── Returning-The-Event-Object.md ├── Setup.md ├── Stopping-Event-Propagation.md └── The-basics-of-$emit-and-$broadcast.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 HaoJu Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chapter1/Checking-for-Dirty-Values.md: -------------------------------------------------------------------------------- 1 | ### 脏值检查 2 | 3 | 根据上面的描述, 监听器 (watcher) 的 watch 函数应当返回我们关心并可以改变的数据. 通常该数据存在于 scope 中. 我们将当前 scope 作为参数去调用 watch 函数, 这样可以很方便的在 watch 函数中访问 Scope 上的数据, 如下所示: 4 | 5 | ```js 6 | function(scope) { 7 | return scope.firstName; 8 | } 9 | ``` 10 | 11 | 这是 watch 函数的通常写法: 直接返回 scope 上属性. 12 | 13 | 让我们添加一个测试用例, 用来检查 scope 确实作为 watch 函数的参数: 14 | 15 | ```js 16 | it("calls the watch function with the scope as the argument", function() { 17 | var watchFn = jasmine.createSpy(); 18 | var listenerFn = function() { }; 19 | scope.$watch(watchFn, listenerFn); 20 | 21 | scope.$digest(); 22 | expect(watchFn).toHaveBeenCalledWith(scope); 23 | }); 24 | ``` 25 | 26 | 这次, 我们为 watch 函数创建一个 Spy 用来检查 watch 函数的调用, 最简单通过该测试的方式是, 如下面那样修改 `$degist` 函数: 27 | 28 | ```js 29 | Scope.prototype.$digest = function() { 30 | var self = this; 31 | _.forEach(this.$$watchers, function(watcher) { 32 | watcher.watchFn(self); 33 | watcher.listenerFn(); 34 | }); 35 | }; 36 | ``` 37 | 当然这与我们之后的写法完全不同, `$digest` 函数的任务是调用 watch 函数获取其返回值再与 watch 函数上次的返回值进行比较. 如果值不同, 该监听器就是脏的, 并且它的 listener 函数应该被调用. 38 | 让我们继续添加一个测试用例: 39 | 40 | ```js 41 | it("calls the listener function when the watched value changes", function() { 42 | scope.someValue = 'a'; 43 | scope.counter = 0; 44 | scope.$watch( 45 | function(scope) { return scope.someValue; }, 46 | function(newValue, oldValue, scope) { scope.counter++; } 47 | ); 48 | 49 | expect(scope.counter).toBe(0); 50 | scope.$digest(); 51 | expect(scope.counter).toBe(1); 52 | scope.$digest(); 53 | expect(scope.counter).toBe(1); 54 | scope.someValue = 'b'; 55 | expect(scope.counter).toBe(1); 56 | scope.$digest(); 57 | expect(scope.counter).toBe(2); 58 | }); 59 | ``` 60 | 首先, 设置两个属性在 scope 上, 一个字符串, 另一个数字. 然后添加一个监听器去监控字符串, 并且当字符串改变时, 为数字加1. 我们期望第一次调用 $digest 的时候 counter 加1, 而后的每一次调用 $digest, 如果值改变, 则 counter 加1. 61 | 62 | 注意, 我们也指定了相应的 listener 函数, 和 watch 函数一样, 它也将 scope 作为自己的参数, 另外还有监听器中的新值和旧值, 这样做, 让开发者很容易检查到变化. 63 | 64 | 为了实现上面的功能, $digest 需要记住, 每个 watch 函数上次的返回值. 我们已经为每个监听器创建了对象, 可以将上次返回值存储在各自的监听器对象里. 下面是 '$digest' 的新定义: 65 | 66 | ```js 67 | Scope.prototype.$digest = function() { 68 | var self = this; 69 | var newValue, oldValue; 70 | _.forEach(this.$$watchers, function(watcher) { 71 | newValue = watcher.watchFn(self); 72 | oldValue = watcher.last; 73 | if (newValue !== oldValue) { 74 | watcher.last = newValue; 75 | watcher.listenerFn(newValue, oldValue, self); 76 | } 77 | }); }; 78 | ``` 79 | 对于每一个监听器, 我们比较 watch 函数的返回值和之前存储在 last 属性上的值. 如果两值不同, 我们调用 listener 函数, 并将新值, 旧值和当前的 scope 作为参数传入. 最后, 我们将新值设置给监听器对象的 last 属性, 用于下次比较. 80 | 81 | 现在, 我们已经实现了 AngularJS scope 的本质: 添加监控并并在 digest 循环中运行. 82 | 83 | 我们已经可以看到 AngularJS scope 中一些重要的性能特征: 84 | 85 | - 在 Scope 上添加数据, 并不会对性能产生影响, 如果没有监听器去监控属性, 无论是否在scope上都无所谓. AngularJS 不会遍历 scope 的属性, 它遍历监听器. 86 | - 在每个 `$degist` 中调用 watch 函数, 基于这个原因, 最好关注 watch 函数的数量, 还有每个单独 watch 或表达式的性能. -------------------------------------------------------------------------------- /chapter1/Destroying-A-Watch.md: -------------------------------------------------------------------------------- 1 | ### 销毁监听器 2 | 3 | 当你注册一个监听器的时候, 大部分的时候, 你希望它与 scope 一样一直存在, 不希望显示的移除它, 但是有一些情况, 我们想要从 scope 中删除它. 因此我们需要一个删除操作: 4 | 5 | Angular 的实现方式非常聪明: Angular 中的 $watch 函数 有一个返回值, 它是一个函数, 当被调用的时候, 会销毁注册的函数. 6 | 7 | ```js 8 | it('allows destroying a $watch with a removal function', function() { 9 | scope.aValue = 'abc'; 10 | scope.counter = 0; 11 | var destroyWatch = scope.$watch( 12 | function(scope) { return scope.aValue; }, 13 | function(newValue, oldValue, scope) { 14 | scope.counter++; 15 | } 16 | ); 17 | scope.$digest(); 18 | expect(scope.counter).toBe(1); 19 | scope.aValue = 'def'; 20 | scope.$digest(); 21 | expect(scope.counter).toBe(2); 22 | scope.aValue = 'ghi'; 23 | destroyWatch(); 24 | scope.$digest(); 25 | expect(scope.counter).toBe(2); 26 | }); 27 | ``` 28 | 29 | 为了实现它, 我们需要 $watch 返回一个可以将监听器从 $$watchers 数组中删除的函数. 30 | 31 | ```js 32 | Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 33 | var self = this; 34 | var watcher = { 35 | watchFn: watchFn, 36 | listenerFn: listenerFn, 37 | valueEq: !!valueEq, 38 | last: initWatchVal 39 | }; 40 | self.$$watchers.push(watcher); 41 | this.$$lastDirtyWatch = null; 42 | return function() { 43 | var index = self.$$watchers.indexOf(watcher); 44 | if (index >= 0) { 45 | self.$$watchers.splice(index, 1); 46 | } 47 | }; 48 | }; 49 | ``` 50 | 51 | 在一个健壮实现之前, 还有一些边角情况需要我们处理. 我们所要做的就是注意, 在 digest 中删除 watch 的情况. 52 | 53 | 首先, 监听器在 watch 或 listener 函数中删除自己, 不应当影响其他监听器. 54 | 55 | ```js 56 | it('allows destroying a $watch during digest', function() { 57 | scope.aValue = 'abc'; 58 | var watchCalls = []; 59 | scope.$watch( 60 | function(scope) { 61 | watchCalls.push('first'); 62 | return scope.aValue; 63 | } 64 | ); 65 | var destroyWatch = scope.$watch( 66 | function(scope) { 67 | watchCalls.push('second'); 68 | destroyWatch(); 69 | } 70 | ); 71 | scope.$watch( 72 | function(scope) { 73 | watchCalls.push('third'); 74 | return scope.aValue; 75 | } 76 | ); 77 | scope.$digest(); 78 | expect(watchCalls).toEqual(['first', 'second', 'third', 'first', 'third']); 79 | }); 80 | ``` 81 | 82 | 在这个测试中, 我们有三个 watch, 中间的 watch 删除了他自己, 当第一次被调用的时候, 只剩下第一个和第三个. 83 | 84 | 我们需要做两个调整: 85 | 86 | 第一, 添加一个 watch 的时候, 使用 unshift 代替 push 87 | 88 | ```js 89 | Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 90 | var self = this; 91 | var watcher = { 92 | watchFn: watchFn, 93 | listenerFn: listenerFn || function() { }, 94 | last: initWatchVal, 95 | valueEq: !!valueEq 96 | }; 97 | this.$$watchers.unshift(watcher); 98 | this.$$lastDirtyWatch = null; 99 | return function() { 100 | var index = self.$$watchers.indexOf(watcher); 101 | if (index >= 0) { 102 | self.$$watchers.splice(index, 1); 103 | } 104 | }; 105 | }; 106 | ``` 107 | 108 | 第二, 使用 _.forEachRight 替换 _.forEach 109 | 110 | ```js 111 | Scope.prototype.$$digestOnce = function() { 112 | var self = this; 113 | var newValue, oldValue, dirty; 114 | _.forEachRight(this.$$watchers, function(watcher) { 115 | try { 116 | newValue = watcher.watchFn(self); 117 | oldValue = watcher.last; 118 | if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { 119 | self.$$lastDirtyWatch = watcher; 120 | watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 121 | watcher.listenerFn(newValue, 122 | (oldValue === initWatchVal ? newValue : oldValue), 123 | self); 124 | dirty = true; 125 | } else if (self.$$lastDirtyWatch === watcher) { 126 | return false; 127 | } 128 | } catch (e) { 129 | console.error(e); 130 | } 131 | }); 132 | return dirty; 133 | }; 134 | ``` 135 | 136 | 下一个情况, 在一个 watch 函数中删除另一个监听器, 观察下面的测试用例: 137 | 138 | ```js 139 | it('allows a $watch to destroy another during digest', function() { 140 | scope.aValue = 'abc'; 141 | scope.counter = 0; 142 | scope.$watch( 143 | function(scope) { 144 | return scope.aValue; 145 | }, 146 | function(newValue, oldValue, scope) { 147 | destroyWatch(); 148 | } 149 | ); 150 | var destroyWatch = scope.$watch( 151 | function(scope) { }, 152 | function(newValue, oldValue, scope) { } 153 | ); 154 | scope.$watch( 155 | function(scope) { return scope.aValue; }, 156 | function(newValue, oldValue, scope) { 157 | scope.counter++; 158 | } 159 | ); 160 | scope.$digest(); 161 | expect(scope.counter).toBe(1); 162 | }); 163 | ``` 164 | 165 | 这个测试失败了, 罪魁祸首是我们的缩短回路优化和从右向左遍历引起的: 166 | 167 | - 第一个 watcher 的 watch 被执行, 它是脏的, 被存储在 $$lastDirtyWatch, 它的 listener 被执行, 销毁第二个 watcher, 数组变短, 它成了第二个. 168 | - 第一个 watcher 又被执行了一遍, 这次它是干净的, 于是跳出循环, 第三个 watcher 始终不执行. 169 | 170 | 这种情况, 不要进行优化: 171 | 172 | ```js 173 | Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 174 | var self = this; 175 | var watcher = { 176 | watchFn: watchFn, 177 | listenerFn: listenerFn, 178 | valueEq: !!valueEq, 179 | last: initWatchVal 180 | }; 181 | self.$$watchers.unshift(watcher); 182 | this.$$lastDirtyWatch = null; 183 | return function() { 184 | var index = self.$$watchers.indexOf(watcher); 185 | if (index >= 0) { 186 | self.$$watchers.splice(index, 1); 187 | self.$$lastDirtyWatch = null; 188 | } 189 | }; 190 | }; 191 | ``` 192 | 193 | 最后一种情况, 一个 watch 中删除多个监听器 194 | 195 | ```js 196 | it('allows destroying several $watches during digest', function() { 197 | scope.aValue = 'abc'; 198 | scope.counter = 0; 199 | var destroyWatch1 = scope.$watch( 200 | function(scope) { 201 | destroyWatch1(); 202 | destroyWatch2(); 203 | } 204 | ); 205 | var destroyWatch2 = scope.$watch( 206 | function(scope) { return scope.aValue; }, 207 | function(newValue, oldValue, scope) { 208 | scope.counter++; 209 | } 210 | ); 211 | scope.$digest(); 212 | expect(scope.counter).toBe(0); 213 | }); 214 | ``` 215 | 216 | 第一个 watch 函数, 不仅销毁了自己的监听器, 还销毁了第二个监听器, 但是第二个监听器仍然会被执行, 此时我们不希望出现一个异常. 217 | 218 | ```js 219 | Scope.prototype.$$digestOnce = function() { 220 | var self = this; 221 | var newValue, oldValue, dirty; 222 | _.forEachRight(this.$$watchers, function(watcher) { 223 | try { 224 | if (watcher) { 225 | newValue = watcher.watchFn(self); 226 | oldValue = watcher.last; 227 | if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { 228 | self.$$lastDirtyWatch = watcher; 229 | watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 230 | watcher.listenerFn(newValue, 231 | (oldValue === initWatchVal ? newValue : oldValue), 232 | self); 233 | dirty = true; 234 | } else if (self.$$lastDirtyWatch === watcher) { 235 | return false; 236 | } 237 | } 238 | } catch (e) { 239 | console.error(e); 240 | } 241 | }); 242 | return dirty; 243 | }; 244 | ``` 245 | 现在, 我们终于可以在 digest 中安全的删除监听器了. -------------------------------------------------------------------------------- /chapter1/Getting-Notified-of-Digests.md: -------------------------------------------------------------------------------- 1 | ### 在 Digest 时获得通知 2 | 3 | 在 Digest 时, 如果你想获得通知, 你只需要注册一个 watch, 而不需要 listener 函数, 因为每次 digest, 每个 watch 函数都会被执行. 让我们添加一个测试用例: 4 | 5 | ```js 6 | it('may have watchers that omit the listener function', function() { 7 | var watchFn = jasmine.createSpy().and.returnValue('something'); 8 | scope.$watch(watchFn); 9 | scope.$digest(); 10 | expect(watchFn).toHaveBeenCalled(); 11 | }); 12 | ``` 13 | watch 函数没有必要返回任何值, 但是可以返回, 就像上面测试用例一样. 当 scope digest 时, 我们当前的实现会抛出一个异常, 那是因为我们调用了一个不存在的函数. 为了支持这种用例, 我们需要检查 listener 函数是否省略, 如果省略, 放置一个空函数. 14 | 15 | ```js 16 | Scope.prototype.$watch = function(watchFn, listenerFn) { 17 | var watcher = { 18 | watchFn: watchFn, 19 | listenerFn: listenerFn || function() { }, 20 | last: initWatchVal 21 | }; 22 | this.$$watchers.push(watcher); 23 | }; 24 | ``` 25 | 如果使用这种方式, 请记住, 即使没有 listenerFn, AngularJS 也会查看 watchFn 的返回值. 如果返回一个值, 这个值会被做脏检查, 确保使用这种方式不会引起额外的工作, 如果没有返回值, 被监控的值都会变成 undefined . 26 | -------------------------------------------------------------------------------- /chapter1/Giving-Up-On-An-Unstable-Digest.md: -------------------------------------------------------------------------------- 1 | ### 放弃不稳定的 Digest 2 | 3 | 当前我们的实现, 存在一个很明显的漏洞, 就是如果存在两个监听器 (watcher) 相互监控彼此产生的改变, 也就是说, 数据永远都是脏的, 永远都不稳定. 下面的测试用例展示了这种场景: 4 | 5 | ```js 6 | it('gives up on the watches after 10 iterations', function() { 7 | scope.counterA = 0; 8 | scope.counterB = 0; 9 | scope.$watch( 10 | function(scope) { return scope.counterA; }, 11 | function(newValue, oldValue, scope) { 12 | scope.counterB++; 13 | } 14 | ); 15 | scope.$watch( 16 | function(scope) { return scope.counterB; }, 17 | function(newValue, oldValue, scope) { 18 | scope.counterA++; 19 | } 20 | ); 21 | expect((function() { scope.$digest(); })).toThrow(); 22 | }); 23 | ``` 24 | 25 | 我们期望 scope.$digest 方法抛出一个异常, 但是它永远不会. 实际上, 该测试永远不会结束, 那是因为, 两个counter变量 (counterA 和 counterB) 相互彼此依赖, 在每次遍历的时候, 它们中的一个 $$digestOnce 方法肯定返回 true, 也是就是数据始终是脏的. 26 | 27 | 我们需要做的是保持运行 digest 在一个可接受的次数内, 如果运行超过这个次数, 数据仍然还在改变, 抛出一个异常, 表示它是不稳定的. 28 | 29 | 这个可接受次数被称作 TTL (Time To Live) , 默认是10次, 看起来有些小, 但是, 记住, 这是一个性能敏感区, 因为 digest 经常发生, 并且每个 digest 要运行所有的 watch 函数. 用户也不太可能创建 10 个以上链式监听. 30 | 31 | 让我们继续, 在外层 digest 循环添加一个循环计数器, 如果到达 TTL, 抛出一个异常: 32 | 33 | 34 | ```js 35 | Scope.prototype.$digest = function() { 36 | var ttl = 10; 37 | var dirty; 38 | do { 39 | dirty = this.$$digestOnce(); 40 | if (dirty && !(ttl--)) { 41 | throw '10 digest iterations reached'; 42 | } 43 | } while (dirty); 44 | }; 45 | ``` 46 | 47 | 这个更新过的版本会引起我们的测试用例抛出异常, 这正是我们所期望的. -------------------------------------------------------------------------------- /chapter1/Handling-Exceptions.md: -------------------------------------------------------------------------------- 1 | ### 处理异常 2 | 3 | 我们的脏检查的实现与 Angular 的非常类似了, 但是, 它相当的脆弱, 主要是因为我们没有对异常处理花费太多精力. 4 | 5 | 如果一个异常发生在 watch 函数中, 当前的实现将会停止工作. 而 Angular 的实现却要比它健壮的多, Angular 会在 digest 中捕获和记录抛出的异常, 并恢复之前的操作. 6 | 7 | 在监听器中, 有两个产生异常的地方: 一个是 watch 函数, 一个是 listener 函数, 在下面的测试用例中, 我们期望异常被记录下一个 watch 继续执行. 8 | 9 | ```js 10 | it('catches exceptions in watch functions and continues', function() { 11 | scope.aValue = 'abc'; 12 | scope.counter = 0; 13 | scope.$watch( 14 | function(scope) { throw 'Error'; }, 15 | function(newValue, oldValue, scope) { } 16 | ); 17 | scope.$watch( 18 | function(scope) { return scope.aValue; }, 19 | function(newValue, oldValue, scope) { 20 | scope.counter++; 21 | } 22 | ); 23 | scope.$digest(); 24 | expect(scope.counter).toBe(1); 25 | }); 26 | it('catches exceptions in listener functions and continues', function() { 27 | scope.aValue = 'abc'; 28 | scope.counter = 0; 29 | scope.$watch( 30 | function(scope) { return scope.aValue; }, 31 | function(newValue, oldValue, scope) { 32 | throw 'Error'; 33 | } 34 | ); 35 | scope.$watch( 36 | function(scope) { return scope.aValue; }, 37 | function(newValue, oldValue, scope) { 38 | scope.counter++; 39 | } 40 | ); 41 | scope.$digest(); 42 | expect(scope.counter).toBe(1); 43 | }); 44 | ``` 45 | 46 | 我们定义了两个 watch, 第一个 watch 抛出异常, 我们检查第二个 watch 正常执行. 47 | 48 | 为了使上面的测试通过, 我们必须改变 $$digestOnce 函数, 对每个 watch 的执行进行 try catch 处理: 49 | 50 | ```js 51 | Scope.prototype.$$digestOnce = function() { 52 | var self = this; 53 | var newValue, oldValue, dirty; 54 | _.forEach(this.$$watchers, function(watcher) { 55 | try { 56 | newValue = watcher.watchFn(self); 57 | oldValue = watcher.last; 58 | if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { 59 | self.$$lastDirtyWatch = watcher; 60 | watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 61 | watcher.listenerFn(newValue, 62 | (oldValue === initWatchVal ? newValue : oldValue), 63 | self); 64 | dirty = true; 65 | } else if (self.$$lastDirtyWatch === watcher) { 66 | return false; 67 | } 68 | } catch (e) { 69 | console.error(e); 70 | } 71 | }); 72 | return dirty; 73 | }; 74 | ``` 75 | 现在, 我们的 digest 循环健壮多了, 当遇到异常的时候. 76 | -------------------------------------------------------------------------------- /chapter1/Initializing-Watch-Values.md: -------------------------------------------------------------------------------- 1 | ### 初始化监听的值 2 | 3 | 比较 watch 函数返回的值和之前存储在 last 属性上的值在大部分时间工作都不会有问题, 但是首次执行 watch 函数都做了什么, 那时, 我们并未设置 last 值, last 是 undefined. 当 watch 函数首次返回的值也是 undefined 时, 是不会工作的. 4 | 在这种情况下 listener 函数也应当被调用. 但是当前的实现凡是并未考虑初始化值为 undefined 的情况. 5 | 6 | ```js 7 | it('calls listener when watch value is first undefined', function() { 8 | scope.counter = 0; 9 | scope.$watch( 10 | function(scope) { return scope.someValue; }, 11 | function(newValue, oldValue, scope) { scope.counter++; } 12 | ); 13 | scope.$digest(); 14 | expect(scope.counter).toBe(1); 15 | }); 16 | ``` 17 | 这种情况下 listener 函数也应该被调用, 我们需要保证 last 属性初始化的值必须唯一, 确保它与 watch 函数返回任何值都不同, 包括 undefined. 18 | 19 | 函数很适合这个目的, 因为 JavaScript 函数是引用值, 不会和任何值相同除过他们自己. 让我们在 scope.js 最上面引入一个函数作为值. 20 | 21 | ```js 22 | function initWatchVal() { } 23 | ``` 24 | 25 | 现在, 我们可以用这个函数来初始化 last 属性 26 | 27 | ```js 28 | Scope.prototype.$watch = function(watchFn, listenerFn) { 29 | var watcher = { 30 | watchFn: watchFn, 31 | listenerFn: listenerFn, 32 | last: initWatchVal 33 | }; 34 | this.$$watchers.push(watcher); 35 | }; 36 | ``` 37 | 这种方式, 新的 watch 总会使它们的 listener 函数调用, 无论 watch 函数返回什么. 38 | 39 | 但是, initWatchVal 函数会作为 oldValue 参数传入 listener 函数, 我们不想将 initWatchVal 函数泄露到 scope.js 之外, 因此 我们需要用新值替换旧值. 40 | 41 | ```js 42 | it('calls listener with new value as old value the rst time', function() { 43 | scope.someValue = 123; 44 | var oldValueGiven; 45 | scope.$watch( 46 | function(scope) { return scope.someValue; }, 47 | function(newValue, oldValue, scope) { oldValueGiven = oldValue; } 48 | ); 49 | scope.$digest(); 50 | expect(oldValueGiven).toBe(123); 51 | }); 52 | ``` 53 | 54 | 在 $digset 中, 我们调用 listener, 我们只需要检查旧值是否是 initWatchVal 函数, 并将它替换掉. 55 | 56 | ```js 57 | Scope.prototype.$digest = function() { 58 | var self = this; 59 | var newValue, oldValue; 60 | _.forEach(this.$$watchers, function(watcher) { 61 | newValue = watcher.watchFn(self); 62 | oldValue = watcher.last; 63 | if (newValue !== oldValue) { 64 | watcher.last = newValue; 65 | watcher.listenerFn(newValue, 66 | (oldValue === initWatchVal ? newValue : oldValue), 67 | self); 68 | } 69 | }); 70 | }; 71 | ``` -------------------------------------------------------------------------------- /chapter1/Keeping-The-Digest-Going-While-Is-Stays-Dirty.md: -------------------------------------------------------------------------------- 1 | ### 当数据为 Dirty 时, 持续 Digest 2 | 3 | Digest 核心实现都在这里了, 但是离完成还很远, 举例, 有一种相当典型的场景还不支持: listener 函数本身也可以改变 scope 上的属性, 如果这种情况发生的话, 并且另一个监听器监控这个属性, 即使这个属性改变, 也不会在同一个 digest 中获得属性的变化. 4 | 5 | ```js 6 | it('triggers chained watchers in the same digest', function() { 7 | scope.name = 'Jane'; 8 | scope.$watch( 9 | function(scope) { return scope.nameUpper; }, 10 | function(newValue, oldValue, scope) { 11 | if (newValue) { 12 | scope.initial = newValue.substring(0, 1) + '.'; 13 | } 14 | }); 15 | scope.$watch( 16 | function(scope) { return scope.name; }, 17 | function(newValue, oldValue, scope) { 18 | if (newValue) { 19 | scope.nameUpper = newValue.toUpperCase(); 20 | } 21 | }); 22 | scope.$digest(); 23 | expect(scope.initial).toBe('J.'); 24 | scope.name = 'Bob'; 25 | scope.$digest(); 26 | expect(scope.initial).toBe('B.'); 27 | }); 28 | ``` 29 | 在该 scope 上我们有两个监听器: 一个监控 nameUpper 属性并在对应的 listener 为 initial 属性赋值, 另一个则监控 name 属性并在对应的 listener 为 nameUpper 赋值, 30 | 我们期望 scope 上的 name 属性发生变化时, 在 digest 中 nameUpper 和 initial 属性也被更新. 31 | 32 | 然而, 我们需要修改 digest, 确保它遍历所有的 watch 直到监控的值停止变化. 33 | 34 | 首先, 重命名当前的 $digest 函数为 $$digestOnce, 修改其逻辑, 以确保所有监听器(watcher)执行一次, 并返回一个布尔值, 来确定是否有变化. 35 | 36 | ```js 37 | Scope.prototype.$$digestOnce = function() { 38 | var self = this; 39 | var newValue, oldValue, dirty; 40 | _.forEach(this.$$watchers, function(watcher) { 41 | newValue = watcher.watchFn(self); 42 | oldValue = watcher.last; 43 | if (newValue !== oldValue) { 44 | watcher.last = newValue; 45 | watcher.listenerFn(newValue, 46 | (oldValue === initWatchVal ? newValue : oldValue), 47 | self); 48 | dirty = true; 49 | } 50 | }); 51 | return dirty; 52 | }; 53 | ``` 54 | 然后, 让我们来重新定义 $digest, 如下所示: 55 | 56 | ```js 57 | Scope.prototype.$digest = function() { 58 | var dirty; 59 | do { 60 | dirty = this.$$digestOnce(); 61 | } while (dirty); 62 | }; 63 | ``` 64 | 现在 $digest 至少会运行所有监听器一次, 在第一次结束, 有监控的值发生变化, 标记为dirty, 所有的监听器在运行第二次, 直到没有监控的值发生变化. 65 | 66 | 我们现在可以对 AngularJS 的监听函数有另外一个重要结论:它们可能在单次 digest 里面被执行多次。这也就是为什么人们经常说,监听器应当是幂等的:一个监听器应当没有边界效应,或者边界效应只应当发生有限次。比如说,假设一个监控函数触发了一个 Ajax 请求,无法保证你的应用程序发了多少个请求。 -------------------------------------------------------------------------------- /chapter1/NaNs.md: -------------------------------------------------------------------------------- 1 | ### NaNs 2 | 3 | 在 JavaScript 中, NaN 是不等于它自己的. 这听起来有些奇怪, 这也正是原因所在. 如果我们不在脏检查函数中明确处理 NaN, 那么将 NaN 作为返回值的 watch 函数将永远是脏的. 4 | 5 | 基于值得脏检查已经通过 LoDash isEqual 方式被处理, 对于基于引用的检查也被我们自己搞定, 就差 NaN 的情况了: 6 | 7 | ```js 8 | it('correctly handles NaNs', function() { 9 | scope.number = 0/0; // NaN 10 | scope.counter = 0; 11 | scope.$watch( 12 | function(scope) { return scope.number; }, 13 | function(newValue, oldValue, scope) { 14 | scope.counter++; 15 | } 16 | ); 17 | scope.$digest(); 18 | expect(scope.counter).toBe(1); 19 | scope.$digest(); 20 | expect(scope.counter).toBe(1); 21 | }); 22 | ``` 23 | 24 | 我们监控一个 NaN 的值, 当它改变的时候, 对 counter 加 1. 我们期望在第一次 $digest 时, counter 加 1, 随后的 $digest, counter 保持不变. 然而, 运行该测试, 我们却得到了 TTL reached 的异常. 25 | 这个 scope 永远都不会稳定, 因为 NaN 永远不可能等于 NaN. 26 | 27 | 让我们对 $$areEqual 稍作调整: 28 | 29 | ```js 30 | Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 31 | if (valueEq) { 32 | return _.isEqual(newValue, oldValue); 33 | } else { 34 | return newValue === oldValue || 35 | (typeof newValue === 'number' && typeof oldValue === 'number' && 36 | isNaN(newValue) && isNaN(oldValue)); 37 | } 38 | }; 39 | ``` -------------------------------------------------------------------------------- /chapter1/Scope-Objects.md: -------------------------------------------------------------------------------- 1 | ### Scope 对象 2 | 3 | Scope 对象是通过构造函数创建的, 它就是一个 POJO (简单的 JavaScript 对象). 让我们来写一个测试用例. 4 | 5 | 创建一个 `test/scope_spec.js` 文件, 并添加如下内容: 6 | 7 | ```js 8 | 'use strict'; 9 | var Scope = require('../src/scope'); 10 | 11 | describe('Scope', function() { 12 | 13 | it('can be constructed and used as an object', function() { 14 | var scope = new Scope(); 15 | scope.aProperty = 1; 16 | 17 | expect(scope.aProperty).toBe(1); 18 | }); 19 | }); 20 | ``` 21 | 测试文件最上方启用 ES5 严格模式, 然后通过 requireJS 方式引入 Scope, 使用该 Scope 创建一个 Scope 对象, 为 Scope 对象随意添加一个属性, 并检查它是否存在. 22 | 23 | 该测试用例很容易通过, 我们来创建一个 `src/scope.js`, 并添加如下内容 24 | 25 | ```js 26 | 'use strict'; 27 | function Scope() { 28 | 29 | } 30 | module.exports = Scope; 31 | ``` 32 | 33 | 在本测试用例中, 我们在 Scope 对象上添加一个 aProperty 属性. 属性在 scope 上如何工作是非常明确的, 就是简单的 JavaScript 属性, 并没有任何特别之处, 当你为其赋值时, 既不需要调用特殊的 setter 方法, 也不必对值做任何限制. 34 | 相反的, 在两个非常特殊的函数 `$watch` 和 `$digest` 中会发生一些神奇的事情, 我们来关注他们. -------------------------------------------------------------------------------- /chapter1/Short-Circuiting-The-Digest-When-The-Last-Watch-Is-Clean.md: -------------------------------------------------------------------------------- 1 | ### 当上次 watcher 是干净的, 可以缩短 Digest 回路 2 | 3 | 在当前实现中, 我们保持完整的遍历所有监听器集合一圈, 直到确认所有 watcher 都是干净的(或者到达TTL), 才会停止. 4 | 5 | 如果, 在一个 digest 循环中有一个巨大的数量的 watcher 集合, 尽可能的减少执行它的次数是非常重要的, 这也是为什么要对 digest 循环应用特殊优化的原因. 6 | 7 | 考虑一种情况, 在一个 scope 中, 注册 100 个 watcher, 当我们 digest 时, 第一次遍历这 100 个 watcher 结果都是脏的, 因此, 还要进行第二次遍历, 在第二次循环中, 没有一个 watcher 是脏的, 但是, 在 digest 结束前, 我们仍然执行了 200 次 watcher. 8 | 9 | 那么, 我们可以做些什么来减少 watcher 的执行次数呢, 只需要记录最近一次结果是脏的 watcher, 然后, 第二次循环的时候, 比较当前执行的 watcher 是否是上次记住的 watcher, 如果是, 说明, 剩余的 watcher 上次的结果都是干净的, 没有必要全部循环完, 直接退出循环就好. 10 | 11 | ```js 12 | it('ends the digest when the last watch is clean', function() { 13 | scope.array = _.range(100); 14 | var watchExecutions = 0; 15 | _.times(100, function(i) { 16 | scope.$watch( 17 | function(scope) { 18 | watchExecutions++; 19 | return scope.array[i]; 20 | }, 21 | function(newValue, oldValue, scope) { 22 | } 23 | ); 24 | }); 25 | scope.$digest(); 26 | expect(watchExecutions).toBe(200); 27 | scope.array[0] = 420; 28 | scope.$digest(); 29 | expect(watchExecutions).toBe(301); 30 | }); 31 | ``` 32 | 33 | 首先, 我们将一个具有 100 项的数组添加到 scope 上, 接着, 添加 100 个监听器, 每个监听器监控数组中的一项. 我们也添加了一个本地变量, 用来追踪 watcher 总的执行次数. 34 | 35 | 然后, 运行一次 digest, 初始化所有 watcher, 在这次 digest 中, 每个 watcher 运行了两遍. 36 | 37 | 此时, 我们修改数组的第一个值, 如果这时缩短回路优化起作用的话, 最终的 watcher 执行次数是 301 而不是 400. 38 | 39 | 正如上面提到的, 优化是通过记录最近一次结果是脏的 watcher, 让我们在 scope 构造函数中添加 $$lastDirtyWatch 变量来记录吧. 40 | 41 | ```js 42 | function Scope() { 43 | this.$$watchers = []; 44 | this.$$lastDirtyWatch = null; 45 | } 46 | ``` 47 | 48 | 当 digest 开始时, 我们将变量 $$lastDirtyWatch 设置为 null. 49 | 50 | ```js 51 | Scope.prototype.$digest = function() { 52 | var ttl = 10; 53 | var dirty; 54 | this.$$lastDirtyWatch = null; 55 | do { 56 | dirty = this.$$digestOnce(); 57 | if (dirty && !(ttl--)) { 58 | throw '10 digest iterations reached'; 59 | } 60 | } while (dirty); 61 | }; 62 | ``` 63 | 64 | 在 $$digestOnce 中, 当遇到一个返回结果是 dirty 的 watcher, 我们将它赋给 $$lastDirtyWatch 变量. 65 | 66 | ```js 67 | Scope.prototype.$$digestOnce = function() { 68 | var self = this; 69 | var newValue, oldValue, dirty; 70 | _.forEach(this.$$watchers, function(watcher) { 71 | newValue = watcher.watchFn(self); 72 | oldValue = watcher.last; 73 | if (newValue !== oldValue) { 74 | self.$$lastDirtyWatch = watcher; 75 | watcher.last = newValue; 76 | watcher.listenerFn(newValue, 77 | (oldValue === initWatchVal ? newValue : oldValue), 78 | self); 79 | dirty = true; 80 | } 81 | }); 82 | return dirty; 83 | }; 84 | ``` 85 | 86 | 仍然在 $$digestOnce 中, 当遇到一个返回结果是干净的 watcher 并且是上一次记录的返回结果是 dirty 的 watcher, 跳出 $digest 循环. 87 | 88 | ```js 89 | Scope.prototype.$$digestOnce = function() { 90 | var self = this; 91 | var newValue, oldValue, dirty; 92 | _.forEach(this.$$watchers, function(watcher) { 93 | newValue = watcher.watchFn(self); 94 | oldValue = watcher.last; 95 | if (newValue !== oldValue) { 96 | self.$$lastDirtyWatch = watcher; 97 | watcher.last = newValue; 98 | watcher.listenerFn(newValue, 99 | (oldValue === initWatchVal ? newValue : oldValue), 100 | self); 101 | dirty = true; 102 | } else if (self.$$lastDirtyWatch === watcher) { 103 | return false; // lodash 的 forEach, 如果返回 false, 会跳出循环. 104 | } 105 | }); 106 | return dirty; 107 | }; 108 | ``` 109 | 110 | 现在优化生效了, 但是还有一种情况需要处理, 在 listener 函数中, 添加监听器. 111 | 112 | ```js 113 | it('does not end digest so that new watches are not run', function() { 114 | scope.aValue = 'abc'; 115 | scope.counter = 0; 116 | scope.$watch( 117 | function(scope) { return scope.aValue; }, 118 | function(newValue, oldValue, scope) { 119 | scope.$watch( 120 | function(scope) { return scope.aValue; }, 121 | function(newValue, oldValue, scope) { 122 | scope.counter++; 123 | } 124 | ); 125 | } 126 | ); 127 | scope.$digest(); 128 | expect(scope.counter).toBe(1); 129 | }); 130 | ``` 131 | 第二个 watcher 并未执行, 原因是在第二次 digest 循环中, 我们检测到第一个 watcher 就是上次记录的结果返回是脏的 watcher, 跳出循环了. 让我们来修复这个问题: 132 | 当我们添加一个新的 watcher 时, 重新设置 $$lastDirtyWatch 为 null, 禁用优化. 133 | 134 | ```js 135 | Scope.prototype.$watch = function(watchFn, listenerFn) { 136 | var watcher = { 137 | watchFn: watchFn, 138 | listenerFn: listenerFn || function() { }, 139 | last: initWatchVal 140 | }; 141 | this.$$watchers.push(watcher); 142 | this.$$lastDirtyWatch = null; 143 | }; 144 | ``` 145 | 146 | 现在, digest 循环潜在的已经比之前快了很多, 在一个典型的应用中, 这种优化并不能总是生效如上面的例子, 但是在平均情况下, 它已经做的足够好, AngulaJS 团队已经决定引入它. 147 | 148 | 接下来, 让我们继续关注如何检测数据变化. -------------------------------------------------------------------------------- /chapter1/Value-Based-Dirty-Checking.md: -------------------------------------------------------------------------------- 1 | ### 基于值得脏检查 2 | 3 | 目前, 我们已经使用严格等于 === 比较旧值和新值. 在大多数情况下, 当检测所有基本类型的变化, 对象或数组变化成一个新的时候, 是没有问题的. 4 | 但是, 还有另一种 Angular 可以检测的变化, 那就是检测对象和数组内部的变化. 也就是说, 你不仅能检测引用的变化, 也能检测值得变化. 5 | 6 | 通过对 $watch 方法提供第三个可选的布尔参数启用这种脏检查, 当这个参数是 true 时, 使用基于值的检查, 让我们添加一个测试用例吧: 7 | 8 | ```js 9 | it('compares based on value if enabled', function() { 10 | scope.aValue = [1, 2, 3]; 11 | scope.counter = 0; 12 | scope.$watch( 13 | function(scope) { return scope.aValue; }, 14 | function(newValue, oldValue, scope) { 15 | scope.counter++; 16 | }, 17 | true 18 | ); 19 | scope.$digest(); 20 | expect(scope.counter).toBe(1); 21 | scope.aValue.push(4); 22 | scope.$digest(); 23 | expect(scope.counter).toBe(2); 24 | }); 25 | ``` 26 | 27 | 当 scope.aValue 数组变化时, counter 加 1, 当我们往数组里添加东西时, 我们期望它作为一个变化被通知. 28 | 29 | 让我们来重新定义 $watch 方法: 30 | 31 | ```js 32 | Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 33 | var watcher = { 34 | watchFn: watchFn, 35 | listenerFn: listenerFn || function() { }, 36 | valueEq: !!valueEq, 37 | last: initWatchVal 38 | }; 39 | this.$$watchers.push(watcher); 40 | this.$$lastDirtyWatch = null; 41 | }; 42 | ``` 43 | 44 | 我们所做是为 watcher 添加一个标志, 通过 !! 操作符将其转成布尔值. 45 | 46 | 基于值得脏检查表明如果旧值或新值是对象或者数组的时, 我们必须遍历它们中的一切. 如果在两个值中有任何不同, watcher 就是脏的, 如果存在对象和数组嵌套, 它们将会被递归的进行值比较. 47 | 48 | Angular 拥有自己的等于检查函数, 但是我们将会使用 LoDash 提供的来代替. 让我们重新定义一个新的比较函数. 49 | 50 | ```js 51 | Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 52 | if (valueEq) { 53 | return _.isEqual(newValue, oldValue); 54 | } else { 55 | return newValue === oldValue; 56 | } 57 | }; 58 | ``` 59 | 60 | 注意值得变化, 我们也需要改变旧值得存储方式, 不能只存当前值的引用, 因为任何对该值的改变都会被应用到该值的引用, 我们通过 $$areEqual 比较同一值的两个引用, 结果永远都是相等的. 基于这个原因, 我们需要写一个深 copy 函数, copy 该值, 并存储它. 61 | 62 | 让我们来修改 $digestOnce 方法吧: 63 | 64 | ```js 65 | Scope.prototype.$$digestOnce = function() { 66 | var self = this; 67 | var newValue, oldValue, dirty; 68 | _.forEach(this.$$watchers, function(watcher) { 69 | newValue = watcher.watchFn(self); 70 | oldValue = watcher.last; 71 | if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { 72 | self.$$lastDirtyWatch = watcher; 73 | watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 74 | watcher.listenerFn(newValue, 75 | (oldValue === initWatchVal ? newValue : oldValue), 76 | self); 77 | dirty = true; 78 | } else if (self.$$lastDirtyWatch === watcher) { 79 | return false; 80 | } 81 | }); 82 | return dirty; 83 | }; 84 | ``` 85 | 86 | 基于值的检查明显比基于引用的检查引入了更多的操作, 有时候更多. 遍历嵌套数据结构需要花费时间, 保存一个深 copy 需要消耗更多内存, 这也是为什么 Angular 默认不是基于值的检查. -------------------------------------------------------------------------------- /chapter1/Watching-Object-Properties-$watch-$digest.md: -------------------------------------------------------------------------------- 1 | ### 监控对象的属性: `$watch` 和 `$digest` 2 | 3 | `$watch` 和 `$digest` 就像一枚硬币的两面, 共同组成了 AngularJS 的核心, 脏检查循环 (digest cycle): 对数据变化的响应. 4 | 5 | 你可以通过 $watch 方法在 scope 上添加一个监听器 (watcher), 当 Scope 中发生一些变化的时候, 监听器会收到通知. $watch 方法需要提供两个函数, 才可以创建一个监听器: 6 | 7 | - watch 函数, 指定你感兴趣的数据. 8 | - listener 函数, 当数据发生变化时, 该函数会被调用. 9 | 10 | 硬币的另一面: `$digest` 函数, 它会遍历你添加到 scope 上的所有监听器 (watcher), 并运行相对应的 listener 函数. 11 | 12 | 让我们来详细说明这些基础, 先定义一个测试用例: 使用 `$watch` 方法注册一个监听器 (watcher), 当某人调用 `$digest` 方法时, 监听器的 listener 函数会被执行. 13 | 14 | 为了让事情变得更容易管理, 在 `test/scope_spec.js` 添加一个嵌套的 describe 块. 并且创建一个 beforeEach 函数用来初始化 scope, 这样就不用在每次测试前重复初始化 scope. 15 | 16 | ```js 17 | describe("Scope", function() { 18 | 19 | it("can be constructed and used as an object", function() { 20 | var scope = new Scope(); 21 | scope.aProperty = 1; 22 | expect(scope.aProperty).toBe(1); 23 | }); 24 | 25 | describe("digest", function() { 26 | var scope; 27 | beforeEach(function() { 28 | scope = new Scope(); 29 | }); 30 | it("calls the listener function of a watch on first $digest", function() { 31 | var watchFn = function() { return 'wat'; }; 32 | var listenerFn = jasmine.createSpy(); 33 | scope.$watch(watchFn, listenerFn); 34 | 35 | scope.$digest(); 36 | 37 | expect(listenerFn).toHaveBeenCalled(); 38 | }); 39 | }); 40 | }); 41 | 42 | ``` 43 | 44 | 在本测试中, 调用 `$watch` 方法在 scope 上注册一个监听器 (watcher), 我们并不关心 watch 函数, 因此仅仅让它返回一个不变的值. 再提供一个 Jasmine Spy 做为 listener 函数. 调用 $digest 方法, 然后检查 listener 函数是否被调用. 45 | 46 | 为了使该测试通过, 我们需要做一些事情. 首先, Scope 上需要有一个地方用来存储所有被注册的监听器, 让我们为 Scope 的构造函数添加一个数组吧: 47 | 48 | ```js 49 | function Scope() { 50 | this.$$watchers = []; 51 | } 52 | ``` 53 | 54 | 现在, 我们可以定义 `$watch` 方法了. 该方法需要两个函数作为参数, 并将它们存储到 `$$watchers` 数组中, 我们想让每一个 Scope 对象都拥有该函数, 因此将它添加到 Scope 的原型上: 55 | 56 | ```js 57 | Scope.prototype.$watch = function(watchFn, listenerFn) { 58 | var watcher = { 59 | watchFn: watchFn, 60 | listenerFn: listenerFn 61 | }; 62 | this.$$watchers.push(watcher); 63 | }; 64 | ``` 65 | 最后到了 `$digest` 函数, 我们目前先来定义一个简单版本的 `$digest` 函数吧, 仅仅提供遍历并执行所有注册的监听器 (watcher) 功能: 66 | 67 | ```js 68 | Scope.prototype.$digest = function() { 69 | _.forEach(this.$$watchers, function(watcher) { 70 | watcher.listenerFn(); 71 | }); 72 | }; 73 | ``` 74 | `$degist` 函数使用了 LoDash 库中的 forEach 函数, 因此我们需要在文件上面引入 LoDash 库: 75 | 76 | ```js 77 | 'use strict' 78 | 79 | var _ = require('lodash'); 80 | ``` 81 | 测试通过了, 但是这个版本的 `$degist` 不是很有用. 我们真正想要的是通过 watch 函数指定的值发生了变化时, 各自的 listener 函数会被执行. 这被称为脏检查. (dirty-checking) 82 | -------------------------------------------------------------------------------- /chapter11/Array-Style-Dependency-Annotation.md: -------------------------------------------------------------------------------- 1 | ### 数组风格的依赖标注 2 | 3 | 虽然你可以一直使用函数的 $inject 属性标示注入函数的依赖, 你不可能总想这样做, 因此这种方式有些繁琐. 4 | 一种稍微不繁琐的提供依赖名称的方式是为 injector.invoke 提供一个数组而不是一个函数. 在这个数组中, 首先给出依赖的名称, 数组的最后一项是实际被调用的函数: 5 | 6 | ```js 7 | ['a', 'b', function(one, two) { 8 | return one + two; 9 | }] 10 | ``` 11 | 因为我们现在谈论多种不同方式去标示函数依赖, 所以调用一个方法用来提取任何类型标示的函数依赖. 这样的一个方法实际上是在 injector 中实现和公开的, 它叫做 annotate. 12 | 13 | ```js 14 | describe('annotate', function() { 15 | it('returns the $inject annotation of a function when it has one', function() { 16 | var injector = createInjector([]); 17 | 18 | var fn = function() { }; 19 | 20 | fn.$inject = ['a', 'b']; 21 | 22 | expect(injector.annotate(fn)).toEqual(['a', 'b']); 23 | }); 24 | }); 25 | ``` 26 | 27 | 我们将在 createInjector 中引入一个本地函数, 作为一个 injector 的一个属性公开出去: 28 | 29 | ```js 30 | function createInjector(modulesToLoad) { 31 | var cache = {}; 32 | var loadedModules = {}; 33 | var $provide = { 34 | constant: function(key, value) { 35 | if (key === 'hasOwnProperty') { 36 | throw 'hasOwnProperty is not a valid constant name!'; 37 | } 38 | cache[key] = value; 39 | } 40 | }; 41 | 42 | function annotate(fn) { 43 | return fn.$inject; 44 | } 45 | 46 | function invoke(fn, self, locals) { 47 | var args = _.map(fn.$inject, function(token) { 48 | if (_.isString(token)) { 49 | return locals && locals.hasOwnProperty(token) ? 50 | locals[token] : 51 | cache[token]; 52 | } else { 53 | throw 'Incorrect injection token! Expected a string, got '+token; 54 | } 55 | }); 56 | 57 | return fn.apply(self, args); 58 | } 59 | 60 | _.forEach(modulesToLoad, function loadModule(moduleName) { 61 | if (!loadedModules.hasOwnProperty(moduleName)) { 62 | loadedModules[moduleName] = true; 63 | var module = window.angular.module(moduleName); 64 | _.forEach(module.requires, loadModule); 65 | _.forEach(module._invokeQueue, function(invokeArgs) { 66 | var method = invokeArgs[0]; 67 | var args = invokeArgs[1]; 68 | $provide[method].apply($provide, args); 69 | }); 70 | } 71 | }); 72 | 73 | return { 74 | has: function(key) { 75 | return cache.hasOwnProperty(key); 76 | }, 77 | get: function(key) { 78 | return cache[key]; 79 | }, 80 | annotate: annotate, 81 | invoke: invoke 82 | }; 83 | } 84 | ``` 85 | 86 | 基于数组风格依赖注入, 当传入一个数组时, annotate 从数组中提取依赖名称: 87 | 88 | ```js 89 | it('returns the array-style annotations of a function', function() { 90 | var injector = createInjector([]); 91 | var fn = ['a', 'b', function() { }]; 92 | 93 | expect(injector.annotate(fn)).toEqual(['a', 'b']); 94 | }); 95 | ``` 96 | 97 | 如果 fn 是一个数组, annotate 方法应该返回除了最后一项的数组: 98 | 99 | ```js 100 | function annotate(fn) { 101 | if (_.isArray(fn)) { 102 | return fn.slice(0, fn.length - 1); 103 | } else { 104 | return fn.$inject; 105 | } 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /chapter11/Binding-this-in-Injected-Functions.md: -------------------------------------------------------------------------------- 1 | ### 为注入的函数绑定 this 对象 2 | 3 | 有时, 我们想要注入的函数实际上是对象上的方法. 在这类方法中, this 的值通常是需要注意的. 当你直接调用时, JavaScript 语言小心处理绑定的 this, 但是通过 injector.invoke 调用, this 并没有自动绑定 4 | 5 | 然而, 我们可以给 injector.invoke 方法一个 this 值作为第二个可选参数, 当调用方法时, 这个 this 值会被绑定. 6 | 7 | ```js 8 | it('invokes a function with the given this context', function() { 9 | var module = window.angular.module('myModule', []); 10 | 11 | module.constant('a', 1); 12 | 13 | var injector = createInjector(['myModule']); 14 | 15 | var obj = { 16 | two: 2, 17 | fn: function(one) { return one + this.two; } 18 | }; 19 | 20 | obj.fn.$inject = ['a']; 21 | 22 | expect(injector.invoke(obj.fn, obj)).toBe(3); 23 | }); 24 | ``` 25 | 26 | 因为已经使用 Function.apply 调用函数了, 所以只需要将 this 值传给 Function.apply: 27 | 28 | ```js 29 | function invoke(fn, self) { 30 | var args = _.map(fn.$inject, function(token) { 31 | if (_.isString(token)) { 32 | return cache[token]; 33 | } else { 34 | throw 'Incorrect injection token! Expected a string, got '+token; 35 | } 36 | }); 37 | 38 | return fn.apply(self, args); 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /chapter11/Dependency-Annotation-from-Function-Arguments.md: -------------------------------------------------------------------------------- 1 | ### 从函数参数中获取依赖标示 2 | 3 | 第三种也是最后一种, 或许是最最感兴趣的方式去定义一个函数的依赖, 实际上并未定义任何依赖. 当传给 injector 的函数没有带 $inject 属性并且也没有被数组包裹时, injector 将尝试从函数本身抽取依赖名称. 4 | 5 | 首先, 让我们来处理一个简单情况: 没有参数的函数. 6 | 7 | ```js 8 | it('returns an empty array for a non-annotated 0-arg function', function() { 9 | var injector = createInjector([]); 10 | var fn = function() { }; 11 | expect(injector.annotate(fn)).toEqual([]); 12 | }); 13 | ``` 14 | 15 | 对于 annotate 方法, 当函数没有被标示依赖时, 我们返回一个空数组. 上面的测试就会通过: 16 | 17 | ```js 18 | function annotate(fn) { 19 | if (_.isArray(fn)) { 20 | return fn.slice(0, fn.length - 1); 21 | } else if (fn.$inject) { 22 | return fn.$inject; 23 | } else { 24 | return []; 25 | } 26 | } 27 | ``` 28 | 29 | 如果函数有参数, 我们需要一种方式去抽取函数的依赖名称, 为了通过下面的测试: 30 | 31 | ```js 32 | it('returns annotations parsed from function args when not annotated', function() { 33 | var injector = createInjector([]); 34 | var fn = function(a, b) { }; 35 | expect(injector.annotate(fn)).toEqual(['a', 'b']); 36 | }); 37 | ``` 38 | 39 | 一个小技巧是读取函数的源码然后用正在表达式提取参数声明. 在 JavaScript 中你可以通过调用函数的 toString 方法得到函数的源码: 40 | 41 | ```js 42 | (function(a, b) { }).toString() // => "function (a, b) { }" 43 | ``` 44 | 45 | 因为函数的源码包含参数列表, 所以我们可以使用下面的正则表达式抓取参数. 46 | 47 | ```js 48 | var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; 49 | ``` 50 | 51 | 当我们在 annotate 中使用正则表达式匹配成功时, 我们已经从捕获组中的第二项中得到了参数列表. 然后使用 `,` 去 split, 得到一个参数名称的数组. 对于没有参数的函数, 我们添加一个特殊条件去处理. 52 | 53 | ```js 54 | function annotate(fn) { 55 | if (_.isArray(fn)) { 56 | return fn.slice(0, fn.length - 1); 57 | } else if (fn.$inject) { 58 | return fn.$inject; 59 | } else if (!fn.length) { 60 | return []; 61 | } else { 62 | var argDeclaration = fn.toString().match(FN_ARGS); 63 | return argDeclaration[1].split(','); 64 | } 65 | } 66 | ``` 67 | 68 | 使用这个实现, 你会注意到我们测试仍然是失败的. 那是因为有一些额外的空白在第二个依赖名称里: `' b'`. 我们的正则删除参数列表开头的空白, 但是没有删除参数名称之间的空白. 为了删除这些空白, 我们需要在返回之前遍历这些参数名称. 69 | 70 | 下面的这则表达式会匹配一个字符串前后的空白, 并捕获非空白部分. 71 | 72 | ```js 73 | var FN_ARG = /^\s*(\S+)\s*$/; 74 | ``` 75 | 76 | 通过将参数名映射到这个正则表达式的第二个匹配结果,我们可以得到没有空白的参数名: 77 | 78 | ```js 79 | function annotate(fn) { 80 | if (_.isArray(fn)) { 81 | return fn.slice(0, fn.length - 1); 82 | } else if (fn.$inject) { 83 | return fn.$inject; 84 | } else if (!fn.length) { 85 | return []; 86 | } else { 87 | var argDeclaration = fn.toString().match(FN_ARGS); 88 | return _.map(argDeclaration[1].split(','), function(argName) { 89 | return argName.match(FN_ARG)[1]; 90 | }); 91 | } 92 | } 93 | ``` 94 | 95 | 简单的情况都是工作的, 但是如果在函数的声明中注释掉一些参数, 会发生什么呢? 96 | 97 | ```js 98 | it('strips comments from argument lists when parsing', function() { 99 | var injector = createInjector([]); 100 | var fn = function(a, /*b,*/ c) { }; 101 | expect(injector.annotate(fn)).toEqual(['a', 'c']); 102 | }); 103 | ``` 104 | 105 | 在抽取函数参数之前, 我们需要预先处理掉函数源码中的注释. 正则表达式如下: 106 | 107 | ```js 108 | var STRIP_COMMENTS = /\/\*.*\*\//; 109 | ``` 110 | 111 | 这个正则表达式匹配字符 `/*`, 然后是一连串的任意字符, 接着是字符 `*/`. 通过将正则表达式的匹配结果替换为空字符串,我们可以删除注释: 112 | 113 | ```js 114 | function annotate(fn) { 115 | if (_.isArray(fn)) { 116 | return fn.slice(0, fn.length - 1); 117 | } else if (fn.$inject) { 118 | return fn.$inject; 119 | } else if (!fn.length) { 120 | return []; 121 | } else { 122 | var source = fn.toString().replace(STRIP_COMMENTS, ''); 123 | var argDeclaration = source.match(FN_ARGS); 124 | return _.map(argDeclaration[1].split(','), function(argName) { 125 | return argName.match(FN_ARG)[1]; 126 | }); 127 | } 128 | } 129 | ``` 130 | 131 | 当参数列表中有多个注释掉的部分时,上面的正则表达式就不对了: 132 | 133 | ```js 134 | it('strips several comments from argument lists when parsing', function() { 135 | var injector = createInjector([]); 136 | var fn = function(a, /*b,*/ c/*, d*/) { }; 137 | expect(injector.annotate(fn)).toEqual(['a', 'c']); 138 | }); 139 | ``` 140 | 141 | 该正则表达式匹配第一个 `/*` 和最后一个 `*/` 之间的一切, 因此它们之间的非注释内容也丢失了. 我们需要将开始和结束注释之间的量词转换为懒惰的,这样它会尽可能减少匹配, 我们也需要为正则表达式添加 g 标志, 确保匹配字符串中的多个注释: 142 | 143 | ```js 144 | var STRIP_COMMENTS = /\/\*.*?\*\//g; 145 | ``` 146 | 147 | 然后还有另一种类型的注释,参数列表分布在多行可能包括 `//` 的样式的注释, 注释掉一行的剩余部分: 148 | 149 | ```js 150 | it('strips // comments from argument lists when parsing', function() { 151 | var injector = createInjector([]); 152 | var fn = function(a, //b, 153 | c) { }; 154 | expect(injector.annotate(fn)).toEqual(['a', 'c']); 155 | }); 156 | ``` 157 | 158 | 要删掉这些注释, 我们的 STRIP_COMMENTS 正则表达式需要匹配两类不同类型的输入: 我们早前定义的输入 和以 `//` 开头的直到行尾的注释. 我们也要为正则表达式添加 m 标志去匹配多行字符串. 159 | 160 | ```js 161 | var STRIP_COMMENTS = /(\/\/.*$)|(\/\*.*?\*\/)/mg; 162 | ``` 163 | 164 | 这个正则表达式可以处理参数列表中所有类型注释! 165 | 166 | 最后的特性我们需要删除参数名称两边的下划线. angular 允许传入一个两边带下划线的参数名称, 它会忽略两边的下划线. 这样就可以注入一个与本地变量名相同的依赖: 167 | 168 | ```js 169 | var aVariable; 170 | injector.invoke(function(_aVariable_) { 171 | aVariable = _aVariable_; 172 | }); 173 | ``` 174 | 175 | 所以, 如果一个参数两边被下划线包围, 两边的下划线应该从依赖名称中被删除. 如果参数名称只有一边有下划线, 或者中间有, 下划线不会被删除. 176 | 177 | ```js 178 | it('strips surrounding underscores from argument names when parsing', function() { 179 | var injector = createInjector([]); 180 | var fn = function(a, _b_, c_, _d, an_argument) { }; 181 | expect(injector.annotate(fn)).toEqual(['a', 'b', 'c_', '_d', 'an_argument']); 182 | }); 183 | ``` 184 | 185 | 删除下划线也是被 FN_ARG 正则表达式处理的, 之前只用它删除空白. 186 | 187 | ```js 188 | var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; 189 | ``` 190 | 191 | 现在, 我们已经添加一个新的捕获组在正则表达式中, 实际的参数名称已经从第二个匹配结果变成第三个匹配结果: 192 | 193 | ```js 194 | function annotate(fn) { 195 | if (_.isArray(fn)) { 196 | return fn.slice(0, fn.length - 1); 197 | } else if (fn.$inject) { 198 | return fn.$inject; 199 | } else if (!fn.length) { 200 | return []; 201 | } else { 202 | var source = fn.toString().replace(STRIP_COMMENTS, ''); 203 | var argDeclaration = source.match(FN_ARGS); 204 | return _.map(argDeclaration[1].split(','), function(argName) { 205 | return argName.match(FN_ARG)[2]; 206 | }); 207 | } 208 | } 209 | ``` 210 | -------------------------------------------------------------------------------- /chapter11/Dependency-Injection.md: -------------------------------------------------------------------------------- 1 | ### 依赖注入 2 | 3 | 现在, 我们有一个部分有用的应用组件 injector, 在其中, 我们可以加载东西, 也可以查找东西. 但是 injector 真正的目的实际上是去做依赖注入. 4 | 所谓依赖注入就是调用函数和构造器对象时, 自动查找它们需要的依赖. 本章剩余的部分, 我们将关注 injector 的依赖注入特性. 5 | 6 | 这是一个基本的想法: 我们给 injector 一个函数, 要求它去调用这个函数. 也期望 inject 找出函数需要什么参数并将这些参数提供给函数. 7 | 那么, injector 如何找出给定的函数需要什么参数呢? 最简单的方法是显示地提供参数信息, 使用一个叫做 $inject 属性. $inject 属性可以保存函数依赖名称的数组. 8 | 9 | injector 会查找这些依赖, 并在调用函数时使用它们: 10 | 11 | ```js 12 | it('invokes an annotated function with dependency injection', function() { 13 | var module = window.angular.module('myModule', []); 14 | module.constant('a', 1); 15 | module.constant('b', 2); 16 | 17 | var injector = createInjector(['myModule']); 18 | 19 | var fn = function(one, two) { return one + two; }; 20 | fn.$inject = ['a', 'b']; 21 | 22 | expect(injector.invoke(fn)).toBe(3); 23 | }); 24 | ``` 25 | 26 | 通过简单从 injector cache 中查找 $inject 数组中的每一项去实现. injector cache 中存储这些依赖名称对应的值: 27 | 28 | ```js 29 | function createInjector(modulesToLoad) { 30 | var cache = {}; 31 | var loadedModules = {}; 32 | 33 | var $provide = { 34 | constant: function(key, value) { 35 | if (key === 'hasOwnProperty') { 36 | throw 'hasOwnProperty is not a valid constant name!'; 37 | } 38 | 39 | cache[key] = value; 40 | } 41 | }; 42 | 43 | function invoke(fn) { 44 | var args = _.map(fn.$inject, function(token) { 45 | return cache[token]; 46 | }); 47 | return fn.apply(null, args); 48 | } 49 | 50 | _.forEach(modulesToLoad, function loadModule(moduleName) { 51 | if (!loadedModules.hasOwnProperty(moduleName)) { 52 | loadedModules[moduleName] = true; 53 | var module = window.angular.module(moduleName); 54 | _.forEach(module.requires, loadModule); 55 | _.forEach(module._invokeQueue, function(invokeArgs) { 56 | var method = invokeArgs[0]; 57 | var args = invokeArgs[1]; 58 | $provide[method].apply($provide, args); 59 | }); 60 | } 61 | }); 62 | 63 | return { 64 | has: function(key) { 65 | return cache.hasOwnProperty(key); 66 | }, 67 | get: function(key) { 68 | return cache[key]; 69 | }, 70 | invoke: invoke 71 | }; 72 | } 73 | ``` 74 | 75 | 这是你在使用 angular 时最低级别的依赖注明方式. 76 | -------------------------------------------------------------------------------- /chapter11/Getting-A-Registered-Module.md: -------------------------------------------------------------------------------- 1 | ### 获取一个注册的模块 2 | 3 | angular.module 提供的另一个行为就是回去一个早前已经注册的 module 对象. 这个你可以通过省略第二参数去做. 4 | 5 | ```js 6 | it('allows getting a module', function() { 7 | var myModule = window.angular.module('myModule', []); 8 | var gotModule = window.angular.module('myModule'); 9 | expect(gotModule).toBeDefned(); 10 | expect(gotModule).toBe(myModule); 11 | }); 12 | ``` 13 | 14 | 我们引入另一个私有函数去获取 module, 叫 getModule. 我们也需要一个存放已经注册的 module 对象的地方. 我们在 ensure 函数中添加一个私有对象. 15 | 将私有变量作为参数传递给 createModule 和 getModule 方法: 16 | 17 | ```js 18 | ensure(angular, 'module', function() { 19 | var modules = {}; 20 | return function(name, requires) { 21 | if (requires) { 22 | return createModule(name, requires, modules); 23 | } else { 24 | return getModule(name, modules); 25 | } 26 | }; 27 | }); 28 | ``` 29 | 30 | 在 createModule 中我们必须存储新创建的 module 对象: 31 | 32 | ```js 33 | var createModule = function(name, requires, modules) { 34 | var moduleInstance = { 35 | name: name, 36 | requires: requires 37 | }; 38 | modules[name] = moduleInstance; 39 | return moduleInstance; 40 | }; 41 | ``` 42 | 43 | 在 getModule 中, 我们查找 module 对象: 44 | 45 | ```js 46 | var getModule = function(name, modules) { 47 | return modules[name]; 48 | }; 49 | ``` 50 | 51 | 基本上我们将所有的注册过的 module 保留在本地 modules 变量上. 这就是为什么只定义一次 angular 和 angular.module 是如此重要. 否则本地变量会被清除. 52 | 53 | 现在, 当你尝试获得一个不存在的 module, 你会获得一个 undefined 作为一个返回值. 应该用一个异常代替 undefined. 54 | 55 | 56 | ```js 57 | it('throws when trying to get a nonexistent module', function() { 58 | expect(function() { 59 | window.angular.module('myModule'); 60 | }).toThrow(); 61 | }); 62 | ``` 63 | 64 | 在 getModule 中我们应该尝试返回 module 对象之前, 检查是否存在: 65 | 66 | ```js 67 | var getModule = function(name, modules) { 68 | if (modules.hasOwnProperty(name)) { 69 | return modules[name]; 70 | } else { 71 | throw 'Module '+name+' is not available!'; 72 | } 73 | }; 74 | ``` 75 | 76 | 最终, 我们使用 hasOwnProperty 方法去检查一个 module 是否存在, 我们必须小心不要在 module cache 上覆盖该方法. 所以不允许注册一个叫做 hasOwnProperty module. 77 | 78 | ```js 79 | it('does not allow a module to be called hasOwnProperty', function() { 80 | expect(function() { 81 | window.angular.module('hasOwnProperty', []); 82 | }).toThrow(); 83 | }); 84 | ``` 85 | 86 | 这个检查在 createModule 中: 87 | 88 | ```js 89 | var createModule = function(name, requires, modules) { 90 | if (name === 'hasOwnProperty') { 91 | throw 'hasOwnProperty is not a valid module name'; 92 | } 93 | var moduleInstance = { 94 | name: name, 95 | requires: requires 96 | }; 97 | modules[name] = moduleInstance; 98 | return moduleInstance; 99 | }; 100 | ``` 101 | -------------------------------------------------------------------------------- /chapter11/Initializing-The-Global-Just-Once.md: -------------------------------------------------------------------------------- 1 | ### 初始化 angular 全局对象, 只能初始化一次 2 | 3 | 因为 angular 全局对象提供存储注册模块的功能, 它本质上是一个全局状态持有者. 这意味着我们需要一些措施去管理这些状态. 首先, 我们想为每一个测试用例提供一个干净的开始, 所以在每个测试开头我们需要清除任何已经存在的 angular 全局对象. 4 | 5 | ```js 6 | beforeEach(function() { 7 | delete window.angular; 8 | }); 9 | ``` 10 | 11 | 同样的, 在 setupModuleLoader 中, 我们必须要小心, 不要覆盖了已经存在的 angular 全局对象. 即使 setupModuleLoader 被调用多次. 当你调用相同的 window 对象作为参数调用 setupModuleLoader 两次, 调用参数的 angular 全局对象应该指向完全相同的对象. 12 | 13 | ```js 14 | function setupModuleLoader(window) { 15 | var angular = (window.angular = window.angular || {}); 16 | } 17 | ``` 18 | 19 | 我们很快还会使用这种 "load once" 模式, 所以, 我们将它抽象成一个叫做 ensure 的通用函数. 该函数需要三个参数: 一个对象, 一个属性名, 一个工厂方法. 该函数使用工厂方法生成一个属性, 仅当该属性不存在. 20 | 21 | ```js 22 | function setupModuleLoader(window) { 23 | var ensure = function(obj, name, factory) { 24 | return obj[name] || (obj[name] = factory()); 25 | }; 26 | 27 | var angular = ensure(window, 'angular', Object); 28 | } 29 | ``` 30 | 31 | 在这种情况下, 我们调用 Object() 方法赋给 window.angular 一个空对象, 实际上这和调用 new Object() 一样. 32 | -------------------------------------------------------------------------------- /chapter11/Instantiating-Objects-with-Dependency-Injection.md: -------------------------------------------------------------------------------- 1 | ### Instantiating Objects with Dependency Injection 2 | 3 | 我们决定在本章为 injector 添加一种能力: 不仅可以注入普通的函数也可以注入构造函数: 4 | 5 | 当你拥有一个构造函数并想将它实例化成对象, 同时也要注入依赖. 你可以使用 injector.instantiate, 它可以处理带有显示依赖标注的构造函数: 6 | 7 | ```js 8 | it('instantiates an annotated constructor function', function() { 9 | var module = window.angular.module('myModule', []); 10 | module.constant('a', 1); 11 | module.constant('b', 2); 12 | var injector = createInjector(['myModule']); 13 | function Type(one, two) { 14 | this.result = one + two; 15 | } 16 | Type.$inject = ['a', 'b']; 17 | var instance = injector.instantiate(Type); 18 | expect(instance.result).toBe(3); 19 | }); 20 | ``` 21 | 22 | 当然也可以处理数组包裹的风格的标注: 23 | 24 | ```js 25 | it('instantiates an array-annotated constructor function', function() { 26 | var module = window.angular.module('myModule', []); 27 | module.constant('a', 1); 28 | module.constant('b', 2); 29 | var injector = createInjector(['myModule']); 30 | function Type(one, two) { 31 | this.result = one + two; 32 | } 33 | var instance = injector.instantiate(['a', 'b', Type]); 34 | expect(instance.result).toBe(3); 35 | }); 36 | ``` 37 | 38 | 最后一种, 从构造函数源码中抽取依赖名称: 39 | 40 | ```js 41 | it('instantiates a non-annotated constructor function', function() { 42 | var module = window.angular.module('myModule', []); 43 | module.constant('a', 1); 44 | module.constant('b', 2); 45 | var injector = createInjector(['myModule']); 46 | function Type(a, b) { 47 | this.result = a + b; 48 | } 49 | var instance = injector.instantiate(Type); 50 | expect(instance.result).toBe(3); 51 | }); 52 | ``` 53 | 54 | 让我们在 injector 中引入 instantiate 方法. 它指向一个本地方法, 我们暂时引入到这: 55 | 56 | ```js 57 | return { 58 | has: function(key) { 59 | return cache.hasOwnProperty(key); 60 | }, 61 | get: function(key) { 62 | return cache[key]; 63 | }, 64 | annotate: annotate, 65 | invoke: invoke, 66 | instantiate: instantiate 67 | }; 68 | ``` 69 | 70 | 一个非常简单的 instantiate 的实现仅仅是创建一个新对象, 将新对象作为构造函数的上下文, 用 invoke 函数执行构造函数, 然后返回新对象: 71 | 72 | ```js 73 | function instantiate(Type) { 74 | var instance = {}; 75 | invoke(Type, instance); 76 | return instance; 77 | } 78 | ``` 79 | 80 | 这样确实可以是我们现有的测试通过. 但是使用构造函数有一个非常重要的行为: 当你使用 new 构造对象时, 你也基于构造函数的原型链设置了对象的原型链. 在 injector.instantiate 中, 我们需要遵循这种行为. 81 | 82 | 举例, 如果我们实例化一个在 prototype 上增加行为的构造函数, 实例化的对象也可以通过继承使用增加的行为. 83 | 84 | ```js 85 | it('uses the prototype of the constructor when instantiating', function() { 86 | function BaseType() { } 87 | BaseType.prototype.getValue = _.constant(42); 88 | function Type() { this.v = this.getValue(); } 89 | Type.prototype = BaseType.prototype; 90 | var module = window.angular.module('myModule', []); 91 | var injector = createInjector(['myModule']); 92 | var instance = injector.instantiate(Type); 93 | expect(instance.v).toBe(42); 94 | }); 95 | ``` 96 | 97 | 设置原型链, 我们可以使用 ES5 的 Object.create 方法代替一个简单的字面量构建一个对象. 我们还要记得去掉函数外的数组, 因为有可能使用数组风格的依赖标注. 98 | 99 | ```js 100 | function instantiate(Type) { 101 | var UnwrappedType = _.isArray(Type) ? _.last(Type) : Type; 102 | var instance = Object.create(UnwrappedType.prototype); 103 | invoke(Type, instance); 104 | return instance; 105 | } 106 | ``` 107 | 108 | 最终, 就像 injector.invoke 支持本地参数一样, injector. instantiate 也要支持: 109 | 110 | ```js 111 | it('supports locals when instantiating', function() { 112 | var module = window.angular.module('myModule', []); 113 | module.constant('a', 1); 114 | module.constant('b', 2); 115 | var injector = createInjector(['myModule']); 116 | function Type(a, b) { 117 | this.result = a + b; 118 | } 119 | var instance = injector.instantiate(Type, {b: 3}); 120 | expect(instance.result).toBe(4); 121 | }); 122 | ``` 123 | 124 | 我们只需要把 local 参数作为的第三个参数传给 invoke. 125 | 126 | ```js 127 | function instantiate(Type, locals) { 128 | var UnwrappedType = _.isArray(Type) ? _.last(Type) : Type; 129 | var instance = Object.create(UnwrappedType.prototype); 130 | invoke(Type, instance, locals); 131 | return instance; 132 | } 133 | ``` 134 | -------------------------------------------------------------------------------- /chapter11/Integrating-Annotation-with-Invocation.md: -------------------------------------------------------------------------------- 1 | ### 将 annotate 集成到 invoke 2 | 3 | 现在, 我们可以使用三种 Angular 支持的不同的方法去获取依赖的名称, 三种方法分别是 $inject, 数组包裹和从函数源码中抽取. 4 | 我们仍然需要做的是将依赖名称的查找集成到 injector.invoke 方法中. 你应该能够给它一个数组标注的函数, 并期望它做正确的事情: 5 | 6 | ```js 7 | it('invokes an array-annotated function with dependency injection', function() { 8 | var module = window.angular.module('myModule', []); 9 | module.constant('a', 1); 10 | module.constant('b', 2); 11 | var injector = createInjector(['myModule']); 12 | var fn = ['a', 'b', function(one, two) { return one + two; }]; 13 | expect(injector.invoke(fn)).toBe(3); 14 | }); 15 | ``` 16 | 17 | 用完全相同的方式, 你应该能够给它一个未标注的函数, 并期望它从源码中解析依赖标注: 18 | 19 | ```js 20 | it('invokes a non-annotated function with dependency injection', function() { 21 | var module = window.angular.module('myModule', []); 22 | module.constant('a', 1); 23 | module.constant('b', 2); 24 | var injector = createInjector(['myModule']); 25 | var fn = function(a, b) { return a + b; }; 26 | expect(injector.invoke(fn)).toBe(3); 27 | }); 28 | ``` 29 | 30 | 在 invoke 方法中, 我们需要做两件事: 第一件, 需要使用 annotate 方法替代直接访问 $inject 查找依赖名称. 第二件, 我们需要检查, 如果这个函数被一个数组包裹着, 在调用它之前, 需要去掉包裹的数组: 31 | 32 | ```js 33 | function invoke(fn, self, locals) { 34 | var args = _.map(annotate(fn), function(token) { 35 | if (_.isString(token)) { 36 | return locals && locals.hasOwnProperty(token) ? locals[token] : cache[token]; 37 | } else { 38 | throw 'Incorrect injection token! Expected a string, got '+token; 39 | } 40 | }); 41 | 42 | if (_.isArray(fn)) { 43 | fn = _.last(fn); 44 | } 45 | 46 | return fn.apply(self, args); 47 | } 48 | ``` 49 | 50 | 现在, 我们提供这三种类型的函数调用了. 51 | -------------------------------------------------------------------------------- /chapter11/Providing-Locals-to-Injected-Functions.md: -------------------------------------------------------------------------------- 1 | ### 为注入的函数提供本地值 2 | 3 | 大部分情况, 你只想让 injector 为函数提供所有参数, 但是有种情况, 你想在函数调用时, 显示地提供一些参数. 这可能是因为你想覆盖一些参数, 或者使用一些没有被 injector 注册的参数. 4 | 5 | 基于这个目的, injector.invoke 需要第三个可选参数, 这个可选参数是一个映射关系(依赖名称到值)对象. 如果提供该参数, 优先从这个参数对象中进行依赖查找, 其次才是 injector 自身. 6 | 7 | ```js 8 | it('overrides dependencies with locals when invoking', function() { 9 | var module = window.angular.module('myModule', []); 10 | module.constant('a', 1); 11 | module.constant('b', 2); 12 | 13 | var injector = createInjector(['myModule']); 14 | 15 | var fn = function(one, two) { return one + two; }; 16 | 17 | fn.$inject = ['a', 'b']; 18 | 19 | expect(injector.invoke(fn, undefned, {b: 3})).toBe(4); 20 | }); 21 | ``` 22 | 23 | 在依赖映射函数中, 我们首先查找本地依赖, 如果没有找到, 在查找 cache 中的: 24 | 25 | ```js 26 | function invoke(fn, self, locals) { 27 | var args = _.map(fn.$inject, function(token) { 28 | if (_.isString(token)) { 29 | return locals && locals.hasOwnProperty(token) ? locals[token] : cache[token]; 30 | } else { 31 | throw 'Incorrect injection token! Expected a string, got '+token; 32 | } 33 | }); 34 | 35 | return fn.apply(self, args); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /chapter11/Registering-A-Constant.md: -------------------------------------------------------------------------------- 1 | ### 注册一个 Constant 2 | 3 | 我们将要实现 Angular 应用组件中的第一种类型 constant. 你可以使用 constant 为 Angular 模块注册一个简单的值, 例如, 数字, 字符串, 一个对象, 或一个函数. 4 | 5 | 在我们注册一个 constant 并且创建一个 injector 之后, 我们可以使用 injector 的 has 方法检查 constant 是否存在: 6 | 7 | ```js 8 | it('has a constant that has been registered to a module', function() { 9 | var module = window.angular.module('myModule', []); 10 | module.constant('aConstant', 42); 11 | var injector = createInjector(['myModule']); 12 | expect(injector.has('aConstant')).toBe(true); 13 | }); 14 | ``` 15 | 16 | 在这里, 我们第一次看到定义一个 module 并为其创建一个 injector 的完整顺序. 关于创建 injector 的一个非常有意思的现象, 我们并没有传递给 createInjector 函数一个模块对象的引用, 而是一个模块名称, 并且期望可以在 angular.module 中找到它们: 17 | 18 | 作为一个检查, 让我们确保组件没有被注册时, has 方法返回 false: 19 | 20 | ```js 21 | it('does not have a non-registered constant', function() { 22 | var module = window.angular.module('myModule', []); 23 | var injector = createInjector(['myModule']); 24 | expect(injector.has('aConstant')).toBe(false); 25 | }); 26 | ``` 27 | 28 | 因此, 一个注册在模块中的 constant 如何在 injector 中变的可用? 首先, 我们需要在 module 对象中添加 constant 的注册方法: 29 | 30 | ```js 31 | var createModule = function(name, requires, modules) { 32 | if (name === 'hasOwnProperty') { 33 | throw 'hasOwnProperty is not a valid module name'; 34 | } 35 | 36 | var moduleInstance = { 37 | name: name, 38 | requires: requires, 39 | constant: function(key, value) { 40 | 41 | } 42 | }; 43 | 44 | modules[name] = moduleInstance; 45 | return moduleInstance; 46 | }; 47 | ``` 48 | 49 | 关于模块和 injector 的一个通用规则, 模块实际上不存储任何应用组件, 只存储创建应用组件的方法. 实际上 injector 才是将它们变成具体组件的地方. 50 | 51 | 模块应该保存一些任务的集合, 例如注册一个 constant, 当模块加载时, injector 才去执行它们. 这些任务的集合叫做 invoke queue. 52 | 每个模块都有一个 invoke queue, 当模块被 injector 加载时, injector 运行 invoke queue 中的任务. 53 | 54 | 现在, 我们将要定义一个数组作为 invoke queue. 每个在 queue 中的数组都有两项: 注册的应用组件类型和注册该组件的参数. 55 | 56 | ```js 57 | [ 58 | ['constant', ['aConstant', 42]] 59 | ] 60 | ``` 61 | 62 | invoke queue 被存储在一个叫做 _invokeQueue 的模块对象属性上, 由于在 constant 函数中, 我们要 push 一个 constant 定义到 invoke queue 中, 所以我们在 createModule 引入一个本地变量: 63 | 64 | ```js 65 | var createModule = function(name, requires, modules) { 66 | if (name === 'hasOwnProperty') { 67 | throw 'hasOwnProperty is not a valid module name'; 68 | } 69 | var invokeQueue = []; 70 | var moduleInstance = { 71 | name: name, 72 | requires: requires, 73 | constant: function(key, value) { 74 | invokeQueue.push(['constant', [key, value]]); 75 | }, 76 | _invokeQueue: invokeQueue 77 | }; 78 | modules[name] = moduleInstance; 79 | return moduleInstance; 80 | }; 81 | ``` 82 | 83 | 然后, 我们来创建 injector. 我们应该遍历传入模块名称, 查找相应的模块对象, 然后再遍历它们的 invoke queue: 84 | 85 | ```js 86 | 'use strict'; 87 | var _ = require('lodash'); 88 | function createInjector(modulesToLoad) { 89 | _.forEach(modulesToLoad, function(moduleName) { 90 | var module = window.angular.module(moduleName); 91 | _.forEach(module._invokeQueue, function(invokeArgs) { 92 | 93 | }); 94 | }); 95 | return {}; 96 | } 97 | module.exports = createInjector; 98 | ``` 99 | 100 | 在 injector 内, 我们有一些代码知道如何处理 invoke queue 保存的每一项. 我们把这些代码放到一个叫做 $provide 的对象里. 当我们遍历 invoke queue 101 | 中的项时, 我们根据每个 invocation 数组的第一项从 $provide 中查找方法(例如 'constant'), 然后用 invocation 数组的第二项作为参数调用方法: 102 | 103 | ```js 104 | function createInjector(modulesToLoad) { 105 | var $provide = { 106 | constant: function(key, value) { 107 | 108 | } 109 | }; 110 | 111 | _.forEach(modulesToLoad, function(moduleName) { 112 | var module = window.angular.module(moduleName); 113 | _.forEach(module._invokeQueue, function(invokeArgs) { 114 | var method = invokeArgs[0]; 115 | var args = invokeArgs[1]; 116 | $provide[method].apply($provide, args); 117 | }); 118 | }); 119 | 120 | return {}; 121 | } 122 | ``` 123 | 124 | 所以, 当你在 module 上调用一个方法, 例如 constant 时, 这会引起 createInjector 方法中的 $provide 对象中相同参数的相同方法被调用, 但是它不会立即发生, 只有模块加载的时候. 与此同时, 关于方法调用的信息被存储在 invoke queue. 125 | 126 | 仍然还剩注册 constant 的逻辑没被实现. 不同的类型的应用组件需要不同的初始化逻辑, 但是它们有共同的地方, 一旦它们被初始化后, 它们会被 injector 保存. constant 实际上是非常简单的东西, 我们可以把它放在 cache 中. 然后我们实现 injector 的 has 方法, 用来在 cache 中检查相对应的 key. 127 | 128 | ```js 129 | function createInjector(modulesToLoad) { 130 | var cache = {}; 131 | var $provide = { 132 | constant: function(key, value) { 133 | cache[key] = value; 134 | } 135 | }; 136 | 137 | _.forEach(modulesToLoad, function(moduleName) { 138 | var module = window.angular.module(moduleName); 139 | _.forEach(module._invokeQueue, function(invokeArgs) { 140 | var method = invokeArgs[0]; 141 | var args = invokeArgs[1]; 142 | $provide[method].apply($provide, args); 143 | }); 144 | }); 145 | 146 | return { 147 | has: function(key) { 148 | return cache.hasOwnProperty(key); 149 | } 150 | }; 151 | } 152 | ``` 153 | 154 | 现在, 我们又一次遇到一种情况: 需要保护对象的 hasOwnProperty 属性. 不允许注册一个叫做 hasOwnProperty 的 constant: 155 | 156 | ```js 157 | it('does not allow a constant called hasOwnProperty', function() { 158 | var module = window.angular.module('myModule', []); 159 | module.constant('hasOwnProperty', false); 160 | 161 | expect(function() { 162 | createInjector(['myModule']); 163 | }).toThrow(); 164 | 165 | }); 166 | ``` 167 | 168 | 除此之外检查一个应用组件是否存在方法, injector 也要有一个获取组件的本身的方法. 基于此, 我们引入一个叫做 get 的方法: 169 | 170 | ```js 171 | it('can return a registered constant', function() { 172 | var module = window.angular.module('myModule', []); 173 | module.constant('aConstant', 42); 174 | var injector = createInjector(['myModule']); 175 | expect(injector.get('aConstant')).toBe(42); 176 | }); 177 | ``` 178 | 179 | 该方法简单的通过 key 从缓存中查找: 180 | 181 | ```js 182 | return { 183 | has: function(key) { 184 | return cache.hasOwnProperty(key); 185 | }, 186 | get: function(key) { 187 | return cache[key]; 188 | } 189 | }; 190 | ``` 191 | -------------------------------------------------------------------------------- /chapter11/Registering-A-Module.md: -------------------------------------------------------------------------------- 1 | ### 注册一个模块 2 | 3 | 有了上面几节的基础, 让我们在本节进入实际想做的的事情: 注册模块. 4 | 5 | 本节所有在 loader_spec.js 中剩余的测试都将与模块加载器一起工作, 所以让我们创建一个嵌套的 describe 块, 并将模块加载器的设置放到一个 before 块中. 6 | 7 | ```js 8 | describe('modules', function() { 9 | beforeEach(function() { 10 | setupModuleLoader(window); 11 | }); 12 | }); 13 | ``` 14 | 15 | 第一种我们将要测试的行为是, 调用 angular.module 方法并返回一个 module 对象. 16 | 17 | angular.module 方法需要一个表示模块名称的字符串参数和一个可以为空数组的表示模块依赖的数组参数. 该方法构建一个 module 对象并返回. 这个 module 对象包含一个存有它的名称的 name 属性: 18 | 19 | ```js 20 | it('allows registering a module', function() { 21 | var myModule = window.angular.module('myModule', []); 22 | expect(myModule).toBeDefned(); 23 | expect(myModule.name).toEqual('myModule'); 24 | }); 25 | ``` 26 | 27 | 当你多次注册一个相同名称的 module, 新的 module 会取代旧的. 这也意味着使用相同的 name 两次滴啊用 module 方法, 会得到不同的 module 对象: 28 | 29 | ```js 30 | it('replaces a module when registered with same name again', function() { 31 | var myModule = window.angular.module('myModule', []); 32 | var myNewModule = window.angular.module('myModule', []); 33 | expect(myNewModule).not.toBe(myModule); 34 | }); 35 | ``` 36 | 37 | 在我们的 module 方法中, 让我们把创建 module 的工作交给一个叫 createModule 函数去做. 在这个函数中, 我们创建一个 module 对象并返回. 38 | 39 | ```js 40 | function setupModuleLoader(window) { 41 | var ensure = function(obj, name, factory) { 42 | return obj[name] || (obj[name] = factory()); 43 | }; 44 | 45 | var angular = ensure(window, 'angular', Object); 46 | 47 | var createModule = function(name, requires) { 48 | var moduleInstance = { 49 | name: name 50 | }; 51 | return moduleInstance; 52 | }; 53 | 54 | ensure(angular, 'module', function() { 55 | return function(name, requires) { 56 | return createModule(name, requires); 57 | }; 58 | }); 59 | } 60 | ``` 61 | 62 | 除了 module 的名称, module 对象还应该含有一个模块依赖数组的引用: 63 | 64 | ```js 65 | it('attaches the requires array to the registered module', function() { 66 | var myModule = window.angular.module('myModule', ['myOtherModule']); 67 | expect(myModule.requires).toEqual(['myOtherModule']); 68 | }); 69 | ``` 70 | 71 | 我们给 module 对象添加一个模块依赖数组去满足这个需求: 72 | 73 | ```js 74 | var createModule = function(name, requires) { 75 | var moduleInstance = { 76 | name: name, 77 | requires: requires 78 | }; 79 | return moduleInstance; 80 | }; 81 | ``` 82 | -------------------------------------------------------------------------------- /chapter11/Rejecting-Non-String-DI-Tokens.md: -------------------------------------------------------------------------------- 1 | ### 拒绝非字符串类型的 DI 依赖 2 | 3 | 我们已经看到, $inject 数组应该包含依赖名称. 如果在 $inject 数组中放入无效东西, 例如 放入一个数字, 当前的实现仅会匹配一个 undefined. 此时我们需要抛出一个异常, 让使用者知道他们做错了: 4 | 5 | ```js 6 | it('does not accept non-strings as injection tokens', function() { 7 | var module = window.angular.module('myModule', []); 8 | module.constant('a', 1); 9 | 10 | var injector = createInjector(['myModule']); 11 | 12 | var fn = function(one, two) { return one + two; }; 13 | fn.$inject = ['a', 2]; 14 | 15 | expect(function() { 16 | injector.invoke(fn); 17 | }).toThrow(); 18 | }); 19 | ``` 20 | 21 | 在遍历依赖的 mapping 函数中, 通过一个简单的类型检查就可以搞定: 22 | 23 | ```js 24 | function invoke(fn) { 25 | var args = _.map(fn.$inject, function(token) { 26 | if (_.isString(token)) { 27 | return cache[token]; 28 | } else { 29 | throw 'Incorrect injection token! Expected a string, got '+token; 30 | } 31 | }); 32 | 33 | return fn.apply(null, args); 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /chapter11/Requiring-Other-Modules.md: -------------------------------------------------------------------------------- 1 | ### 依赖其他模块 2 | 3 | 到目前为止, 我们已经通过加载单个模块创建 injector, 但是也可以通过加载多个模块创建 injector. 最直接的方式是将多个模块名称放到一个数组中提供给 createInjector, 所有给定模块的应用组件都将被注册: 4 | 5 | ```js 6 | it('loads multiple modules', function() { 7 | var module1 = window.angular.module('myModule', []); 8 | var module2 = window.angular.module('myOtherModule', []); 9 | module1.constant('aConstant', 42); 10 | module2.constant('anotherConstant', 43); 11 | var injector = createInjector(['myModule', 'myOtherModule']); 12 | expect(injector.has('aConstant')).toBe(true); 13 | expect(injector.has('anotherConstant')).toBe(true); 14 | }); 15 | ``` 16 | 17 | 这个我们已经处理过了, 因为我们在 createInjector 中遍历 modulesToLoad 数组. 18 | 另一种引起多个模块被加载的方式是加载的模块依赖其他模块. 当使用 angular.module 方法注册一个模块时, 会有第二个数组参数, 目前是一个空数组, 但是它也可以存放依赖的模块名称. 当模块加载时, 这些依赖的模块也会被加载. 19 | 20 | ```js 21 | it('loads the required modules of a module', function() { 22 | var module1 = window.angular.module('myModule', []); 23 | var module2 = window.angular.module('myOtherModule', ['myModule']); 24 | 25 | module1.constant('aConstant', 42); 26 | module2.constant('anotherConstant', 43); 27 | 28 | var injector = createInjector(['myOtherModule']); 29 | 30 | expect(injector.has('aConstant')).toBe(true); 31 | expect(injector.has('anotherConstant')).toBe(true); 32 | }); 33 | ``` 34 | 35 | ```js 36 | it('loads the transitively required modules of a module', function() { 37 | var module1 = window.angular.module('myModule', []); 38 | var module2 = window.angular.module('myOtherModule', ['myModule']); 39 | var module3 = window.angular.module('myThirdModule', ['myOtherModule']); 40 | 41 | module1.constant('aConstant', 42); 42 | module2.constant('anotherConstant', 43); 43 | module3.constant('aThirdConstant', 44); 44 | 45 | var injector = createInjector(['myThirdModule']); 46 | 47 | expect(injector.has('aConstant')).toBe(true); 48 | expect(injector.has('anotherConstant')).toBe(true); 49 | expect(injector.has('aThirdConstant')).toBe(true); 50 | }); 51 | ``` 52 | 53 | 这种工作方式其实很简单, 当我们加载一个模块, 在遍历模块的 invoke queue 之前, 我们遍历该模块依赖的模块, 递归的加载它们每一个. 因此我们需要一个模块加载函数, 可以递归调用它: 54 | 55 | ```js 56 | _.forEach(modulesToLoad, function loadModule(moduleName) { 57 | var module = window.angular.module(moduleName); 58 | _.forEach(module.requires, loadModule); 59 | _.forEach(module._invokeQueue, function(invokeArgs) { 60 | var method = invokeArgs[0]; 61 | var args = invokeArgs[1]; 62 | $provide[method].apply($provide, args); 63 | }); 64 | }); 65 | ``` 66 | 67 | 当你有模块依赖其他模块时, 它很容易出现一种情况, 在多个模块中循环依赖: 68 | 69 | ```js 70 | it('loads each module only once', function() { 71 | window.angular.module('myModule', ['myOtherModule']); 72 | window.angular.module('myOtherModule', ['myModule']); 73 | createInjector(['myModule']); 74 | }); 75 | ``` 76 | 77 | 我们当前的实现在加载模块时, 总是递归也不检查模块是否需要加载. 78 | 79 | 通过确保每个模块只被加载一次来处理循环依赖. 这样做还有一个效果是当有两个相同的模块要被加载, 不存在循环, 它不会被加载两次, 因此避免了额外的工作. 80 | 我们将引入一个对象, 追踪模块已经被加载过. 在加载一个模块前, 检查该模块是否已经被加载过: 81 | 82 | 83 | ```js 84 | function createInjector(modulesToLoad) { 85 | var cache = {}; 86 | var loadedModules = {}; 87 | var $provide = { 88 | constant: function(key, value) { 89 | if (key === 'hasOwnProperty') { 90 | throw 'hasOwnProperty is not a valid constant name!'; 91 | } 92 | cache[key] = value; 93 | } 94 | }; 95 | 96 | _.forEach(modulesToLoad, function loadModule(moduleName) { 97 | if (!loadedModules.hasOwnProperty(moduleName)) { 98 | loadedModules[moduleName] = true; 99 | var module = window.angular.module(moduleName); 100 | _.forEach(module.requires, loadModule); 101 | _.forEach(module._invokeQueue, function(invokeArgs) { 102 | var method = invokeArgs[0]; 103 | var args = invokeArgs[1]; 104 | $provide[method].apply($provide, args); 105 | }); 106 | } 107 | }); 108 | 109 | return { 110 | has: function(key) { 111 | return cache.hasOwnProperty(key); 112 | }, 113 | 114 | get: function(key) { 115 | return cache[key]; 116 | } 117 | }; 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /chapter11/Strict-Mode.md: -------------------------------------------------------------------------------- 1 | ### 严格模式 2 | 3 | 从源码中确认函数依赖方式是 Angular 最具争议的特性之一. 在一定程度上是因为它的不可以预期和神奇性. 但是使用这种方式仍然有一些非常实际的问题: 在发布前, 如果你选择压缩你的 JavaScript 代码: 大部分人都会这样做, 应用源码会被改变, 关键是当你使用例如 UglifyJS 或 Closure Compiler 这样的压缩工具时, 函数的参数名称也会改变. 这样会破坏掉未标注的依赖注入. 4 | 5 | 因为这个原因, 许多人不会选择使用未标注依赖注入方式的特性. Angular 可以通过强制执行严格的依赖注入模式来帮助坚持这个决定, 如果你尝试注入一个没有明确标注依赖的函数,它会抛出一个错误。 6 | 7 | 通过将布尔标志作为第二个参数传给 createInjector 函数启用严格的依赖注入: 8 | 9 | ```js 10 | it('throws when using a non-annotated fn in strict mode', function() { 11 | var injector = createInjector([], true); 12 | var fn = function(a, b, c) { }; 13 | expect(function() { 14 | injector.annotate(fn); 15 | }).toThrow(); 16 | }); 17 | ``` 18 | 19 | createInjector 函数接受第二个参数, 只有为 true 时, 启用严格模式: 20 | 21 | ```js 22 | function createInjector(modulesToLoad, strictDi) { 23 | var cache = {}; 24 | var loadedModules = {}; 25 | strictDi = (strictDi === true); 26 | // ... 27 | } 28 | ``` 29 | 30 | 在 annotate 方法中, 当我们获得解析到的函数参数时, 我们要检查如果是严格模式并抛出异常: 31 | 32 | ```js 33 | function annotate(fn) { 34 | if (_.isArray(fn)) { 35 | return fn.slice(0, fn.length - 1); 36 | } else if (fn.$inject) { 37 | return fn.$inject; 38 | } else if (!fn.length) { 39 | return []; 40 | } else { 41 | if (strictDi) { 42 | throw 'fn is not using explicit annotation and '+ 43 | 'cannot be invoked in strict mode'; 44 | } 45 | var source = fn.toString().replace(STRIP_COMMENTS, ''); 46 | var argDeclaration = source.match(FN_ARGS); 47 | return _.map(argDeclaration[1].split(','), function(argName) { 48 | return argName.match(FN_ARG)[2]; 49 | }); 50 | } 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /chapter11/The-Injector.md: -------------------------------------------------------------------------------- 1 | ### Injector 2 | 3 | 让我们调整一下方向, 来说说 Angular 依赖注入的基础: injector. 4 | 5 | injector 不是模块加载器(module loader)的一部分, 是一个独立服务, 所以我们将代码和测试放在新文件里. 在这些测试中, 我们将假设一个新的模块加载已经设置完成: 6 | 7 | ```js 8 | 'use strict'; 9 | var setupModuleLoader = require('../src/loader'); 10 | 11 | describe('injector', function() { 12 | beforeEach(function() { 13 | delete window.angular; 14 | setupModuleLoader(window); 15 | }); 16 | }); 17 | ``` 18 | 19 | 通过调用函数 createInjector 创建一个 injector. createInjector 函数需要一个 module 名称的数组参数, 并返回一个 injector 对象: 20 | 21 | ```js 22 | 'use strict'; 23 | var setupModuleLoader = require('../src/loader'); 24 | var createInjector = require('../src/injector'); 25 | 26 | describe('injector', function() { 27 | beforeEach(function() { 28 | delete window.angular; 29 | setupModuleLoader(window); 30 | }); 31 | 32 | it('can be created', function() { 33 | var injector = createInjector([]); 34 | expect(injector).toBeDefned(); 35 | }); 36 | }); 37 | ``` 38 | 39 | 现在, 我们先给出一个实现, 简单返回一个空对象字面量: 40 | 41 | ```js 42 | 'use strict'; 43 | function createInjector(modulesToLoad) { 44 | return {}; 45 | } 46 | module.exports = createInjector; 47 | ``` 48 | -------------------------------------------------------------------------------- /chapter11/The-angular-Global.md: -------------------------------------------------------------------------------- 1 | ### 全局对象 angular 2 | 3 | 如果你曾经使用过 Angular, 你或许对全局 angular 对象有兴趣. 这是我们在这里介绍全局 angular 对象的目的. 4 | 5 | 现在我们需要这个对象的原因是用来存储注册的 Angular 模块信息. 当我们开始创建模块和 injector 时, 我们需要存储它们. 6 | 7 | 在一个叫做 loader.js 的文件中, 实现处理模块的框架组件: 模块加载器. 我们在这里引入全局 angular 对象. 但是首先, 让我们添加一个测试. 8 | 9 | ```js 10 | 'use strict'; 11 | var setupModuleLoader = require('../src/loader'); 12 | describe('setupModuleLoader', function() { 13 | it('exposes angular on the window', function() { 14 | setupModuleLoader(window); 15 | expect(window.angular).toBeDefned(); 16 | }); 17 | }); 18 | ``` 19 | 20 | 该测试假设已经有一个叫做 setupModuleLoader 的函数可以使用, 也假设你可以使用 window 对象作为参数调用 setupModuleLoader 函数. 当调用结束, window 对象上会有一个 angular 属性. 21 | 22 | 让我们创建一个 load.js 并使上面的测试通过: 23 | 24 | ```js 25 | function setupModuleLoader(window) { 26 | var angular = window.angular = {}; 27 | } 28 | module.exports = setupModuleLoader; 29 | ``` 30 | -------------------------------------------------------------------------------- /chapter11/The-module-Method.md: -------------------------------------------------------------------------------- 1 | ### module 方法 2 | 3 | 我们将介绍 angular 的第一个方法, 也是我们将会在下面的章节中使用很多的方法: module. 我们判断该方法已经存在于刚创建的 angular 全局对象中: 4 | 5 | ```js 6 | it('exposes the angular module function', function() { 7 | setupModuleLoader(window); 8 | expect(window.angular.module).toBeDefned(); 9 | }); 10 | ``` 11 | 12 | 和 angular 全局对象一样, 当 setupModuleLoader 方法被调用多次时, module 方法不应该被覆盖: 13 | 14 | ```js 15 | it('exposes the angular module function just once', function() { 16 | setupModuleLoader(window); 17 | var module = window.angular.module; 18 | setupModuleLoader(window); 19 | expect(window.angular.module).toBe(module); 20 | }); 21 | ``` 22 | 23 | 我们可以重用 ensure 函数去构建 module 方法. 24 | 25 | ```js 26 | function setupModuleLoader(window) { 27 | var ensure = function(obj, name, factory) { 28 | return obj[name] || (obj[name] = factory()); 29 | }; 30 | 31 | var angular = ensure(window, 'angular', Object); 32 | 33 | ensure(angular, 'module', function() { 34 | return function() { 35 | 36 | }; 37 | }); 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /chapter12/Circular-Dependencies.md: -------------------------------------------------------------------------------- 1 | ### 循环依赖 2 | 3 | 因为我们现在有依赖依赖于其他依赖, 我们介绍一种可能发生的情况, 循环依赖链: 如果 A 依赖于 B, B 依赖于 C 并且 C 又依赖于 A. 没有一种方式可以在构造 A, B 或 C 时避免进入无限循环. 或许我们可以显示一些有用的错误信息. 4 | 5 | ```js 6 | it('notifes the user about a circular dependency', function() { 7 | var module = window.angular.module('myModule', []); 8 | 9 | module.provider('a', {$get: function(b) { }}); 10 | module.provider('b', {$get: function(c) { }}); 11 | module.provider('c', {$get: function(a) { }}); 12 | 13 | var injector = createInjector(['myModule']); 14 | expect(function() { 15 | injector.get('a'); 16 | }).toThrowError(/Circular dependency found/); 17 | 18 | }); 19 | ``` 20 | 21 | 这个小技巧在这儿有两部分: 第一, 在调用 $get 方法构建依赖之前, 我们在 instanceCache 中放一个特殊标记值. 这个标记值告诉我们, 依赖正在被构建. 22 | 第二, 当我们查找依赖时, 发现了这个标记, 就意味着, 我们正在试图查找正在构建的依赖, 也就是说存在循环依赖. 23 | 24 | 我们可以使用一个空对象作为标记值. 非常重要的事情是它不等于任何其他事物. 让我们在 injector.js 开头引入这个标记: 25 | 26 | ```js 27 | var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; 28 | var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; 29 | var STRIP_COMMENTS = /(\/\/.*$)|(\/\*.*?\*\/)/mg; 30 | var INSTANTIATING = { }; 31 | ``` 32 | 33 | 在 getService 方法中, 在调用 provider 之前, 我们将这个标记放入 instanceCache 中, 当查找实例时, 检查它: 34 | 35 | ```js 36 | function getService(name) { 37 | if (instanceCache.hasOwnProperty(name)) { 38 | if (instanceCache[name] === INSTANTIATING) { 39 | throw new Error('Circular dependency found'); 40 | } 41 | return instanceCache[name]; 42 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 43 | instanceCache[name] = INSTANTIATING; 44 | var provider = providerCache[name + 'Provider']; 45 | var instance = instanceCache[name] = invoke(provider.$get); 46 | return instance; 47 | } 48 | } 49 | ``` 50 | 51 | 因为依赖最终是会被构建的, 所以 INSTANTIATING 标志在 instanceCache 中会被替换的. 但是在实例化时, 一些事情也会出错, 此时我们不想将标记留在 instanceCache 中: 52 | 53 | ```js 54 | it('cleans up the circular marker when instantiation fails', function() { 55 | var module = window.angular.module('myModule', []); 56 | module.provider('a', {$get: function() { 57 | throw 'Failing instantiation!'; 58 | }}); 59 | 60 | var injector = createInjector(['myModule']); 61 | 62 | expect(function() { 63 | injector.get('a'); 64 | }).toThrow('Failing instantiation!'); 65 | 66 | expect(function() { 67 | injector.get('a'); 68 | }).toThrow('Failing instantiation!'); 69 | 70 | }); 71 | ``` 72 | 73 | 我们正在这里做的就是尝试初始化实例并失败两次, 它将尝试调用 provider 两次. 当前的实现不会这样做, 因为在第一次, 在 instanceCache 中留下了 74 | INSTANTIATING 标记, 在第二次调用时, 发现 INSTANTIATING 标志, 得出这是一个循环依赖的结论. 所以, 我们要确保如果实例化依赖失败, 不要留下 INSTANTIATING 标记: 75 | 76 | ```js 77 | function getService(name) { 78 | if (instanceCache.hasOwnProperty(name)) { 79 | if (instanceCache[name] === INSTANTIATING) { 80 | throw new Error('Circular dependency found'); 81 | } 82 | return instanceCache[name]; 83 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 84 | instanceCache[name] = INSTANTIATING; 85 | try { 86 | var provider = providerCache[name + 'Provider']; 87 | var instance = instanceCache[name] = invoke(provider.$get); 88 | return instance; 89 | } fnally { 90 | if (instanceCache[name] === INSTANTIATING) { 91 | delete instanceCache[name]; 92 | } 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | 只是通知用户有一个循环依赖, 不是非常的有用. 最好可以让他们知道问题出在哪里: 99 | 100 | 我们想要显示用户发生问题的依赖关系路径. 在我们例子中, 请从右往左读: 101 | 102 | ```a <- c <- b <- a``` 103 | 104 | 更新现有的测试用例, 并期望一个这样的错误信息: 105 | 106 | ```js 107 | it('notifes the user about a circular dependency', function() { 108 | var module = window.angular.module('myModule', []); 109 | module.provider('a', {$get: function(b) { }}); 110 | module.provider('b', {$get: function(c) { }}); 111 | module.provider('c', {$get: function(a) { }}); 112 | var injector = createInjector(['myModule']); 113 | expect(function() { 114 | injector.get('a'); 115 | }).toThrowError('Circular dependency found: a <- c <- b <- a'); 116 | }); 117 | ``` 118 | 119 | 我们需要做的是用一种数据结构存储当前的依赖关系路径. 让我们在 createInjector 方法中引入一个数组: 120 | 121 | ```js 122 | function createInjector(modulesToLoad, strictDi) { 123 | var providerCache = {}; 124 | var instanceCache = {}; 125 | var loadedModules = {}; 126 | var path = []; 127 | // ... 128 | ``` 129 | 130 | In getService() we can treat path essentially as a stack. When we start resolving a dependency, 131 | we add its name to the front of the path. When we’re done, we pop it off: 132 | 133 | 在 getService 方法中, 本质上, 我们可以将路径看做一个堆栈. 当我们开始分析一个依赖, 我们将它的名字添加到 path 的前面. 当我们完成时, 弹出它: 134 | 135 | ```js 136 | function getService(name) { 137 | if (instanceCache.hasOwnProperty(name)) { 138 | if (instanceCache[name] === INSTANTIATING) { 139 | throw new Error('Circular dependency found'); 140 | } 141 | return instanceCache[name]; 142 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 143 | path.unshift(name); 144 | instanceCache[name] = INSTANTIATING; 145 | try { 146 | var provider = providerCache[name + 'Provider']; 147 | var instance = instanceCache[name] = invoke(provider.$get); 148 | return instance; 149 | } finally { 150 | path.shift(); 151 | if (instanceCache[name] === INSTANTIATING) { 152 | delete instanceCache[name]; 153 | } 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | 如果, 我们遇到一个循环依赖, 我们可以使用当前的 path 值来显示有问题的用户依赖关系路径: 160 | 161 | ```js 162 | function getService(name) { 163 | if (instanceCache.hasOwnProperty(name)) { 164 | if (instanceCache[name] === INSTANTIATING) { 165 | throw new Error('Circular dependency found: ' + 166 | name + ' <- ' + path.join(' <- ')); 167 | } 168 | return instanceCache[name]; 169 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 170 | path.unshift(name); 171 | instanceCache[name] = INSTANTIATING; 172 | try { 173 | var provider = providerCache[name + 'Provider']; 174 | var instance = instanceCache[name] = invoke(provider.$get, provider); 175 | return instance; 176 | } finally { 177 | path.shift(); 178 | } 179 | } 180 | } 181 | ``` 182 | 183 | 现在, 用户可以看出哪里有问题了! 184 | -------------------------------------------------------------------------------- /chapter12/Injecting-Dependencies-To-The-$get-Method.md: -------------------------------------------------------------------------------- 1 | ### 对 $get 方法注入依赖 2 | 3 | 在这点上, 我们通过介绍 provider 获得的很少, 我们仅仅隐藏 $get 方法之后的实际依赖. 4 | 5 | 当我们想到一种构建的应用组件有自己的依赖的情况, 也就是说当我们的依赖有依赖的时候, 好处开始显现出来了. 目前, 我们没有办法处理这种情况, 但是我们有 $get 方法, 我们可以用依赖注入的方式调用它: 6 | 7 | ```js 8 | it('injects the $get method of a provider', function() { 9 | var module = window.angular.module('myModule', []); 10 | module.constant('a', 1); 11 | module.provider('b', { 12 | $get: function(a) { 13 | return a + 2; 14 | } 15 | }); 16 | 17 | var injector = createInjector(['myModule']); 18 | expect(injector.get('b')).toBe(3); 19 | }); 20 | ``` 21 | 22 | 这个很容易: 我们需要用依赖注入的方式调用 provider.$get 在上一章节, 我们已经准确实现该功能: invoke 23 | 24 | ```js 25 | var $provide = { 26 | constant: function(key, value) { 27 | if (key === 'hasOwnProperty') { 28 | throw 'hasOwnProperty is not a valid constant name!'; 29 | } 30 | cache[key] = value; 31 | }, 32 | 33 | provider: function(key, provider) { 34 | cache[key] = invoke(provider.$get, provider); 35 | } 36 | }; 37 | ``` 38 | 39 | 注意, 我们在调用中将 this 绑定到 provider 对象上. 因为 $get 是 provider 对象的一个方法, 它的 this 应该绑到 provider 对象上. 40 | 41 | 所以, invoke 不仅是一个可以调用你自己的函数的 injector 的方法, 而且也可以内部使用, 对 provider.$get 方法注入依赖. 42 | -------------------------------------------------------------------------------- /chapter12/Lazy-Instantiation-of-Dependencies.md: -------------------------------------------------------------------------------- 1 | ### 依赖的延迟实例化 2 | 3 | 现在, 我们可以在依赖之间使用依赖, 我们将要开始考虑这些事情完成的顺序. 当我们创建一个依赖的时候, 我们所拥有的所有的依赖都可用吗? 考虑下面的情况, b 依赖 a, 但是 b 注册在 a 之前: 4 | 5 | ```js 6 | it('injects the $get method of a provider lazily', function() { 7 | var module = window.angular.module('myModule', []); 8 | module.provider('b', { 9 | $get: function(a) { 10 | return a + 2; 11 | } 12 | }); 13 | 14 | module.provider('a', {$get: _.constant(1)}); 15 | 16 | var injector = createInjector(['myModule']); 17 | 18 | expect(injector.get('b')).toBe(3); 19 | }); 20 | ``` 21 | 22 | 这个测试的失败是因为我们尝试为 b 调用 $get 方法, 它的依赖的 a 还不可用. 23 | 24 | 如果真实 Angular injector 是这样工作的, 你必须非常小心按照依赖顺序加载你的代码. 幸运的是, injector 不是这样工作的. 相反的, injecter 会延迟调用这些 $get 方法, 只有当返回值是需要的时候. 25 | 因此, 即使 b 第一个被注册, 这时它的 $get 方法也不会被调用. 只有当我们要求 injector b 它才被调用, 这个时候 a 已经被注册过了. 26 | 27 | 因为, 当我们遍历 invoke queue 时, 此时不能调用 provider 的 $get, 我们需要保持住 provider 对象稍后调用它. 我们有一个用于存储的 cache 对象, 但是将 provider 对象放在这里是讲不通的, 因为 cache 是用来存储依赖实例的, 不是用来存储生产它们的 provider 的. 我们需要将 cache 分为两个: 一个存储所有实例, 一个存储所有 provider 28 | 29 | ```js 30 | function createInjector(modulesToLoad, strictDi) { 31 | var providerCache = {}; 32 | var instanceCache = {}; 33 | var loadedModules = {}; 34 | // ... 35 | } 36 | ``` 37 | 38 | 然后, 在 $provider 中, 我们把 constant 放入 instanceCache, 把 provider 放入 providerCache 中. 当存储一个 provider 时, 我们在 cache 的 key 上加一个 provider 前缀. 因此, 注册的 a provider, 它的会以 'aProvider' 作为 key 存入 provider 的 cache 中. 我们需要一个清楚的界限, 在 instance 和它们的 provider 之间, 它们的名字强化这种区别: 39 | 40 | ```js 41 | var $provide = { 42 | constant: function(key, value) { 43 | if (key === 'hasOwnProperty') { 44 | throw 'hasOwnProperty is not a valid constant name!'; 45 | } 46 | instanceCache[key] = value; 47 | }, 48 | 49 | provider: function(key, provider) { 50 | providerCache[key + 'Provider'] = provider; 51 | } 52 | }; 53 | ``` 54 | 55 | 让我们创建一个 getService 方法, 给一个依赖名称参数, 首先, 在 instanceCache 中查找, 如果有, 直接返回, 反之, 在 providerCache 中查找, 如果找到, 实例化并返回: 56 | 57 | ```js 58 | function getService(name) { 59 | if (instanceCache.hasOwnProperty(name)) { 60 | return instanceCache[name]; 61 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 62 | var provider = providerCache[name + 'Provider']; 63 | return invoke(provider.$get, provider); 64 | } 65 | } 66 | ``` 67 | 68 | 在 invoke 的依赖查找循环中, 可以调用我们的新函数 getService, 如果从本地查找失败, 我们可以根据依赖名称在 cache 中查找: 69 | 70 | 71 | ```js 72 | function invoke(fn, self, locals) { 73 | var args = _.map(annotate(fn), function(token) { 74 | 75 | if (_.isString(token)) { 76 | return locals && locals.hasOwnProperty(token) ? 77 | locals[token] : 78 | getService(token); 79 | } else { 80 | throw 'Incorrect injection token! Expected a string, got '+token; 81 | } 82 | }); 83 | 84 | if (_.isArray(fn)) { 85 | fn = _.last(fn); 86 | } 87 | 88 | return fn.apply(self, args); 89 | } 90 | ``` 91 | 92 | 最后, 我们也需要更新 injector.get 和 injector.has 方法, 让它们以新方式工作. injector.get 方法只是 getService 方法的一个别名. 在 has 方法中, 我们需要对 instanceCache 和 providerCache 做一个属性检查, 当检查 providerCache 时, 加上 Provider 后缀: 93 | 94 | ```js 95 | return { 96 | has: function(key) { 97 | return instanceCache.hasOwnProperty(key) || 98 | providerCache.hasOwnProperty(key + 'Provider'); 99 | }, 100 | get: getService, 101 | annotate: annotate, 102 | invoke: invoke 103 | }; 104 | ``` 105 | 106 | 所以, 一个 provider 的依赖, 只有当它被注入到某个地方时被实例化, 或者通过 injector.get 被显示调用. 如果没有人请求依赖, 依赖将永远不会被实例化, 也就是说它的 provider 的 $get 永远不会调用. 107 | 108 | 你通过 injector.has 可以检查依赖是否存在, 这样做, 并不会引起依赖被实例化. 它仅仅检查依赖的实例或 provider 是否可用. 109 | -------------------------------------------------------------------------------- /chapter12/Making-Sure-Everything-Is-A-Singleton.md: -------------------------------------------------------------------------------- 1 | ### 确保一切都是单例 2 | 3 | 你或许听过 "Angular 中的一切都是单例的", 这是真的. 当你在不同的地方使用相同的依赖, 你将会有一个引用指向相同的对象. 4 | 5 | 当前 injector 实现不是这样工作的. 当你请求创建两次 provider 时, 你会得到两个结果. 6 | 7 | ```js 8 | it('instantiates a dependency only once', function() { 9 | var module = window.angular.module('myModule', []); 10 | module.provider('a', {$get: function() { return {}; }}); 11 | 12 | var injector = createInjector(['myModule']); 13 | expect(injector.get('a')).toBe(injector.get('a')); 14 | }); 15 | ``` 16 | 17 | 测试失败, 是因为, 我们调用了两次 $get, 每一次都会它都返回一个新对象给我们. 18 | 19 | 解决方法很简单, 只需将 provider 调用后的返回值放到 instanceCache 中. 下次, 需要相同的依赖时, 可以直接在 instanceCache 中拿到, 我们永远不会调用同一个 provider 的 $get 方法两次: 20 | 21 | ```js 22 | function getService(name) { 23 | if (instanceCache.hasOwnProperty(name)) { 24 | return instanceCache[name]; 25 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 26 | var provider = providerCache[name + 'Provider']; 27 | var instance = instanceCache[name] = invoke(provider.$get); 28 | return instance; 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /chapter12/Provider-Constructors.md: -------------------------------------------------------------------------------- 1 | ### Provider 的构造函数 2 | 3 | 早先, 我们定义的 provider 是一个带有 $get 方法的对象. 这是正确的, 但是当你注册一个 provider, 你也可以选择使用构造函数实例化一个 provider: 4 | 5 | ```js 6 | function AProvider() { 7 | this.$get = function() { return 42; } 8 | } 9 | ``` 10 | 11 | 这是一个构造函数, 当你实例化它时, 结果是一个带有 $get 方法的对象(provider), 另一种构造函数如下所示: 12 | 13 | ```js 14 | function AProvider() { 15 | this.value = 42; 16 | } 17 | 18 | AProvider.prototype.$get = function() { 19 | return this.value; 20 | } 21 | ``` 22 | 23 | 所以, 使用这种构造函数风格, Angular 不会真正关系 $get 方法来自于哪里, 只要返回结果的对象有就行. 你可以充分利用 JavaScript 类式(使用 prototype 模拟)编程(或 ES2015 中的类)以及继承: 24 | 25 | ```js 26 | it('instantiates a provider if given as a constructor function', function() { 27 | var module = window.angular.module('myModule', []); 28 | module.provider('a', function AProvider() { 29 | this.$get = function() { return 42; }; 30 | }); 31 | 32 | var injector = createInjector(['myModule']); 33 | expect(injector.get('a')).toBe(42); 34 | }); 35 | ``` 36 | 37 | 更重要的是, 这些构造函数也可以被注入其他依赖: 38 | 39 | ```js 40 | it('injects the given provider constructor function', function() { 41 | var module = window.angular.module('myModule', []); 42 | module.constant('b', 2); 43 | module.provider('a', function AProvider(b) { 44 | this.$get = function() { return 1 + b; }; 45 | }); 46 | 47 | var injector = createInjector(['myModule']); 48 | expect(injector.get('a')).toBe(3); 49 | }); 50 | ``` 51 | 52 | 要启用构造函数的风格的 provider, 我们需要在注册时, 检查 provider 是否是一个函数. 如果是, 需要实例化它. 在上一章节, 我们创建一个函数 instantiate 是来做这个的. 53 | 54 | ```js 55 | provider: function(key, provider) { 56 | if (_.isFunction(provider)) { 57 | provider = instantiate(provider); 58 | } 59 | providerCache[key + 'Provider'] = provider; 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /chapter12/The Simplest-Possible-Provider-An-Object-with-A-$get-Method.md: -------------------------------------------------------------------------------- 1 | ### 一个尽可能最简单的 Provider: 拥有 $get 方法的对象 2 | 3 | 一般来说, Angular 调用的 provider 是一个拥有 $get 方法的任意 JavaScript 对象. 当你将这样一个对象传给 injector, injector 会调用 $get 方法并将它的返回值作为实际的依赖: 4 | 5 | ```js 6 | it('allows registering a provider and uses its $get', function() { 7 | var module = window.angular.module('myModule', []); 8 | module.provider('a', { 9 | $get: function() { 10 | return 42; 11 | } 12 | }); 13 | var injector = createInjector(['myModule']); 14 | expect(injector.has('a')).toBe(true); 15 | expect(injector.get('a')).toBe(42); 16 | }); 17 | ``` 18 | 19 | 因为 a 的 provider 是对象 `{$get: function() { return 42 }}`, 所以, 上面测试中的 a 是 42. 20 | 21 | 我们现在正在使用的这种间接的方式不是非常有用, 但是正如我们将要看到的, 它给了我们一个方便配置 a 的机会, 这是 constant 不可能有的. 22 | 23 | 为了实现这种 provider, 让我们从模块加载器开始, 它需要一个方法去注册 provider, 这个方法必须把注册的调用信息放到 invoke queue 中: 24 | 25 | ```js 26 | var moduleInstance = { 27 | name: name, 28 | requires: requires, 29 | constant: function(key, value) { 30 | invokeQueue.push(['constant', [key, value]]); 31 | }, 32 | provider: function(key, provider) { 33 | invokeQueue.push(['provider', [key, provider]]); 34 | }, 35 | _invokeQueue: invokeQueue 36 | }; 37 | ``` 38 | 39 | 在组件注册中, 有一些重复代码结构, 所以让我们引入一个通用的 invokeLater 函数, 该函数可以以最小的代价实现 module.constant 和 module.provider. 40 | 41 | ```js 42 | var createModule = function(name, requires, modules) { 43 | if (name === 'hasOwnProperty') { 44 | throw 'hasOwnProperty is not a valid module name'; 45 | } 46 | 47 | var invokeQueue = []; 48 | var invokeLater = function(method) { 49 | return function() { 50 | invokeQueue.push([method, arguments]); 51 | return moduleInstance; 52 | }; 53 | }; 54 | 55 | var moduleInstance = { 56 | name: name, 57 | requires: requires, 58 | constant: invokeLater('constant'), 59 | provider: invokeLater('provider'), 60 | _invokeQueue: invokeQueue 61 | }; 62 | 63 | modules[name] = moduleInstance; 64 | 65 | return moduleInstance; 66 | }; 67 | ``` 68 | 69 | invokeLater 返回一个函数, 是一个已经被预先注册特定类型的应用组件, 更确切的说是一个特定的 $provide 方法的函数. 该函数会被 push 到 invoke queue. 70 | 71 | 注意, 在注册的时候, 我们也返回模块实例, 这样就使链式注册成为可能: module.constant(‘a’, 42’).constant(‘b’, 43). 72 | 73 | 在 injector 的 $provide 对象, 我们仍然需要代码去处理 invoke queue 中的 provider. 现在, 我们简单的调用 provider 的 $get 方法 和 将返回的值放到 cache 中, 让我们满足第一个测试用例: 74 | 75 | ```js 76 | var $provide = { 77 | constant: function(key, value) { 78 | if (key === 'hasOwnProperty') { 79 | throw 'hasOwnProperty is not a valid constant name!'; 80 | } 81 | cache[key] = value; 82 | }, 83 | provider: function(key, provider) { 84 | cache[key] = provider.$get(); 85 | } 86 | }; 87 | ``` 88 | -------------------------------------------------------------------------------- /chapter12/Two-Injectors-The-Provider-Injector-and-The-Instance-Injector.md: -------------------------------------------------------------------------------- 1 | ### Two Injectors: The Provider Injector and The Instance Injector 2 | 3 | 两个 injector 第一个区别是, 你可以对 provider 的构造函数注入其他依赖: 4 | 5 | ```js 6 | it('injects another provider to a provider constructor function', function() { 7 | var module = window.angular.module('myModule', []); 8 | module.provider('a', function AProvider() { 9 | var value = 1; 10 | this.setValue = function(v) { value = v; }; 11 | this.$get = function() { return value; }; 12 | }); 13 | 14 | module.provider('b', function BProvider(aProvider) { 15 | aProvider.setValue(2); 16 | this.$get = function() { }; 17 | }); 18 | 19 | var injector = createInjector(['myModule']); 20 | expect(injector.get('a')).toBe(2); 21 | }); 22 | ``` 23 | 24 | 目前, 我们只能注入实例, 例如 constant 和 provider 包含 $get 方法的返回值. 这里, 我们注入了一个 provider: provider b 有一个依赖 provider a, 我们期望 aProvider 被注入. 然后调用它的 setValue() 方法去配置 aProvider. 当我们通过 injector 获得实例时, 我们看到的方法调用实际已经发生了. 25 | 26 | 为了使我们测试通过, 在 getService 中添加一个查找 providerCache 27 | 28 | ```js 29 | function getService(name) { 30 | if (instanceCache.hasOwnProperty(name)) { 31 | if (instanceCache[name] === INSTANTIATING) { 32 | throw new Error('Circular dependency found: ' + name + ' <- ' + path.join(' <- ')); 33 | } 34 | return instanceCache[name]; 35 | } else if (providerCache.hasOwnProperty(name)) { 36 | return providerCache[name]; 37 | } else if (providerCache.hasOwnProperty(name + 'Provider')) { 38 | path.unshift(name); 39 | instanceCache[name] = INSTANTIATING; 40 | try { 41 | var provider = providerCache[name + 'Provider']; 42 | var instance = instanceCache[name] = invoke(provider.$get); 43 | return instance; 44 | } finally { 45 | path.shift(); 46 | if (instanceCache[name] === INSTANTIATING) { 47 | delete instanceCache[name]; 48 | } 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | 我们现在为了两种不同的目的去查找 providerCache: 第一种, 我们查找 provider 为了实例化一个实例, 第二种仅仅查找一个 provider. 55 | 不管怎样, 它没有那么简单. 结果是, 你不能在任何你想注入的地方注入 provider 或 instance. 例如, 当你对一个 provider 的构造函数注入一个 provider 时, 你就不能注入一个实例在哪儿: 56 | 57 | ```js 58 | it('does not inject an instance to a provider constructor function', function() { 59 | var module = window.angular.module('myModule', []); 60 | module.provider('a', function AProvider() { 61 | this.$get = function() { return 1; }; 62 | }); 63 | 64 | module.provider('b', function BProvider(a) { 65 | this.$get = function() { return a; }; 66 | }); 67 | 68 | expect(function() { 69 | createInjector(['myModule']); 70 | }).toThrow(); 71 | }); 72 | ``` 73 | 74 | 所以, 虽然 BProvider 依赖于 aProvider, 但是它不依赖于 a. 同样的, 虽然你可以对 $get 方法注入一个实例, 但是你不能在这里对它注入 provider: 75 | 76 | ```js 77 | it('does not inject a provider to a $get function', function() { 78 | var module = window.angular.module('myModule', []); 79 | module.provider('a', function AProvider() { 80 | this.$get = function() { return 1; }; 81 | }); 82 | 83 | module.provider('b', function BProvider() { 84 | this.$get = function(aProvider) { return aProvider.$get(); }; 85 | }); 86 | 87 | var injector = createInjector(['myModule']); 88 | expect(function() { 89 | injector.get('b'); 90 | }).toThrow(); 91 | }); 92 | ``` 93 | 94 | 你也不能使用 injector.invoke 调用一个被注入 provider 的函数: 95 | 96 | ```js 97 | it('does not inject a provider to invoke', function() { 98 | var module = window.angular.module('myModule', []); 99 | module.provider('a', function AProvider() { 100 | this.$get = function() { return 1; } 101 | }); 102 | 103 | var injector = createInjector(['myModule']); 104 | 105 | expect(function() { 106 | injector.invoke(function(aProvider) { }); 107 | }).toThrow(); 108 | }); 109 | ``` 110 | 111 | 也不能通过调用 injector.get 获取并访问一个 provider: 112 | 113 | ```js 114 | it('does not give access to providers through get', function() { 115 | var module = window.angular.module('myModule', []); 116 | module.provider('a', function AProvider() { 117 | this.$get = function() { return 1; }; 118 | }); 119 | 120 | var injector = createInjector(['myModule']); 121 | 122 | expect(function() { 123 | injector.get('aProvider'); 124 | }).toThrow(); 125 | }); 126 | ``` 127 | 我们用这些测试概括出两种类型的注入之间的一个清晰的分界: 一种注入只会发生 provider 的构造函数之间并且只能注入其他 provider. 另一种发生在 $get 方法之间并且外部的 injector API 只能处理实例. 实例或许是被 provider 创建的, 但是 provider 不会被暴露出来. 128 | 129 | 这种分界实际上是通过两个单独的 injector 实现: 一个专门处理 provider, 另一个专门处理 instance. 后者将被通过公共 API 暴露出去, 前者只能被在 createInjector 内部使用. 130 | 131 | 让我们重新组织代码去支持两种 injector. 首先, 我们将一步一步介绍代码的变化, 最后列出所有重组后的 createInjector 的源码. 132 | 133 | 首先, 我们将会在 createInjector 里有一个内部函数用于处理两种内部的 injector. 这个函数有两个参数: 一个是用于查找依赖的 cache, 另一个是当 cache 中没有依赖时, 用于回调的工厂方法: 134 | 135 | ```js 136 | function createInternalInjector(cache, factoryFn) { 137 | 138 | } 139 | ``` 140 | 141 | 我们需要将所有的依赖查找函数移动到 createInternalInjector 中, 因为它们需要 cache 和 factoryFn 在作用域中. 首先, getService 已经在 createInternalInjector 中了, 它可以从给定 cache 中查找依赖: 142 | 143 | ```js 144 | function createInternalInjector(cache, factoryFn) { 145 | function getService(name) { 146 | if (cache.hasOwnProperty(name)) { 147 | if (cache[name] === INSTANTIATING) { 148 | throw new Error('Circular dependency found: ' + 149 | name + ' <- ' + path.join(' <- ')); 150 | } 151 | return cache[name]; 152 | } else { 153 | path.unshift(name); 154 | cache[name] = INSTANTIATING; 155 | try { 156 | return (cache[name] = factoryFn(name)); 157 | } finally { 158 | path.shift(); 159 | if (cache[name] === INSTANTIATING) { 160 | delete cache[name]; 161 | } 162 | } 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | 注意, 我们不再需要在 else 分支显示调用 provider. 这个工作发生在, 当缓存中没有时, 委托给 factoryFn 赋给 createInternalInjector. 169 | 170 | invoke 和 instantiate 方法也需要移动到 createInternalInjector 中, 因为它们依赖 getService. 它们自己的实现不需要改变. 171 | 172 | ```js 173 | function createInternalInjector(cache, factoryFn) { 174 | function getService(name) { 175 | if (cache.hasOwnProperty(name)) { 176 | if (cache[name] === INSTANTIATING) { 177 | throw new Error('Circular dependency found: ' + 178 | name + ' <- ' + path.join(' <- ')); 179 | } 180 | return cache[name]; 181 | } else { 182 | path.unshift(name); 183 | cache[name] = INSTANTIATING; 184 | try { 185 | return (cache[name] = factoryFn(name)); 186 | } finally { 187 | path.shift(); 188 | if (cache[name] === INSTANTIATING) { 189 | delete cache[name]; 190 | } 191 | } 192 | } 193 | } 194 | 195 | function invoke(fn, self, locals) { 196 | var args = annotate(fn).map(function(token) { 197 | if (_.isString(token)) { 198 | return locals && locals.hasOwnProperty(token) ? 199 | locals[token] : 200 | getService(token); 201 | } else { 202 | throw 'Incorrect injection token! Expected a string, got '+token; 203 | } 204 | }); 205 | 206 | if (_.isArray(fn)) { 207 | fn = _.last(fn); 208 | } 209 | return fn.apply(self, args); 210 | } 211 | 212 | function instantiate(Type, locals) { 213 | var instance = Object.create((_.isArray(Type) ? _.last(Type) : Type).prototype); 214 | invoke(Type, instance, locals); 215 | return instance; 216 | } 217 | } 218 | ``` 219 | 220 | createInternalInjector 的最后一部分是创建一个 injector 对象并返回. 这个对象与之前 createInjector 的返回值是相同的: 221 | 222 | ```js 223 | function createInternalInjector(cache, factoryFn) { 224 | // ... 225 | return { 226 | has: function(name) { 227 | return cache.hasOwnProperty(name) || 228 | providerCache.hasOwnProperty(name + 'Provider'); 229 | }, 230 | get: getService, 231 | annotate: annotate, 232 | invoke: invoke, 233 | instantiate: instantiate 234 | }; 235 | } 236 | ``` 237 | 238 | get, invoke 和 instantiate 方法引用 createInternalInjector 中的函数. annotate 引用 annotate 函数. has 方法检查依赖在本地的 cache 和 providerCache 中是否存在. 239 | 240 | 现在, 我们拥有 createInternalInjector, 我们可以用它创建两个 injector. provider injector 与 provider cache 工作, 它的回调函数将会返回一个异常, 让用户知道查找的依赖不存在. 241 | 242 | ```js 243 | function createInjector(modulesToLoad) { 244 | var providerCache = {}; 245 | var providerInjector = createInternalInjector(providerCache, function() { 246 | throw 'Unknown provider: '+path.join(' <- '); 247 | }); 248 | // ... 249 | } 250 | ``` 251 | 252 | instance injector 与相应的 instanceCache 工作. 它的回调函数用于查找 provider 并实例化依赖. 这是早先在 getService 中 else 分支的逻辑: 253 | 254 | ```js 255 | function createInjector(modulesToLoad) { 256 | var providerCache = {}; 257 | var providerInjector = createInternalInjector(providerCache, function() { 258 | throw 'Unknown provider: '+path.join(' <- '); 259 | }); 260 | var instanceCache = {}; 261 | var instanceInjector = createInternalInjector(instanceCache, function(name) { 262 | var provider = providerInjector.get(name + 'Provider'); 263 | return instanceInjector.invoke(provider.$get, provider); 264 | }); 265 | // ... 266 | } 267 | ``` 268 | 269 | 注意, 我们从 provider injector 获得 provider, 但是, 我们使用 instance injector 调用它(provider)的 $get 方法. 这是我们如何确保只有实例才能注入 $get 的方式. 270 | 271 | 现在, 实例化 provider, 我们使用 provider 的 instantiate 方法. 这种方式, provider 只能访问其他 provider 272 | 273 | ```js 274 | provider: function(key, provider) { 275 | if (_.isFunction(provider)) { 276 | provider = providerInjector.instantiate(provider); 277 | } 278 | providerCache[key + 'Provider'] = provider; 279 | } 280 | ``` 281 | 282 | constant 是一种特殊的情况. 我们将它的引用放入 provider cache 和 instance cache 中. 所以 constant 可以在任何地方被注入: 283 | 284 | ```js 285 | constant: function(key, value) { 286 | if (key === 'hasOwnProperty') { 287 | throw 'hasOwnProperty is not a valid constant name!'; 288 | } 289 | providerCache[key] = value; 290 | instanceCache[key] = value; 291 | }, 292 | ``` 293 | 294 | 最后, 返回调用 createInjector 的结果: 295 | 296 | ```js 297 | function createInjector(modulesToLoad, strictDi) { 298 | // ... 299 | return instanceInjector; 300 | } 301 | ``` 302 | 303 | 下面是 createInjector 的完整实现: 304 | 305 | ```js 306 | // 暂时省略 307 | ``` 308 | 309 | 我们实现了两种不同阶段的依赖注入: 310 | 311 | 1. provider 的注入发生在注册时, 之后在 providerCache 中不会再有任何改变. 312 | 2. instance 的注入发生在运行时, 当某人调用 injector 的外部 API. instanceCache 会被实例化的依赖填充, 实例化发生在 instanceInjector 的回调函数中. 313 | -------------------------------------------------------------------------------- /chapter12/Unshifting-Constants-in-The-Invoke-Queue.md: -------------------------------------------------------------------------------- 1 | ### 将 constant 放在 invoke queue 最前面 2 | 3 | 正如之前所看到的, 实例构建的延迟是一个很好的特性, 这样应用开发者不必再按照它们依赖的顺序去注册应用组件. 你可以在 B 之后注册 A, 即使 A 依赖 B. 4 | 5 | 对于 provider 构造函数, 就没有那么自由了. 当 provider 被注册时, provider 的构造函数被调用, 如果 BProvider 依赖 AProvider, 那么 A 需要在 B 之前注册. 6 | 7 | 对于 constant, Angular 确实帮了你一些. constant 将永远被注册在最前面, 所以你可以有一个依赖之后注册的 constant 的 provider. 8 | 9 | ```js 10 | it('registers constants frst to make them available to providers', function() { 11 | var module = window.angular.module('myModule', []); 12 | module.provider('a', function AProvider(b) { 13 | this.$get = function() { return b; }; 14 | }); 15 | 16 | module.constant('b', 42); 17 | var injector = createInjector(['myModule']); 18 | expect(injector.get('a')).toBe(42); 19 | }); 20 | ``` 21 | 22 | 当 constant 被注册在一个模块上时, 模块加载器总是将它们添加 invoke queue 的前面. 这是它们为何被最先注册的原因. 这是 Angular 对队列的安全排序, 因为 constant 不会依赖任何其它东西. 23 | 24 | 如何工作呢? 模块加载器的 invokeLater 函数需要一个可选参数去制定数组使用哪种方法添加队列元素. 默认是 push, 它会添加元素到队列的最后面: 25 | 26 | ```js 27 | var invokeLater = function(method, arrayMethod) { 28 | return function() { 29 | invokeQueue[arrayMethod || 'push']([method, arguments]); 30 | return moduleInstance; 31 | }; 32 | }; 33 | ``` 34 | 35 | constant 的注册会使用 unshift 覆盖默认的 push 方法, 但是 provider 的注册使用默认的 push 方法. 36 | 37 | ```js 38 | var moduleInstance = { 39 | name: name, 40 | requires: requires, 41 | constant: invokeLater('constant', 'unshift'), 42 | provider: invokeLater('provider'), 43 | _invokeQueue: invokeQueue 44 | }; 45 | ``` 46 | -------------------------------------------------------------------------------- /chapter16/Creating-The-$compile-Provider.md: -------------------------------------------------------------------------------- 1 | ### 创建 $compile Provider 2 | 3 | 指令的编译发生是因为使用一个叫做 $compile 的函数. 它和 $rootScope, $parse, $q 和 $http 一样, 是由 injector 提供的内建的服务. 当我们使用 ng 模块创建一个 injector. 我们期望 $compile 已经有了. 4 | 5 | ```js 6 | it('sets up $compile', function() { 7 | publishExternalAPI(); 8 | var injector = createInjector(['ng']); 9 | expect(injector.has('$compile')).toBe(true); 10 | }); 11 | ``` 12 | 13 | $compile 在文件 compile.js 中被定义为一个 provider. 14 | 15 | ```js 16 | function $CompileProvoder() { 17 | this.$get = function() { 18 | 19 | }; 20 | } 21 | 22 | module.exports = $CompileProvoder; 23 | ``` 24 | 25 | 我们在 angular_public.js 中的 ng 模块中引入 $compile, 就像引入其他 service 一样: 26 | 27 | ```js 28 | function publishExternalAPI() { 29 | setupModuleLoader(window); 30 | var ngModule = angular.module('ng', []); 31 | ngModule.provider('$flter', require('./flter')); 32 | ngModule.provider('$parse', require('./parse')); 33 | ngModule.provider('$rootScope', require('./scope')); 34 | ngModule.provider('$q', require('./q').$QProvider); 35 | ngModule.provider('$$q', require('./q').$$QProvider); 36 | ngModule.provider('$httpBackend', require('./http_backend')); 37 | ngModule.provider('$http', require('./http').$HttpProvider); 38 | ngModule.provider('$httpParamSerializer', 39 | require('./http').$HttpParamSerializerProvider); 40 | ngModule.provider('$httpParamSerializerJQLike', 41 | require('./http').$HttpParamSerializerJQLikeProvider); 42 | ngModule.provider('$compile', require('./compile')); 43 | } 44 | ``` -------------------------------------------------------------------------------- /chapter2/$apply-Integrating-External-Code-With-The-Digest-Cycle.md: -------------------------------------------------------------------------------- 1 | ### $apply - 使用脏检查来整合外部代码引起的数据变化 2 | 大概,Scope 所拥有的方法中,最为著名的就是 $apply 了,它被认为是向 angular 中整合由外部行为导致的数据变化的标准方法。 3 | 4 | 关于这样做的原因,下面的内容将是一个很好的解释: 5 | 6 | $apply 方法接收一个函数作为参数,首先使用 $eval 来调用这个函数,接着通过调用 $digest 来发起一轮脏检查循环,下面是一个针对这个过程的测试用例: 7 | 8 | ```js 9 | // test / scope_spec.js 10 | describe('$apply', function () { 11 | var scope; 12 | beforeEach(function () { 13 | scope = new Scope(); 14 | }); 15 | it('executes the given function and starts the digest', function () { 16 | scope.aValue = 'someValue'; 17 | scope.counter = 0; 18 | scope.$watch( 19 | function (scope) { 20 | return scope.aValue; 21 | }, 22 | function (newValue, oldValue, scope) { 23 | scope.counter++; 24 | } 25 | ); 26 | scope.$digest(); 27 | expect(scope.counter).toBe(1); 28 | scope.$apply(function (scope) { 29 | scope.aValue = 'someOtherValue'; 30 | }); 31 | expect(scope.counter).toBe(2); 32 | }); 33 | }); 34 | ``` 35 | 上面的用例中,我们针对 scope.aValue 设立了一个 watcher,并且在对应的 listener 函数中通过 scope.counter 进行了计数。我们期望当 $apply 方法被调用时,这个监听能立刻被触发。 36 | 37 | 为了使我们的测试用例通过,我们可以对 $apply 做一个简单的实现: 38 | 39 | ```js 40 | // src/scope.js 41 | Scope.prototype.$apply = function(expr) { 42 | try { 43 | return this.$eval(expr); 44 | } finally { 45 | this.$digest(); 46 | } 47 | }; 48 | ``` 49 | 在 finally 区域中我们调用了 $digest 方法,这样做可以确保,即使当你所执行的 expr 发生错误并抛出异常时,脏检查依然会被发起。 50 | 51 | 当我们在 angular 没有掌控的范围内运行了一段代码,这些代码恰恰可能会引起 Scope 的变化,这时,只要我们使用 $apply 来“包裹”着运行该段代码,就可以保证所有发生了变化的数据所对应的监听被触发。 52 | 通常,人们口中所说的,使用 $apply 将代码的运行整合到“Angular 的生命周期中”,本质上来说正是这样的逻辑,并没有比这深奥很多。 53 | -------------------------------------------------------------------------------- /chapter2/$eval-Evaluating-Code-In-The-Context-of-A-Scope.md: -------------------------------------------------------------------------------- 1 | ### $eval - 基于特定的Scope解析并运行表达式 2 | 3 | 在 angular 中,可以通过多种方式在特定的 Scope 中执行一段 code 。其中最简单的方式就是 $eval ,这个方法接收一个 function 作为参数,然后立刻给该函数传入这个 scope 本身来执行它。通过 $eval 调用的返回值就是该函数执行后的返回值。同时 $eval 也接受第二个可选参数并将其简单的传入该函数中。 4 | 5 | 以下几个单元测试向我们展示了如何使用 $eval ,我们可以另起一个新的 describe 块来书写这些用例。 6 | 7 | ```js 8 | // test/scope_spec.js 9 | describe('$eval', function () { 10 | var scope; 11 | beforeEach(function () { 12 | scope = new Scope(); 13 | }); 14 | it('executes $evaled function and returns result', function () { 15 | scope.aValue = 42; 16 | var result = scope.$eval(function (scope) { 17 | return scope.aValue; 18 | }); 19 | expect(result).toBe(42); 20 | }); 21 | it('passes the second $eval argument straight through', function () { 22 | scope.aValue = 42; 23 | var result = scope.$eval(function (scope, arg) { 24 | return scope.aValue + arg; 25 | }, 2); 26 | expect(result).toBe(44); 27 | }); 28 | }); 29 | ``` 30 | 31 | $eval 的实现也很简单直接: 32 | 33 | ```js 34 | // src/scope.js 35 | Scope.prototype.$eval = function(expr, locals) { 36 | return expr(this, locals); 37 | }; 38 | ``` 39 | 40 | 我们为什么要多此一举的通过 $eval 来执行函数而不是直接调用它呢?有人可能会说,$eval 将函数的执行与特定的 Scope 连接起来。并且,$eval 还是 $apply 构成的一部分,后者我们会在之后的章节中遇到。 41 | 然而,更为重要的是,$eval 最引人关注的用途在于它使我们不只是关注原生的函数,而且能够关注表达式 - expressions 这一概念。 42 | 比如当我们使用 $watch 时,我们就可以直接传递一个字符串的表达式给 $eval,它会帮助我们在特定的Scope中解析并且执行该表达式,针对这一功能,我们会在本书的第二部分来实现。 -------------------------------------------------------------------------------- /chapter2/$evalAsync-Deferred-Execution.md: -------------------------------------------------------------------------------- 1 | ### $evalAsync - 推迟代码的执行 2 | 3 | 在 JavaScript 的世界中,存在一个很常见的举动:推迟一段代码的执行,也就是说,将该段代码的执行推迟到将来的某个时间点,而这个时间点正是我们当前逻辑结束的时刻。最常见的做法是使用0 或者很短的时间间隔来调用 setTimeout(); 4 | 5 | 这种方法在 angular 中也是适用的,很多人偏爱使用 $timeout service 来实现延迟的目的,除此之外, $timeout service 还将代码的延迟执行与脏检查的过程通过 $apply 整合在了一起。 6 | 7 | 但是,angular 中还存在另一种延迟代码执行的方法,就是 Scope 对象拥有的 $evalAsync 方法。它接收一个 function 作为参数并安排其在正在进行的或者即将进行的脏检查过程中被执行。比如,你可以在一个监听器中推迟一段代码的执行,你会发现,即使该段代码被推迟执行了,它仍然会在当前的脏检查中被调用。使用 $evalAsync 的方式通常是比通过 0 调用 $timeout 更为可取的,其原因在于浏览器的 event loop 机制。 8 | 9 | 当你使用 $timeout 来安排一段代码的执行,意味着你将该段代码的执行控制交给了浏览器,让浏览器来决定什么时间点来执行该段代码。然而,在执行你的安排之前,浏览器可能会选择执行其他的工作,诸如渲染界面,执行点击事件以及 Ajax 回调操作。 10 | 相比较,$evalAsync 方法对延迟任务在什么时间点执行控制得更为精确, 通过其安排的延迟任务始终会在脏检查的过程中被执行,在浏览器选择执行其他工作执行之前,它保障了延迟任务的执行。 11 | 12 | 当你想要避免不必要的渲染时,$timeout 与 $evalAsync 这两种方式的区别就显得十分有意义了。当 DOM 的改变不稳定,立刻会被覆盖时,为什么还要浏览器去渲染这些改变呢,显然是没有意义的。 13 | 14 | 下面是一个关于 $evalAsync 的单元测试: 15 | ```js 16 | // test/scope_spec.js 17 | describe('$evalAsync', function() { 18 | var scope; 19 | beforeEach(function() { 20 | scope = new Scope(); 21 | }); 22 | it('executes given function later in the same cycle', function() { 23 | scope.aValue = [1, 2, 3]; 24 | scope.asyncEvaluated = false; 25 | scope.asyncEvaluatedImmediately = false; 26 | scope.$watch( 27 | function(scope) { return scope.aValue; }, 28 | function(newValue, oldValue, scope) { 29 | scope.$evalAsync(function(scope) { 30 | scope.asyncEvaluated = true; 31 | }); 32 | scope.asyncEvaluatedImmediately = scope.asyncEvaluated; 33 | } 34 | ); 35 | scope.$digest(); 36 | expect(scope.asyncEvaluated).toBe(true); 37 | expect(scope.asyncEvaluatedImmediately).toBe(false); 38 | }); 39 | }); 40 | ``` 41 | 上面的测试中,我们在监听器中调用了 $evalAsync,然后验证该任务是否会在接下来的脏检查中被执行,并且是在该监听器执行完成之后。 42 | 43 | 要实现这一点,首先,需要一种方式来存储经过 $evalAsync 安排的任务,我们可以在 Scope 的构造函数中使用一个数组来实现: 44 | ```js 45 | // src/scope.js 46 | function Scope() { 47 | this.$$watchers = []; 48 | this.$$lastDirtyWatch = null; 49 | this.$$asyncQueue = []; 50 | } 51 | ``` 52 | 接着,我们来实现 $evalAsync,将需要延迟执行的任务添加到这个队列中: 53 | ```js 54 | //src/scope.js 55 | Scope.prototype.$evalAsync = function(expr) { 56 | this.$$asyncQueue.push({scope: this, expression: expr}); 57 | }; 58 | ``` 59 | 目前我们已经存储好了等待运行的任务,但是还没有真正的运行它们。在脏检查的过程中,我们将实现这一动作: 60 | 首先,我们需要取出队列中所有等待运行的任务并且通过 $eval 的方式执行它们: 61 | ```js 62 | // src/scope.js 63 | Scope.prototype.$digest = function() { 64 | var ttl = 10; 65 | var dirty; 66 | this.$$lastDirtyWatch = null; 67 | do { 68 | while (this.$$asyncQueue.length) { 69 | var asyncTask = this.$$asyncQueue.shift(); 70 | asyncTask.scope.$eval(asyncTask.expression); 71 | } 72 | dirty = this.$$digestOnce(); 73 | if (dirty && !(ttl--)) { 74 | throw '10 digest iterations reached'; 75 | } 76 | } while (dirty); 77 | }; 78 | ``` 79 | 这样的实现过程保证了,当你在脏检查的过程中安排了一个延迟执行的任务,这个任务会在这个脏检查过程中被执行,这样正满足了我们的测试用例。 -------------------------------------------------------------------------------- /chapter2/Scheduling-$evalAsync-from-Watch-Functions.md: -------------------------------------------------------------------------------- 1 | ### 在 Watch 函数中使用 $evalAsync 2 | 在上一小节中,当我们在 listener 函数中通过 $evalAsync 来延迟任务的执行时,该任务会在所处的脏检查循环中被执行。试想一下,当我们在 watch 函数中使用 $evalAsync 来延迟任务时,会造成什么结果? 3 | 当然,这样做本身是不应该的,因为 watch 函数本身就应该是没有副作用,没有额外的影响的。但事实上,人们仍有可能去这样做,所以,我们要确保这样做不会对脏检查过程造成大的破坏。 4 | 5 | 想象一下,我们在 watch 函数中只调用了一次 $evalAsync,那么我们目前的代码逻辑是没有问题的,也意味着下面的测试用例会通过: 6 | ```js 7 | // test/scope_spec.js 8 | it('executes $evalAsynced functions added by watch functions', function() { 9 | scope.aValue = [1, 2, 3]; 10 | scope.asyncEvaluated = false; 11 | scope.$watch( 12 | function(scope) { 13 | if (!scope.asyncEvaluated) { 14 | scope.$evalAsync(function(scope) { 15 | scope.asyncEvaluated = true; 16 | }); 17 | } 18 | return scope.aValue; 19 | }, 20 | function(newValue, oldValue, scope) { } 21 | ); 22 | scope.$digest(); 23 | expect(scope.asyncEvaluated).toBe(true); 24 | }); 25 | ``` 26 | 那么,问题出在哪里呢?就目前来说,只要至少存在一个“脏”的 watch,我们便会持续进行脏检查过程。上面的测试中,当我们进行对 watcher 进行第一次遍历时,watch 函数返回的 scope.aValue 检测到是“脏”的,导致我们需要进入第二次遍历,而在第二次遍历中,我们正好执行了我们在第一次中通过 $evalAsync 安排的任务。 27 | 但是,当我们调用 $evalAsync 时,整个脏检查状态是“干净”的呢,没有“脏”的 watch 呢? 28 | ```js 29 | // test/scope_spec.js 30 | it('executes $evalAsynced functions even when not dirty', function () { 31 | scope.aValue = [1, 2, 3]; 32 | scope.asyncEvaluatedTimes = 0; 33 | scope.$watch( 34 | function (scope) { 35 | if (scope.asyncEvaluatedTimes < 2) { 36 | scope.$evalAsync(function (scope) { 37 | scope.asyncEvaluatedTimes++; 38 | }); 39 | } 40 | return scope.aValue; 41 | }, 42 | function (newValue, oldValue, scope) { 43 | } 44 | ); 45 | scope.$digest(); 46 | expect(scope.asyncEvaluatedTimes).toBe(2); 47 | }); 48 | ``` 49 | 在这个测试用,我们在 watch 函数中调用了两次 $evalAsync,在第二次调用时,该 watch 函数检测结果是“干净”因为 scope.aValue 的值并没有改变,这也意味着第二次 $evalAsync 安排的任务不会再被执行了,因为脏检查在第二次就终止了。 50 | 相比在下一个新的脏检查中执行该任务,我们更期望该任务在这次脏检查中就被执行,这就意味着,我们需要去改变脏检查的结束条件,在结束条件中检测是否还有安排了但未执行的 async queue 任务。 51 | ```js 52 | // src/scope.js 53 | Scope.prototype.$digest = function() { 54 | var ttl = 10; 55 | var dirty; 56 | this.$$lastDirtyWatch = null; 57 | do { 58 | while (this.$$asyncQueue.length) { 59 | var asyncTask = this.$$asyncQueue.shift(); 60 | asyncTask.scope.$eval(asyncTask.expression); 61 | } 62 | dirty = this.$$digestOnce(); 63 | if (dirty && !(ttl--)) { 64 | throw '10 digest iterations reached'; 65 | } 66 | } while (dirty || this.$$asyncQueue.length); 67 | }; 68 | ``` 69 | 这使得上一个测试能够通过,但是事实上,我们又遇到了另一个问题,倘若我们在一个 watch 函数中多次调用 $evalAsync 来安排任务呢?那会发生什么?我们希望这样会触发脏检查的限制来并且停止脏检查,但目前的逻辑上并不是这样: 70 | ```js 71 | // test/scope_spec.js 72 | it('eventually halts $evalAsyncs added by watches', function () { 73 | scope.aValue = [1, 2, 3]; 74 | scope.$watch( 75 | function (scope) { 76 | scope.$evalAsync(function (scope) { 77 | }); 78 | return scope.aValue; 79 | }, function (newValue, oldValue, scope) { 80 | } 81 | ); 82 | expect(function () { 83 | scope.$digest(); 84 | }).toThrow(); 85 | }); 86 | ``` 87 | 上述的测试代码会一直执行下去,永远不会停止,因为脏检查的 while 循环永远不会停下来,现在我们需要在脏检查的终止限制中增加对 async queue 的检查来避免这种情况的发生。 88 | ```js 89 | // src/scope.js 90 | Scope.prototype.$digest = function() { 91 | var ttl = 10; 92 | var dirty; 93 | this.$$lastDirtyWatch = null; 94 | do { 95 | while (this.$$asyncQueue.length) { 96 | var asyncTask = this.$$asyncQueue.shift(); 97 | asyncTask.scope.$eval(asyncTask.expression); 98 | } 99 | dirty = this.$$digestOnce(); 100 | if ((dirty || this.$$asyncQueue.length) && !(ttl--)) { 101 | throw '10 digest iterations reached'; 102 | } 103 | } while (dirty || this.$$asyncQueue.length); 104 | }; 105 | ``` 106 | 现在我们可以保证,无论检查结果是否是“脏”以及在是否在 async queue 中还有未执行的任务,在脏检查达到我们的限制条件时,它就会终止。 -------------------------------------------------------------------------------- /chapter3/Attribute-Shadowing.md: -------------------------------------------------------------------------------- 1 | ### 属性遮蔽 2 | 3 | Scope 继承的一个方面, 属性遮蔽, 常常绊倒 Angular 新手. 然而这却是使用 JavaScript 原型链的直接结果, 它非常值得讨论. 4 | 5 | 从现有的测试用例中可以看的非常清楚, 当你从 Scope 上读一个属性, 如果属性不在当前 Scope, 它会通过原型链往上查找, 从父 Scope 上查找, 直到找到为止. 6 | 7 | 还有, 当你在一个 Scope 上添加一个属性, 这个属性在该 Scope 和它的子 Scope 上都是可用的, 它的父 Scope 上不可用. 8 | 9 | 属性遮蔽的关键实现是可以在子 Scope 上使用相同的名称的属性: 10 | 11 | ```js 12 | it('shadows a parents property with the same name', function() { 13 | var parent = new Scope(); 14 | var child = parent.$new(); 15 | parent.name = 'Joe'; 16 | child.name = 'Jill'; 17 | expect(child.name).toBe('Jill'); 18 | expect(parent.name).toBe('Joe'); 19 | }); 20 | ``` 21 | 22 | 当我们在子 Scope 中添加一个名称已经存在于父 Scope 中的属性时, 不会对父 Scope 产生任何改变. 实际上, 我们在 Scope 链上有两个完全不同的属性, 只是名字都叫 name 而已. 23 | 这就是所谓的遮蔽, 从子 Scope 的角度来看, 父 Scope 的 name 属性, 被子 Scope 的 name 属性遮蔽了. 24 | 25 | 那么,我们如何改变父 Scope 上属性的状态呢? 很简单, 将父 Scope 上的属性用对象包裹起来. 26 | 27 | ```js 28 | it('does not shadow members of parent scopes attributes', function() { 29 | var parent = new Scope(); 30 | var child = parent.$new(); 31 | parent.user = {name: 'Joe'}; 32 | child.user.name = 'Jill'; 33 | expect(child.user.name).toBe('Jill'); 34 | expect(parent.user.name).toBe('Jill'); 35 | }); 36 | ``` 37 | 38 | 这种方式工作的原因是, 我们并没有对子 Scope 上做任何赋值. 我们仅仅从父 Scope 上读取 user 对象和在对象内赋值, 两个 Scope 使用了同一个 user 对象的引用. user 对象只是一个 JavaScript 对象, 与 Scope 继承没有任何关系. -------------------------------------------------------------------------------- /chapter3/Destroying-Scope.md: -------------------------------------------------------------------------------- 1 | ### 销毁 Scope 2 | 3 | 一个典型的 Angular 应用的生命周期, 页面元素变化是通过不同的视图和数据呈现给用户的. 这意味着 Scope 层级扩大和收缩发生在应用的生命周期中. 4 | 5 | 在我们的实现中, 可以创建子 Scope, 但是却没有一种机制去删除它们. 当考虑性能时, 一个日益扩大的 Scope 层级是非常不合适的. 因此我们需要一种方式销毁 Scope. 6 | 7 | 销毁一个 Scope 意味着, Scope 上的所有监听器要被删除并且它自己也要从父 Scope 上的 $$children 属性内删除. 该 Scope 不在需要再被任何地方引用, 它在某个时间会被 JavaScript 垃圾回收器回收. 8 | 9 | 我们为 Scope 添加一个叫做 $destroy 的方法, 来实现销毁操作: 10 | 11 | ```js 12 | it('is no longer digested when $destroy has been called', function() { 13 | var parent = new Scope(); 14 | var child = parent.$new(); 15 | child.aValue = [1, 2, 3]; 16 | child.counter = 0; 17 | child.$watch( 18 | function(scope) { return scope.aValue; }, 19 | function(newValue, oldValue, scope) { 20 | scope.counter++; 21 | }, 22 | true 23 | ); 24 | parent.$digest(); 25 | expect(child.counter).toBe(1); 26 | child.aValue.push(4); 27 | parent.$digest(); 28 | expect(child.counter).toBe(2); 29 | child.$destroy(); 30 | child.aValue.push(5); 31 | parent.$digest(); 32 | expect(child.counter).toBe(2); 33 | }); 34 | ``` 35 | 36 | 在 $destroy 中, 我们需要当前 Scope 的父 Scope 的引用, 目前还没有, 因此让我们在 $new 中添加一个. 当子 Scope 被创建时, 它的 $parent 属性直接指向父 Scope 的引用. 37 | 38 | ```js 39 | Scope.prototype.$new = function(isolated, parent) { 40 | var child; 41 | parent = parent || this; 42 | if (isolated) { 43 | child = new Scope(); 44 | child.$root = parent.$root; 45 | child.$$asyncQueue = parent.$$asyncQueue; 46 | child.$$postDigestQueue = parent.$$postDigestQueue; 47 | child.$$applyAsyncQueue = parent.$$applyAsyncQueue; 48 | } else { 49 | var ChildScope = function() { }; 50 | ChildScope.prototype = this; 51 | child = new ChildScope(); 52 | } 53 | parent.$$children.push(child); 54 | child.$$watchers = []; 55 | child.$$children = []; 56 | child.$parent = parent; 57 | return child; 58 | }; 59 | ``` 60 | 61 | 现在, 来实现 $destroy 方法. 当前 Scope 在其父 Scope 的 $$children 数组找到自己, 并删除自己, 同时确保自己不是 Root Scope 并且有一个父 Scope. 当然, 还要删除注册在自己上的所有监听器: 62 | 63 | ```js 64 | Scope.prototype.$destroy = function() { 65 | if (this.$parent) { 66 | var siblings = this.$parent.$$children; 67 | var indexOfThis = siblings.indexOf(this); 68 | if (indexOfThis >= 0) { 69 | siblings.splice(indexOfThis, 1); 70 | } 71 | } 72 | this.$$watchers = null; 73 | }; 74 | ``` 75 | -------------------------------------------------------------------------------- /chapter3/Digesting-The-Whole-Tree-from-$apply-$evalAsync-and-$applyAsync.md: -------------------------------------------------------------------------------- 1 | ### $apply, $evalAsync 和 $applyAsync 方法需要 digest 整个 Scope 树 2 | 3 | 正如我们上一小节看到的, $digest 的工作方式, 仅从当前 Scope 向下遍历. 这不是 $apply 的方式. 当我们在 Angular 中调用 $apply 时, 直接从 Root Scope 开始 digest 整个 Scope 层级. 4 | 目前, 我们的实现还没有这样做. 5 | 6 | ```js 7 | it('digests from root on $apply', function() { 8 | var parent = new Scope(); 9 | var child = parent.$new(); 10 | var child2 = child.$new(); 11 | parent.aValue = 'abc'; 12 | parent.counter = 0; 13 | parent.$watch( 14 | function(scope) { return scope.aValue; }, 15 | function(newValue, oldValue, scope) { 16 | scope.counter++; 17 | } 18 | ); 19 | child2.$apply(function(scope) { }); 20 | expect(parent.counter).toBe(1); 21 | }); 22 | ``` 23 | 基于当前的实现, 当我们在子 Scope 上调用 $apply 时, 不会触发父 Scope 上的监听器. 24 | 25 | 为了使测试通过, 首先, 为所有的 Scope 添加一个 Root Scope 的引用. 虽然我们可以通过原型链找到 Root Scope, 但是通过暴露一个 $root 变量访问会直接和方便. 我们在 Root Scope 构造函数添加一个变量. 26 | 27 | ```js 28 | function Scope() { 29 | this.$$watchers = []; 30 | this.$$lastDirtyWatch = null; 31 | this.$$asyncQueue = []; 32 | this.$$applyAsyncQueue = []; 33 | this.$$applyAsyncId = null; 34 | this.$$postDigestQueue = []; 35 | this.$root = this; 36 | this.$$children = []; 37 | this.$$phase = null; 38 | } 39 | ``` 40 | 41 | 这个单独的变量使 $root 在层级中的每个 Scope 中可用, 这样感谢原型继承链. 42 | 43 | 接下来, 在 $apply 方法中用在 Root Scope 直接调用 $digest 替换 在当前 Scope 上调用 $digest. 44 | 45 | ```js 46 | Scope.prototype.$apply = function(expr) { 47 | try { 48 | this.$beginPhase('$apply'); 49 | return this.$eval(expr); 50 | } finally { 51 | this.$clearPhase(); 52 | this.$root.$digest(); 53 | } 54 | }; 55 | ``` 56 | 57 | 注意: 因为调用的是 this 上 $eval, 所以对给定函数计算的值是当前 Scope 上的, 而不是 Root Scope 上的. 对 $digest 我们却想要从 Root Scope 往下运行. 58 | 59 | 事实上, 在 $apply 中 digest 一直都是从 Root Scope 开始的, 使用这种方式处理的原因就是它是在 Angular digest 循环中集成外部代码的首选方案: 如果你不能准确的知道是在那个 Scope 上做的改变, 最安全的方式就是执行所有的 Scope 上的 $digest 60 | 61 | 很显然, 整个 Angular 应用有且仅有一个 Root Scope, 执行 $apply 方法会引起整个应用中每个 Scope 上的每个监听器执行. 掌握了 $digest 和 $apply 的不同, 当你需要做性能优化时, 有时需要用 $digest 替换 $apply. 62 | 63 | 通过上面修改, 已经覆盖了 $digest 和 $apply 方法, 还有 $applyAsync 和 $evalAsync 方法需要修改, 先看一个测试: 64 | 65 | ```js 66 | it('schedules a digest from root on $evalAsync', function(done) { 67 | var parent = new Scope(); 68 | var child = parent.$new(); 69 | var child2 = child.$new(); 70 | parent.aValue = 'abc'; 71 | parent.counter = 0; 72 | 73 | parent.$watch( 74 | function(scope) { return scope.aValue; }, 75 | function(newValue, oldValue, scope) { 76 | scope.counter++; 77 | } 78 | ); 79 | 80 | child2.$evalAsync(function(scope) { }); 81 | 82 | setTimeout(function() { 83 | expect(parent.counter).toBe(1); 84 | done(); 85 | }, 50); 86 | }); 87 | ``` 88 | 89 | 这个测试与之前的很相似, 我们检查在 Scope 上调用 $evalAsync 方法, 是否会引起 Root Scope 上的监听器执行. 90 | 91 | 因为每个 Scope 已经有了 Root Scope 的引用, 所以修改 $evalAsync 很容易. 92 | 93 | ```js 94 | Scope.prototype.$evalAsync = function(expr) { 95 | var self = this; 96 | if (!self.$$phase && !self.$$asyncQueue.length) { 97 | setTimeout(function() { 98 | if (self.$$asyncQueue.length) { 99 | self.$root.$digest(); 100 | } 101 | }, 0); 102 | } 103 | this.$$asyncQueue.push({scope: this, expression: expr}); 104 | }; 105 | ``` 106 | 107 | 在缩短回路优化中, 我们应当一直使用 Root Scope 上的 $$lastDirtyWatch, 无论哪个 Scope 上 $digest 方法被调用. 108 | 109 | ```js 110 | Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 111 | var self = this; 112 | var watcher = { 113 | watchFn: watchFn, 114 | listenerFn: listenerFn || function() { }, 115 | last: initWatchVal, 116 | valueEq: !!valueEq 117 | }; 118 | this.$$watchers.unshift(watcher); 119 | this.$root.$$lastDirtyWatch = null; 120 | return function() { 121 | var index = self.$$watchers.indexOf(watcher); 122 | if (index >= 0) { 123 | self.$$watchers.splice(index, 1); 124 | self.$root.$$lastDirtyWatch = null; 125 | } 126 | }; 127 | }; 128 | ``` 129 | 130 | 同样的 $digest 也应该这样做. 131 | 132 | ```js 133 | Scope.prototype.$digest = function() { 134 | var ttl = 10; 135 | var dirty; 136 | this.$root.$$lastDirtyWatch = null; 137 | this.$beginPhase('$digest'); 138 | if (this.$$applyAsyncId) { 139 | clearTimeout(this.$$applyAsyncId); 140 | this.$$ ushApplyAsync(); 141 | } 142 | do { 143 | while (this.$$asyncQueue.length) { 144 | try { 145 | var asyncTask = this.$$asyncQueue.shift(); 146 | asyncTask.scope.$eval(asyncTask.expression); 147 | } catch (e) { 148 | console.error(e); 149 | } 150 | } 151 | dirty = this.$$digestOnce(); 152 | if ((dirty || this.$$asyncQueue.length) && !(ttl--)) { 153 | throw '10 digest iterations reached'; 154 | } 155 | } while (dirty || this.$$asyncQueue.length); 156 | this.$clearPhase(); 157 | while (this.$$postDigestQueue.length) { 158 | try { 159 | this.$$postDigestQueue.shift()(); 160 | } catch (e) { 161 | console.error(e); 162 | } 163 | } 164 | }; 165 | ``` 166 | 167 | 最后, 在 $$digestOnce 也做这样处理: 168 | 169 | ```js 170 | Scope.prototype.$$digestOnce = function() { 171 | var dirty; 172 | this.$$everyScope(function(scope) { 173 | var newValue, oldValue; 174 | _.forEachRight(scope.$$watchers, function(watcher) { 175 | try { 176 | if (watcher) { 177 | newValue = watcher.watchFn(scope); 178 | oldValue = watcher.last; 179 | if (!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) { 180 | scope.$root.$$lastDirtyWatch = watcher; 181 | watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 182 | watcher.listenerFn(newValue, 183 | (oldValue === initWatchVal ? newValue : oldValue), 184 | scope); 185 | dirty = true; 186 | } else if (scope.$root.$$lastDirtyWatch === watcher) { 187 | dirty = false; 188 | return false; 189 | } 190 | } 191 | } catch (e) { 192 | console.error(e); 193 | } 194 | }); 195 | return dirty !== false; 196 | }); 197 | return dirty; 198 | }; 199 | ``` -------------------------------------------------------------------------------- /chapter3/Isolated-Scopes.md: -------------------------------------------------------------------------------- 1 | ### 隔离 Scope 2 | 3 | 我们已经看到了, 父 Scope 和 子 Scope 之间的关系是非常亲密的, 当引入原型继承的时候. 无论父 Scope 上的什么属性, 子 Scope 都可以访问. 如果这些是对象或数组属性的话, 子 Scope 也可以修改它们的内容. 4 | 5 | 有时候, 我们不希望它们非常亲密, 子 Scope 只是 父 Scope 层级中的一部分, 并不能访问父 Scope 上的任何属性. 这就是隔离 Scope 的作用. 6 | 7 | 隔离 Scope 背后的想法很简单: 我们就像之前一样, 创建一个 Scope 作为父 Scope 中的一部分, 只是不再从父 Scope 做原型继承. 这相当于切断(隔离)来自父 Scope 原型链. 8 | 9 | 隔离 Scope 可以通过对 $new 方法传递一个布尔参数创建, 当这个参数为 true 时, 将会创建一个隔离 Scope, 当它为 false (或省略)时, 原型继承将会被使用. 10 | 11 | 当 Scope 是隔离的时候, 它将无法访问它父 Scope 上的属性: 12 | 13 | ```js 14 | it('does not have access to parent attributes when isolated', function() { 15 | var parent = new Scope(); 16 | var child = parent.$new(true); 17 | parent.aValue = 'abc'; 18 | expect(child.aValue).toBeUnde ned(); 19 | }); 20 | ``` 21 | 22 | 因为无法访问父 Scope 的属性, 因此也无法 watch 这些属性. 23 | 24 | ```js 25 | it('cannot watch parent attributes when isolated', function() { 26 | var parent = new Scope(); 27 | var child = parent.$new(true); 28 | parent.aValue = 'abc'; 29 | child.$watch( 30 | function(scope) { return scope.aValue; }, 31 | function(newValue, oldValue, scope) { 32 | scope.aValueWas = newValue; 33 | } 34 | ); 35 | child.$digest(); 36 | expect(child.aValueWas).toBeUndefined(); 37 | }); 38 | ``` 39 | 40 | Scope 是否隔离是在 $new 中设置的. 根据一个给定的 boolean 参数, 决定创建的子 Scope 是和目前的做法一样还是使用 Scope 构造器创建独立的 Scope. 41 | 42 | ```js 43 | Scope.prototype.$new = function(isolated) { 44 | var child; 45 | if (isolated) { 46 | child = new Scope(); 47 | } else { 48 | var ChildScope = function() { }; 49 | ChildScope.prototype = this; 50 | child = new ChildScope(); 51 | } 52 | this.$$children.push(child); 53 | child.$$watchers = []; 54 | child.$$children = []; 55 | return child; 56 | }; 57 | ``` 58 | 59 | 如果你使用过 Angular 指令中的隔离 Scope, 你就会知道隔离 Scope 并没有完全与它的父 Scope 隔离, 你可以显示地在父 Scope 上定义一个 Map 去从父 Scope 上获取对应的属性. 60 | 61 | 但是, 这个设计并未在 Scope 中实现, 它是指令实现的一部分. 62 | 63 | 因为破坏了原型继承链, 我们需要重新讨论 $digest, $apply, $evalAsync 和 $applyAsync 在之前章节的实现. 64 | 65 | 首先, 我们想让 $digest 运行当前 Scope 上的监听器并遍历它的所有子 Scope 上的监听器, 这个我们已经处理过了. 因为隔离 Scope 也被它们父 Scope 的 $$children 属性所包含, 所以下面的测试已经通过了: 66 | 67 | ```js 68 | it('digests its isolated children', function() { 69 | var parent = new Scope(); 70 | var child = parent.$new(true); 71 | child.aValue = 'abc'; 72 | child.$watch( 73 | function(scope) { return scope.aValue; }, 74 | function(newValue, oldValue, scope) { 75 | scope.aValueWas = newValue; 76 | } 77 | ); 78 | parent.$digest(); 79 | expect(child.aValueWas).toBe('abc'); 80 | }); 81 | ``` 82 | 83 | 这种情况下, $apply, $evalAsync 和 $applyAsync 就没有这么幸运了, 我们希望这些操作都是从 Root Scope 上开始 digest, 但是在中间层级的隔离 Scope 会破坏这种设定. 正如下面的两个失败的测试用例: 84 | 85 | ```js 86 | it('digests from root on $apply when isolated', function() { 87 | var parent = new Scope(); 88 | var child = parent.$new(true); 89 | var child2 = child.$new(); 90 | parent.aValue = 'abc'; 91 | parent.counter = 0; 92 | parent.$watch( 93 | function(scope) { return scope.aValue; }, 94 | function(newValue, oldValue, scope) { 95 | scope.counter++; 96 | } 97 | ); 98 | child2.$apply(function(scope) { }); 99 | expect(parent.counter).toBe(1); 100 | }); 101 | 102 | it('schedules a digest from root on $evalAsync when isolated', function(done) { 103 | var parent = new Scope(); 104 | var child = parent.$new(true); 105 | var child2 = child.$new(); 106 | parent.aValue = 'abc'; 107 | parent.counter = 0; 108 | parent.$watch( 109 | function(scope) { return scope.aValue; }, 110 | function(newValue, oldValue, scope) { 111 | scope.counter++; 112 | } 113 | ); 114 | child2.$evalAsync(function(scope) { }); 115 | setTimeout(function() { 116 | expect(parent.counter).toBe(1); 117 | done(); 118 | }, 50); 119 | }); 120 | ``` 121 | 122 | 两个测试用例失败的原因是, 我们依赖指向 Root Scope 的 $root 属性, 非隔离 Scope 可以从原型链上继承下来. 而隔离 Scope 却没有. 实际上, 因为我们使用 Scope 构造器创建隔离 Scope, 123 | 构造器提供了 $root 属性, 只不过每个隔离 Scope 的 $root 属性都指向它自己. 这并不是我们想要的. 124 | 125 | 简单的修复一下 $new : 126 | 127 | ```js 128 | Scope.prototype.$new = function(isolated) { 129 | var child; 130 | if (isolated) { 131 | child = new Scope(); 132 | child.$root = this.$root; 133 | } else { 134 | var ChildScope = function() { }; 135 | ChildScope.prototype = this; 136 | child = new ChildScope(); 137 | } 138 | this.$$children.push(child); 139 | child.$$watchers = []; 140 | child.$$children = []; 141 | return child; 142 | }; 143 | ``` 144 | 145 | 在这之前, 我们已经将一切关于继承的情况都覆盖了. 还有一件事, 我们必须要在隔离 Scope 中修复, 那就是我们在 $evalAsync, $applyAsync 和 $$postDigest 函数中使用的队列: 146 | 147 | - $$asyncQueue 148 | - $$postDigestQueue 149 | - $$applyAsyncQueue 150 | 151 | 对于它们, 我们并未做在子 Scope 或父 Scope 上任何额外的工作, 仅仅简单作为整个 Scope 层级上的任务队列而已. 152 | 153 | 对于非隔离 Scope: 无论在任何 Scope 上访问这个队列中一个, 我们访问的都是相同队列, 因为每个 Scope 都通过原型链继承了这些队列. 然而对这些隔离 Scope. 就如同之前的 $root 属性, 154 | $$asyncQueue, $$applyAsyncQueue 和 $$postDigestQueue 都被 Scope 构造器创建了, 只是都指向了它们自己. 155 | 156 | ```js 157 | it('executes $evalAsync functions on isolated scopes', function(done) { 158 | var parent = new Scope(); 159 | var child = parent.$new(true); 160 | child.$evalAsync(function(scope) { 161 | scope.didEvalAsync = true; 162 | }); 163 | setTimeout(function() { 164 | expect(child.didEvalAsync).toBe(true); 165 | done(); 166 | }, 50); 167 | }); 168 | 169 | it('executes $$postDigest functions on isolated scopes', function() { 170 | var parent = new Scope(); 171 | var child = parent.$new(true); 172 | child.$$postDigest(function() { 173 | child.didPostDigest = true; 174 | }); 175 | parent.$digest(); 176 | expect(child.didPostDigest).toBe(true); 177 | }); 178 | ``` 179 | 180 | 就和 $root 属性一样, 我们希望整个 Scope 层级中每个 Scope 都分享相同的队列, 不管它们是否是隔离 Scope. 如果 Scope 不是隔离的, 我们自动获得对应的队列, 如果是隔离的, 我们需要显示的赋值: 181 | 182 | ```js 183 | Scope.prototype.$new = function(isolated) { 184 | var child; 185 | if (isolated) { 186 | child = new Scope(); 187 | child.$root = this.$root; 188 | child.$$asyncQueue = this.$$asyncQueue; 189 | child.$$postDigestQueue = this.$$postDigestQueue; 190 | } else { 191 | var ChildScope = function() { }; 192 | ChildScope.prototype = this; 193 | child = new ChildScope(); 194 | } 195 | this.$$children.push(child); 196 | child.$$watchers = []; 197 | child.$$children = []; 198 | return child; 199 | }; 200 | ``` 201 | 202 | 对于 $$applyAsyncQueue, 问题有些不太一样: 因为清理队列是被 $$applyAsyncId 属性控制的, 并且现在整个 Scope 层级中的每个 Scope 可能会有这个属性的实例, 整个 $applyAsync 的目的, 就是合并 $apply 的调用. 203 | 204 | 如果我们调用子 Scope 上 $digest 方法, 父 Scope 上 $applyAsync 注册的函数应当被清理出队列并被执行. 但是当前的实现不能是下面的测试通过: 205 | 206 | ```js 207 | it("executes $applyAsync functions on isolated scopes", function() { 208 | var parent = new Scope(); 209 | var child = parent.$new(true); 210 | var applied = false; 211 | parent.$applyAsync(function() { 212 | applied = true; 213 | }); 214 | child.$digest(); 215 | expect(applied).toBe(true); 216 | }); 217 | ``` 218 | 首先, 我们应当在 scope 之间共享队列, 就像 $evalAsync 和 $postDigest 队列做的一样. 219 | 220 | ```js 221 | Scope.prototype.$new = function(isolated) { 222 | var child; 223 | if (isolated) { 224 | child = new Scope(); 225 | child.$root = this.$root; 226 | child.$$asyncQueue = this.$$asyncQueue; 227 | child.$$postDigestQueue = this.$$postDigestQueue; 228 | child.$$applyAsyncQueue = this.$$applyAsyncQueue; 229 | } else { 230 | var ChildScope = function() { }; 231 | ChildScope.prototype = this; 232 | child = new ChildScope(); 233 | } 234 | this.$$children.push(child); 235 | child.$$watchers = []; 236 | child.$$children = []; 237 | return child; 238 | }; 239 | ``` 240 | 241 | 第二, 我们需要共享 $$applyAsyncId 属性, 我们不能简单的在 $new 中那样处理, 因为我们需要对它赋值(它是一个基本类型), 所以必须显示的通过 $root 访问就好了. 242 | 243 | ```js 244 | Scope.prototype.$digest = function() { 245 | var ttl = 10; 246 | var dirty; 247 | this.$root.$$lastDirtyWatch = null; 248 | this.$beginPhase('$digest'); 249 | if (this.$root.$$applyAsyncId) { 250 | clearTimeout(this.$root.$$applyAsyncId); 251 | this.$$flushApplyAsync(); 252 | } 253 | do { 254 | while (this.$$asyncQueue.length) { 255 | try { 256 | var asyncTask = this.$$asyncQueue.shift(); 257 | asyncTask.scope.$eval(asyncTask.expression); 258 | } catch (e) { 259 | console.error(e); 260 | } 261 | } 262 | dirty = this.$$digestOnce(); 263 | if ((dirty || this.$$asyncQueue.length) && !(ttl--)) { 264 | throw '10 digest iterations reached'; 265 | } 266 | } while (dirty || this.$$asyncQueue.length); 267 | 268 | this.$clearPhase(); 269 | 270 | while (this.$$postDigestQueue.length) { 271 | try { 272 | this.$$postDigestQueue.shift()(); 273 | } catch (e) { 274 | console.error(e); 275 | } 276 | } 277 | }; 278 | 279 | Scope.prototype.$applyAsync = function(expr) { 280 | var self = this; 281 | self.$$applyAsyncQueue.push(function() { 282 | self.$eval(expr); 283 | }); 284 | if (self.$root.$$applyAsyncId === null) { 285 | self.$root.$$applyAsyncId = setTimeout(function() { 286 | self.$apply(_.bind(self.$$flushApplyAsync, self)); 287 | }, 0); 288 | } 289 | }; 290 | 291 | Scope.prototype.$$ ushApplyAsync = function() { 292 | while (this.$$applyAsyncQueue.length) { 293 | try { 294 | this.$$applyAsyncQueue.shift()(); 295 | } catch (e) { 296 | console.error(e); 297 | } 298 | } 299 | this.$root.$$applyAsyncId = null; 300 | }; 301 | ``` -------------------------------------------------------------------------------- /chapter3/Making-A-Child-Scope.md: -------------------------------------------------------------------------------- 1 | ### 创建一个子 Scope 2 | 3 | 尽管你可以创建很多的 Root Scope, 但是通常情况是在一个已存在的 Scope 上创建一个子的 Scope. 只需要在已存在的 Scope 上调用 $new 方法就行. 4 | 5 | 让我们使用测试驱动的方式来实现 $new 方法, 在开始前, 首先在 test/scope_spec.js 上添加一个描述让所有的测试去继承, 该测试文件应该有像下面的结构: 6 | 7 | ```js 8 | describe('Scope', function() { 9 | describe('digest', function() { 10 | // Tests from the previous chapter... 11 | }); 12 | describe('$watchGroup', function() { 13 | // Tests from the previous chapter... 14 | }); 15 | describe('inheritance', function() { 16 | // Tests for this chapter 17 | }); 18 | }); 19 | ``` 20 | 21 | 第一件事情就是关于子 scope 可以共享父 scope 的属性. 22 | 23 | ```js 24 | it('inherits the parent\'s properties', function() { 25 | var parent = new Scope(); 26 | parent.aValue = [1, 2, 3]; 27 | var child = parent.$new(); 28 | expect(child.aValue).toEqual([1, 2, 3]); 29 | }); 30 | ``` 31 | 同样的一个定义在子 scope 上的属性不应该存在于父 scope 上. 32 | 33 | ```js 34 | it('does not cause a parent to inherit its properties', function() { 35 | var parent = new Scope(); 36 | var child = parent.$new(); 37 | child.aValue = [1, 2, 3]; 38 | expect(parent.aValue).toBeUnde ned(); 39 | }); 40 | ``` 41 | 共享属性和属性什么时候定义没有关系, 当一个属性定义在父 scope 上, 它所有的子 scope 都可以访问这个属性. 42 | 43 | ```js 44 | it('inherits the parents properties whenever they are de ned', function() { 45 | var parent = new Scope(); 46 | var child = parent.$new(); 47 | parent.aValue = [1, 2, 3]; 48 | expect(child.aValue).toEqual([1, 2, 3]); 49 | }); 50 | ``` 51 | 你可以在子 scope 上操作父 scope 上的属性, 实际上它们指向同样的值. 52 | 53 | ```js 54 | it('can manipulate a parent scopes property', function() { 55 | var parent = new Scope(); 56 | var child = parent.$new(); 57 | parent.aValue = [1, 2, 3]; 58 | child.aValue.push(4); 59 | expect(child.aValue).toEqual([1, 2, 3, 4]); 60 | expect(parent.aValue).toEqual([1, 2, 3, 4]); 61 | }); 62 | ``` 63 | 64 | 你可以在子 scope 上监控父 scope 上的属性 65 | 66 | ```js 67 | it('can watch a property in the parent', function() { 68 | var parent = new Scope(); 69 | var child = parent.$new(); 70 | parent.aValue = [1, 2, 3]; 71 | child.counter = 0; 72 | child.$watch( 73 | function(scope) { return scope.aValue; }, 74 | function(newValue, oldValue, scope) { 75 | scope.counter++; 76 | }, 77 | true 78 | ); 79 | child.$digest(); 80 | expect(child.counter).toBe(1); 81 | parent.aValue.push(4); 82 | child.$digest(); 83 | expect(child.counter).toBe(2); 84 | }); 85 | ``` 86 | 87 | 最后, scope 层级关系可以是任意深度. 88 | 89 | ```js 90 | it('can be nested at any depth', function() { 91 | var a = new Scope(); 92 | var aa = a.$new(); 93 | var aaa = aa.$new(); 94 | var aab = aa.$new(); 95 | var ab = a.$new(); 96 | var abb = ab.$new(); 97 | a.value = 1; 98 | 99 | expect(aa.value).toBe(1); 100 | expect(aaa.value).toBe(1); 101 | expect(aab.value).toBe(1); 102 | expect(ab.value).toBe(1); 103 | expect(abb.value).toBe(1); 104 | 105 | ab.anotherValue = 2; 106 | expect(abb.anotherValue).toBe(2); 107 | expect(aa.anotherValue).toBeUnde ned(); 108 | expect(aaa.anotherValue).toBeUnde ned(); 109 | }); 110 | ``` 111 | 112 | 目前这种实现是非常直接, 我们只需要深入理解 JavaScript 对象继承, 因为 Angular 刻意模仿 JavaScript 本身如何工作. 本质上来说, 当你创建一个子的 scope 它的父 scope 将会成为它的原型. 113 | 114 | ```js 115 | Scope.prototype.$new = function() { 116 | var ChildScope = function() { }; 117 | ChildScope.prototype = this; 118 | var child = new ChildScope(); 119 | return child; 120 | }; 121 | ``` 122 | 123 | 在这个函数中, 首先, 我们为子 scope 创建一个构造函数, 将它存在一个局部变量 ChildScope, 然后将 Scope 作为 ChildScope 的原型, 最终我们创建一个 ChildScope 构造器 并返回它. 124 | -------------------------------------------------------------------------------- /chapter3/Recursive-Digestion.md: -------------------------------------------------------------------------------- 1 | ### 递归 Digest 2 | 3 | 在上一章, 我们讨论过, 调用 $digest 不应该执行父层级上的监听器, 但是应该执行子层级上的监听器, 这是有意义的, 因为一些子层级上的监听器是可以通过原型链监控当前 scope 上的属性的. 4 | 5 | 因为每个 Scope 上都有单独的监听器数组, 当我们在父 Scope 上调用 digest 方法时, 子 Scope 不会调用 digest. 我们必须修正 $digest 的行为, 不仅要调用自己的 digest, 也要运行子 scope 的 digest. 6 | 7 | 第一个问题是, 当前的 Scope 根本不知道自己有没有子 Scope,或者有哪些子 Scope. 我们需要每个 Scope 记录它有哪些子的 Scope. 8 | 9 | ```js 10 | it('keeps a record of its children', function() { 11 | var parent = new Scope(); 12 | var child1 = parent.$new(); 13 | var child2 = parent.$new(); 14 | var child2_1 = child2.$new(); 15 | 16 | expect(parent.$$children.length).toBe(2); 17 | expect(parent.$$children[0]).toBe(child1); 18 | expect(parent.$$children[1]).toBe(child2); 19 | expect(child1.$$children.length).toBe(0); 20 | expect(child2.$$children.length).toBe(1); 21 | expect(child2.$$children[0]).toBe(child2_1); 22 | }); 23 | ``` 24 | 25 | 我们需要在 Root Scope 构造函数初始化 $$children 数组: 26 | 27 | ```js 28 | function Scope() { 29 | this.$$watchers = []; 30 | this.$$lastDirtyWatch = null; 31 | this.$$asyncQueue = []; 32 | this.$$applyAsyncQueue = []; 33 | this.$$applyAsyncId = null; 34 | this.$$postDigestQueue = []; 35 | this.$$children = []; 36 | this.$$phase = null; 37 | } 38 | ``` 39 | 然后, 我们需要为新创建的 Scope 添加一个新的 $$children 数组, 我们也需要将它们的子 Scope 也放入它们自己的 $$children 数组中. 看看 $new 方法的变化 40 | 41 | ```js 42 | Scope.prototype.$new = function() { 43 | var ChildScope = function() { }; 44 | ChildScope.prototype = this; 45 | var child = new ChildScope(); 46 | this.$$children.push(child); 47 | child.$$watchers = []; 48 | child.$$children = []; 49 | return child; 50 | }; 51 | ``` 52 | 我们现在已经将所有子 Scope 记录起来了. 我们可以开始讨论如何 digest 它们. 我们希望在父 Scope 上调用 $digest 方法, 也可以执行到子 Scope 中注册的监听器. 53 | 54 | ```js 55 | it('digests its children', function() { 56 | var parent = new Scope(); 57 | var child = parent.$new(); 58 | parent.aValue = 'abc'; 59 | child.$watch( 60 | function(scope) { return scope.aValue; }, 61 | function(newValue, oldValue, scope) { 62 | scope.aValueWas = newValue; 63 | } 64 | ); 65 | parent.$digest(); 66 | expect(child.aValueWas).toBe('abc'); 67 | }); 68 | ``` 69 | 为了使上面的测试通过, 我们需要修改 $$digestOnce, 遍历层级运行监听器, 为了处理起来更容易, 添加一个帮助函数 $$everyScope 70 | 71 | ```js 72 | Scope.prototype.$$everyScope = function(fn) { 73 | if (fn(this)) { 74 | return this.$$children.every(function(child) { 75 | return child.$$everyScope(fn); 76 | }); 77 | } else { 78 | return false; 79 | } 80 | }; 81 | ``` 82 | 83 | 该函数在当前的 scope 上调用 fn 一次, 并且递归的调用当前 Scope 的子 Scope. 84 | 85 | 我们现在可以在 $$digestOnce 中使用这个函数为整个操作做一个外层循环. 86 | 87 | ```js 88 | Scope.prototype.$$digestOnce = function() { 89 | var dirty; 90 | var continueLoop = true; 91 | var self = this; 92 | this.$$everyScope(function(scope) { 93 | var newValue, oldValue; 94 | _.forEachRight(scope.$$watchers, function(watcher) { 95 | try { 96 | if (watcher) { 97 | newValue = watcher.watchFn(scope); 98 | oldValue = watcher.last; 99 | if (!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) { 100 | self.$$lastDirtyWatch = watcher; 101 | watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 102 | watcher.listenerFn(newValue, 103 | (oldValue === initWatchVal ? newValue : oldValue), 104 | scope); 105 | dirty = true; 106 | } else if (self.$$lastDirtyWatch === watcher) { 107 | continueLoop = false; 108 | return false; 109 | } 110 | } 111 | } catch (e) { 112 | console.error(e); 113 | } 114 | }); 115 | return continueLoop; 116 | }); 117 | return dirty; 118 | }; 119 | ``` 120 | 121 | 现在 $$digestOnce 函数可以遍历整个层级并且通过返回一个布尔值区分在层级中任意位置任意监听器是否是脏. 122 | 123 | 循环内部遍历 Scope 的层级, 直到所有 Scope 被访问或者缩短回路优化生效. 缩短回路优化使用 continueLoop 变量追踪. 如果它是 false, 则跳出 124 | 循环和 $$digestOnce 函数. 125 | 126 | 注意, watch 函数必须被传入正确的 scope, 在循环内部用使用特定的 scope 变量替换 this 引用, 确保正确运行. 127 | 128 | 注意, $$lastDirtyWatch 属性, 我们一直使用的是最顶部 scope 的, 缩短回路优化需要保证 scope 层级中的所有的监听器是一起的. 如果我们在在当前的 Scope 上设置 $$lastDirtyWatch, 它会遮蔽父 Scope 上的属性. 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /chapter3/Separated-Watches.md: -------------------------------------------------------------------------------- 1 | ### 隔开监听器 2 | 3 | 我们已经看到我们可以在子 Scope 上添加监听器, 因为子 Scope 继承了包括 $watch 和 $digest 在内的所有父 Scope 的方法, 但是这些监听器存放在哪里呢? 被那些 Scope 4 | 执行呢? 5 | 6 | 在当前的实现中, 所有的监听器实际上是被储存在 Root Scope 上. 这是因为我们将 $$watchers 数组定义在 Root Scope 的构造函数中. 当任何子 Scope 访问 $$watchers 数组时, 它们得到是 Root Scope 上的一个 copy. 7 | 8 | 这种实现有一个严重的后果: 无论我们在哪个 Scope 上调用 $digest 方法, 都会在 Scope 的层级中执行所有的将监听器, 这是因为只有一个监听器数组. 这并不是我们想要的结果. 9 | 10 | 我们真正想要的是, 在 Scope 上调用 $digest 方法时, 该 Scope 上和它的所有子 Scope 上的监听器被执行, 而不是它的父 scope 或其它 scope 上的子 scope 的监听器. 11 | 12 | ```js 13 | it('does not digest its parent(s)', function() { 14 | var parent = new Scope(); 15 | var child = parent.$new(); 16 | parent.aValue = 'abc'; 17 | parent.$watch( 18 | function(scope) { return scope.aValue; }, 19 | function(newValue, oldValue, scope) { 20 | scope.aValueWas = newValue; 21 | } 22 | ); 23 | child.$digest(); 24 | expect(child.aValueWas).toBeUndefined(); 25 | }); 26 | ``` 27 | 28 | 这个测试是失败的, 因为当我们调用 child.$digest() 时, 我们执行了父 Scope 上的监听器. 29 | 30 | 让我们来修复它, 窍门就是为每个子 Scope 添加一个 $$watcher 数组: 31 | 32 | ```js 33 | Scope.prototype.$new = function() { 34 | var ChildScope = function() { }; 35 | ChildScope.prototype = this; 36 | var child = new ChildScope(); 37 | child.$$watchers = []; 38 | return child; 39 | }; 40 | ``` 41 | 42 | 你或许已经注意到了, 我们在这使用了属性遮蔽. 每个子 Scope 上的 $$watchers 数组遮蔽了父 Scope 的. 在层级里的每个 Scope 都有自己的 watcher 数组. 当我们调用 Scope 上的 $digest, scope 上的监听器会被执行. 43 | -------------------------------------------------------------------------------- /chapter3/Substituting-The-Parent-Scope.md: -------------------------------------------------------------------------------- 1 | ### 替换父 Scope 2 | 3 | 在一些情况中, 通过传递其他 Scope 作为新 Scope 的父 Scope, 同时仍然要维护原 Scope 上原型继承链是非常有用的: 4 | 5 | ```js 6 | it('can take some other scope as the parent', function() { 7 | var prototypeParent = new Scope(); 8 | var hierarchyParent = new Scope(); 9 | var child = prototypeParent.$new(false, hierarchyParent); 10 | prototypeParent.a = 42; 11 | expect(child.a).toBe(42); 12 | child.counter = 0; 13 | child.$watch(function(scope) { 14 | scope.counter++; 15 | }); 16 | prototypeParent.$digest(); 17 | expect(child.counter).toBe(0); 18 | hierarchyParent.$digest(); 19 | expect(child.counter).toBe(2); 20 | }); 21 | ``` 22 | 我们用构造函数创建两个父 Scope, 然后创建一个子 Scope, 其中一个父 Scope 是新 Scope 原型链上的 Scope, 另一个则是 Scope 树层级上的父 Scope. 23 | 24 | 我们测试在原型继承上的父子 Scope 之间可以和之前一样工作, 但是我们测试了再原型继承的父 Scope 运行 digest, 没有触发子 Scope 上的监听器运行. 25 | 26 | 相反的, 在层级上的父 Scope 上却成功的触发子 Scope 上的监听器运行. 27 | 28 | 我们为 $new 方法, 引入第二个可选参数, 默认是当前 Scope, 我们会使用该 Scope 的 children 存储新的 Scope. 29 | 30 | ```js 31 | Scope.prototype.$new = function(isolated, parent) { 32 | var child; 33 | parent = parent || this; 34 | if (isolated) { 35 | child = new Scope(); 36 | child.$root = parent.$root; 37 | child.$$asyncQueue = parent.$$asyncQueue; 38 | child.$$postDigestQueue = parent.$$postDigestQueue; 39 | child.$$applyAsyncQueue = parent.$$applyAsyncQueue; 40 | } else { 41 | var ChildScope = function() { }; 42 | ChildScope.prototype = this; 43 | child = new ChildScope(); 44 | } 45 | parent.$$children.push(child); 46 | child.$$watchers = []; 47 | child.$$children = []; 48 | return child; 49 | }; 50 | ``` 51 | 52 | 这个特性引进了原型继承和层级继承之间的细微的区别, 可能在大部分情况下, 它的价值不大, 不值得花费精力区分两种 Scope 继承的细微不同. 但是当我们实现指令的 transclusion 特性时, 它会非常的有用. 53 | -------------------------------------------------------------------------------- /chapter3/The-Root-Scope.md: -------------------------------------------------------------------------------- 1 | ### Root Scope 2 | 3 | 目前, 我们已经使用过通过 Scope 构造函数创建的 scope 对象: 4 | 5 | ```js 6 | var scope = new Scope(); 7 | ``` 8 | 9 | Root Scope 也是像上面一样的方式创建的, 之所以叫 Root Scope, 是因为它没有父亲, 通常情况是一个 Child Scope 树的根. 10 | 11 | 在真实 Angular 应用中, 你不可能用这种方式创建一个 Root Scope, 因为已经存在一个 Root Scope, 其他所有的 Scope 都是它的子孙, 不论是被控制器还是指令创建的. -------------------------------------------------------------------------------- /chapter4/Array-Like-Objects.md: -------------------------------------------------------------------------------- 1 | ### 类数组对象 2 | 3 | 我们已经处理了数组, 但是还有一种特殊的情况需要考虑: 4 | 5 | 除了真正的数组(继承自 Array.prototype)之外, JavaScript 环境中有一些行为类似于数组, 但实际上又不是数组的对象. Angular 的 $watchCollection 把这类对象当作数组对待, 因此我们也这样做. 6 | 7 | 一种类数组对象就是每个函数都有的本地变量 arguments, 它包含了函数被调用时的参数. 让我们通过一个测试用例来检查是否支持这种类数组对象: 8 | 9 | ```js 10 | it('notices an item replaced in an arguments object', function() { 11 | (function() { 12 | scope.arrayLike = arguments; 13 | })(1, 2, 3); 14 | scope.counter = 0; 15 | scope.$watchCollection( 16 | function(scope) { return scope.arrayLike; }, 17 | function(newValue, oldValue, scope) { 18 | scope.counter++; 19 | } 20 | ); 21 | scope.$digest(); 22 | expect(scope.counter).toBe(1); 23 | scope.arrayLike[1] = 42; 24 | scope.$digest(); 25 | expect(scope.counter).toBe(2); 26 | scope.$digest(); 27 | expect(scope.counter).toBe(2); 28 | }); 29 | ``` 30 | 31 | 我们用一些参数去调用一个匿名函数, 将匿名函数的 arguments 存储在 Scope 上. 我们检查 arguments 对象的变化是否被 $watchCollection 注意到. 32 | 33 | 另一种类数组对象就是 DOM 的 NodeList, 你可以通过一些 DOM 操作得到它, 例如 querySelectorAll 和 getElementsByTagName. 让我们也来测试它吧. 34 | 35 | ```js 36 | it('notices an item replaced in a NodeList object', function() { 37 | document.documentElement.appendChild(document.createElement('div')); 38 | scope.arrayLike = document.getElementsByTagName('div'); 39 | scope.counter = 0; 40 | scope.$watchCollection( 41 | function(scope) { return scope.arrayLike; }, 42 | function(newValue, oldValue, scope) { 43 | scope.counter++; 44 | } 45 | ); 46 | scope.$digest(); 47 | expect(scope.counter).toBe(1); 48 | document.documentElement.appendChild(document.createElement('div')); 49 | scope.$digest(); 50 | expect(scope.counter).toBe(2); 51 | scope.$digest(); 52 | expect(scope.counter).toBe(2); 53 | }); 54 | ``` 55 | 56 | 首先, 我们在 DOM 上添加一个 div 元素, 然后调用 document 上的 getElementsByTagName 拿到一个 NodeList 对象, 在 Scope 上存放该 NodeList 对象并添加一个监听器监控它. 57 | 当我们想要引起该 NodeList 变化, 只需要在 DOM 上添加两一个 div 元素. 该 NodeList 会立即增加一个新 div 元素. 它被称为 'live collection'. 58 | 59 | 我们检查 $watchCollection 是否可以检测到这种变化. 60 | 61 | 结果, 两个测试都失败了, 问题是 LoDash 的 _.isArray 方法, 只能检测数组, 而不能检测类数组. 因此我们需要创建适合测试用例的断定函数, 做类型检测. 62 | 63 | ```js 64 | function isArrayLike(obj) { 65 | if (_.isNull(obj) || _.isUndefned(obj)) { 66 | return false; 67 | } 68 | var length = obj.length; 69 | return _.isNumber(length); 70 | } 71 | ``` 72 | 73 | 该函数用一个对象作参数, 返回一个布尔值, 用来断定传入的对象参数是否是类数组对象. 我们通过确定该对象存在, 并且有一个数字型的 length 属性来确定对象是否是类数组. 74 | 75 | 现在, 我们所需要做的是在 $watchCollection 函数调用新的断定函数而不是 _.isArray. 76 | ```js 77 | var internalWatchFn = function(scope) { 78 | newValue = watchFn(scope); 79 | if (_.isObject(newValue)) { 80 | if (isArrayLike(newValue)) { 81 | if (!_.isArray(oldValue)) { 82 | changeCount++; 83 | oldValue = []; 84 | } 85 | if (newValue.length !== oldValue.length) { 86 | changeCount++; 87 | oldValue.length = newValue.length; 88 | } 89 | _.forEach(newValue, function(newItem, i) { 90 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 91 | if (!bothNaN && newItem !== oldValue[i]) { 92 | changeCount++; 93 | oldValue[i] = newItem; 94 | } 95 | }); 96 | } else { 97 | 98 | } 99 | } else { 100 | if (!self.$$areEqual(newValue, oldValue, false)) { 101 | changeCount++; 102 | } 103 | oldValue = newValue; 104 | } 105 | return changeCount; 106 | }; 107 | ``` 108 | 109 | 注意, 当我们处理这种类数组对象时, 内部的 oldValue 永远是一个数组, 不可能是类数组对象. 110 | -------------------------------------------------------------------------------- /chapter4/Dealing-with-Objects-that-Have-A-length.md: -------------------------------------------------------------------------------- 1 | ### 处理具有 length 属性的对象 2 | 3 | 我们几乎处理完所有不同类型的集合了, 但是仍然有一种特殊的对象, 得好好考虑一下: 4 | 5 | 回顾, 我们通过检查对象是否有一个数字类型的 length 属性, 判断一个对象是否是类数组对象. 那么, 下面的对象该如何处理? 6 | 7 | ```js 8 | { 9 | length: 42, 10 | otherKey: 'abc' 11 | } 12 | ``` 13 | 14 | 这不是一个类数组对象. 它仅仅有一个叫做 length 的属性. 既然想到这种对象有可能出现在应用中, 我们需要处理这类对象. 15 | 16 | 让我们添加一个测试用例: 17 | 18 | ```js 19 | it('does not consider any object with a length property an array', function() { 20 | scope.obj = {length: 42, otherKey: 'abc'}; 21 | scope.counter = 0; 22 | scope.$watchCollection( 23 | function(scope) { return scope.obj; }, 24 | function(newValue, oldValue, scope) { 25 | scope.counter++; 26 | } 27 | ); 28 | scope.$digest(); 29 | scope.obj.newKey = 'def'; 30 | scope.$digest(); 31 | expect(scope.counter).toBe(2); 32 | }); 33 | ``` 34 | 35 | 当你运行这个测试, 你会看到在对象发生变化之后, listener 函数并未被调用. 原因在于我们判断数组方法不对, 检测数组变化并不会注意新添加的属性. 36 | 37 | 修复很简单, 代替之前认为对象只要有一个数字类型的 length 属性就是类数组的写法, 让我们缩小范围: 带有数组类型 length 属性的对象, 并且这个对象有一个比这个 length 属性小于 1 的 key. 38 | 39 | 举例, 如果一个对象有一个 length 为 42 的属性, 那它也一定有叫做 41 的属性. 40 | 41 | 这种方式只针对非零长度, 所以需要对 length 为 0 的情况放宽条件. 42 | 43 | ```js 44 | function isArrayLike(obj) { 45 | if (_.isNull(obj) || _.isUndefined(obj)) { 46 | return false; 47 | } 48 | var length = obj.length; 49 | return length === 0 || 50 | (_.isNumber(length) && length > 0 && (length - 1) in obj); 51 | } 52 | ``` 53 | 54 | 这使我们的测试通过了, 确实对大部分对象是工作的, 但是这个检查并不能做到万无一失, 但是这已经是我们所能做到最好了. 55 | -------------------------------------------------------------------------------- /chapter4/Detecting-New-Array.md: -------------------------------------------------------------------------------- 1 | ### 检测数组变化: 初始化数组时 2 | 3 | $watchCollection 内部的 watch 函数将会有两个顶层条件分支: 一个处理对象, 两一个处理非对象. 因为 JavaScript 数组也是对象, 所以我们将在第一个分支中处理数组. 然而在该分支内需要一个嵌套分支, 来对数组和其他对象做不同的处理. 4 | 5 | 现在, 我们可以通过 LoDash 的 _.isObject 和 _.isArray 函数去判断监控的值是数组还是对象. 6 | 7 | ```js 8 | var internalWatchFn = function(scope) { 9 | newValue = watchFn(scope); 10 | if (_.isObject(newValue)) { 11 | if (_.isArray(newValue)) { 12 | 13 | } else { 14 | 15 | } 16 | } else { 17 | if (!self.$$areEqual(newValue, oldValue, false)) { 18 | changeCount++; 19 | } 20 | oldValue = newValue; 21 | } 22 | return changeCount; 23 | }; 24 | ``` 25 | 26 | 第一件事, 我们可以检测 oldValue 是否是数组, 如果不是, 显然值发生了变化. 27 | 28 | ```js 29 | it('notices when the value becomes an array', function() { 30 | scope.counter = 0; 31 | scope.$watchCollection( 32 | function(scope) { return scope.arr; }, 33 | function(newValue, oldValue, scope) { 34 | scope.counter++; 35 | } 36 | ); 37 | scope.$digest(); 38 | expect(scope.counter).toBe(1); 39 | scope.arr = [1, 2, 3]; 40 | scope.$digest(); 41 | expect(scope.counter).toBe(2); 42 | scope.$digest(); 43 | expect(scope.counter).toBe(2); 44 | }); 45 | ``` 46 | 47 | 因为我们的数组条件分支是空的, 我们不会注意到这种改变. 48 | 49 | ```js 50 | var internalWatchFn = function(scope) { 51 | newValue = watchFn(scope); 52 | if (_.isObject(newValue)) { 53 | if (_.isArray(newValue)) { 54 | if (!_.isArray(oldValue)) { 55 | changeCount++; 56 | oldValue = []; 57 | } 58 | } else { 59 | 60 | } 61 | } else { 62 | if (!self.$$areEqual(newValue, oldValue, false)) { 63 | changeCount++; 64 | } 65 | oldValue = newValue; 66 | } 67 | return changeCount; 68 | }; 69 | ``` 70 | 71 | 如果 oldValue 不是数组, 我们记录该变化. 同时将 oldValue 初始化成空数组. 72 | -------------------------------------------------------------------------------- /chapter4/Detecting-New-Objects.md: -------------------------------------------------------------------------------- 1 | ### 检测对象变化: 初始化对象 2 | 3 | 让我们将注意力转向对象, 或者更精确些, 不是数组和类数组对象的对象, 例如下面的字典 4 | 5 | ```js 6 | { 7 | aKey: 'aValue', 8 | anotherKey: 42 9 | } 10 | ``` 11 | 12 | 检测对象中变化的方式, 与我们在数组中所做的类似. 对象变化的检测的实现变得简单一点, 因为没有类对象对象使检测复杂化. 另一方面, 我们需要在对象变化检测上做的多一些, 因为我们不需要处理 length 属性. 13 | 14 | 首先, 像数组一样, 确保覆盖一个不是对象的值变成对象的情况: 15 | 16 | ```js 17 | it('notices when the value becomes an object', function() { 18 | scope.counter = 0; 19 | scope.$watchCollection( 20 | function(scope) { return scope.obj; }, 21 | function(newValue, oldValue, scope) { 22 | scope.counter++; 23 | } 24 | ); 25 | scope.$digest(); 26 | expect(scope.counter).toBe(1); 27 | scope.obj = {a: 1}; 28 | scope.$digest(); 29 | expect(scope.counter).toBe(2); 30 | scope.$digest(); 31 | expect(scope.counter).toBe(2); 32 | }); 33 | ``` 34 | 35 | 我们使用数组中的做法处理这种情况的, 如果 oldValue 不是对象, 初始化一个, 并记录这次改变. 36 | 37 | ```js 38 | var internalWatchFn = function(scope) { 39 | newValue = watchFn(scope); 40 | if (_.isObject(newValue)) { 41 | if (isArrayLike(newValue)) { 42 | if (!_.isArray(oldValue)) { 43 | changeCount++; 44 | oldValue = []; 45 | } 46 | if (newValue.length !== oldValue.length) { 47 | changeCount++; 48 | oldValue.length = newValue.length; 49 | } 50 | _.forEach(newValue, function(newItem, i) { 51 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 52 | if (!bothNaN && newItem !== oldValue[i]) { 53 | changeCount++; 54 | oldValue[i] = newItem; 55 | } 56 | }); 57 | } else { 58 | if (!_.isObject(oldValue) || isArrayLike(oldValue)) { 59 | changeCount++; 60 | oldValue = {}; 61 | } 62 | } 63 | } else { 64 | if (!self.$$areEqual(newValue, oldValue, false)) { 65 | changeCount++; 66 | } 67 | oldValue = newValue; 68 | } 69 | return changeCount; 70 | }; 71 | ``` 72 | 73 | 注意, 数组也是对象, 我们不能只用 _.isObject 判断 oldValue. 我们也需要用 isArrayLike 排除数组和类数组对象. 74 | -------------------------------------------------------------------------------- /chapter4/Detecting-New-Or-Removed-Items-in-Arrays.md: -------------------------------------------------------------------------------- 1 | ### 检测数组变化: 添加和删除数组元素时 2 | 3 | 接下来, 当添加或删除数组元素时, 数组的长度会发生变化. 让我们来为每种操作添加些测试用例: 4 | 5 | ```js 6 | it('notices an item added to an array', function() { 7 | scope.arr = [1, 2, 3]; 8 | scope.counter = 0; 9 | scope.$watchCollection( 10 | function(scope) { return scope.arr; }, 11 | function(newValue, oldValue, scope) { 12 | scope.counter++; 13 | } 14 | ); 15 | scope.$digest(); 16 | expect(scope.counter).toBe(1); 17 | scope.arr.push(4); 18 | scope.$digest(); 19 | expect(scope.counter).toBe(2); 20 | scope.$digest(); 21 | expect(scope.counter).toBe(2); 22 | }); 23 | it('notices an item removed from an array', function() { 24 | scope.arr = [1, 2, 3]; 25 | scope.counter = 0; 26 | scope.$watchCollection( 27 | function(scope) { return scope.arr; }, 28 | function(newValue, oldValue, scope) { 29 | scope.counter++; 30 | } 31 | ); 32 | scope.$digest(); 33 | expect(scope.counter).toBe(1); 34 | scope.arr.shift(); 35 | scope.$digest(); 36 | expect(scope.counter).toBe(2); 37 | scope.$digest(); 38 | expect(scope.counter).toBe(2); 39 | }); 40 | ``` 41 | 42 | 在这两个测试用例中, 我们都监听 Scope 上的数组并且对数组进行了操作, 然后检测这种变化是否在 digest 时被注意到. 43 | 44 | 这种变化, 通过简单的比较新旧数组的长度就可以被检测到. 我们必须将新长度同步到内部 oldValue 数组上, 这里只是简单的做了个赋值. 45 | 46 | ```js 47 | var internalWatchFn = function(scope) { 48 | newValue = watchFn(scope); 49 | if (_.isObject(newValue)) { 50 | if (_.isArray(newValue)) { 51 | if (!_.isArray(oldValue)) { 52 | changeCount++; 53 | oldValue = []; 54 | } 55 | if (newValue.length !== oldValue.length) { 56 | changeCount++; 57 | oldValue.length = newValue.length; 58 | } 59 | } else { 60 | 61 | } 62 | } else { 63 | if (!self.$$areEqual(newValue, oldValue, false)) { 64 | changeCount++; 65 | } 66 | oldValue = newValue; 67 | } 68 | return changeCount; 69 | }; 70 | ``` 71 | 这种将新长度赋值显然是不对的, 只是暂时通过当前测试, 如何解决, 请关注下一节 -------------------------------------------------------------------------------- /chapter4/Detecting-New-Or-Replaced-Attributes-in-Objects.md: -------------------------------------------------------------------------------- 1 | ### 检测对象变化: 添加和替换对象属性 2 | 3 | 我们给对象添加一个新属性去触发一个变化. 4 | 5 | ```js 6 | it('notices when an attribute is added to an object', function() { 7 | scope.counter = 0; 8 | scope.obj = {a: 1}; 9 | scope.$watchCollection( 10 | function(scope) { return scope.obj; }, 11 | function(newValue, oldValue, scope) { 12 | scope.counter++; 13 | } 14 | ); 15 | scope.$digest(); 16 | expect(scope.counter).toBe(1); 17 | scope.obj.b = 2; 18 | scope.$digest(); 19 | expect(scope.counter).toBe(2); 20 | scope.$digest(); 21 | expect(scope.counter).toBe(2); 22 | }); 23 | ``` 24 | 25 | 我们也可以触发一个变化, 当已存在的属性的值发生改变: 26 | 27 | ```js 28 | it('notices when an attribute is changed in an object', function() { 29 | scope.counter = 0; 30 | scope.obj = {a: 1}; 31 | scope.$watchCollection( 32 | function(scope) { return scope.obj; }, 33 | function(newValue, oldValue, scope) { 34 | scope.counter++; 35 | } 36 | ); 37 | scope.$digest(); 38 | expect(scope.counter).toBe(1); 39 | scope.obj.a = 2; 40 | scope.$digest(); 41 | expect(scope.counter).toBe(2); 42 | scope.$digest(); 43 | expect(scope.counter).toBe(2); 44 | }); 45 | ``` 46 | 47 | 上面两种情况可以被用相同的方式处理, 遍历新对象上的所有属性, 检查是否有相同的值在旧对象上. 48 | 49 | ```js 50 | var internalWatchFn = function(scope) { 51 | newValue = watchFn(scope); 52 | if (_.isObject(newValue)) { 53 | if (isArrayLike(newValue)) { 54 | if (!_.isArray(oldValue)) { 55 | changeCount++; 56 | oldValue = []; 57 | } 58 | if (newValue.length !== oldValue.length) { 59 | changeCount++; 60 | oldValue.length = newValue.length; 61 | } 62 | _.forEach(newValue, function(newItem, i) { 63 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 64 | if (!bothNaN && newItem !== oldValue[i]) { 65 | changeCount++; 66 | oldValue[i] = newItem; 67 | } 68 | }); 69 | } else { 70 | if (!_.isObject(oldValue) || isArrayLike(oldValue)) { 71 | changeCount++; 72 | oldValue = {}; 73 | } 74 | _.forOwn(newValue, function(newVal, key) { 75 | if (oldValue[key] !== newVal) { 76 | changeCount++; 77 | oldValue[key] = newVal; 78 | } 79 | }); 80 | } 81 | } else { 82 | if (!self.$$areEqual(newValue, oldValue, false)) { 83 | changeCount++; 84 | } 85 | oldValue = newValue; 86 | } 87 | return changeCount; 88 | }; 89 | ``` 90 | 91 | 当遍历对象的时候, 我们将新对象的所有属性同步到旧对象上, 以便在下次 digest 中使用它们. 92 | 93 | LoDash 的 _.forOwn 方法遍历对象的属性, 但是它只遍历对象自己定义的属性. 从原型链上继承的属性会被排除在外. 因此 $watchCollection 不监控对象上继承自原型链的属性. 94 | 95 | 和数组一样, NaN 需要被特殊对待. 如果一个对象有一个值是 NaN 的属性, 就会因此无限 digest: 96 | 97 | ```js 98 | var internalWatchFn = function(scope) { 99 | newValue = watchFn(scope); 100 | if (_.isObject(newValue)) { 101 | if (isArrayLike(newValue)) { 102 | if (!_.isArray(oldValue)) { 103 | changeCount++; 104 | oldValue = []; 105 | } 106 | if (newValue.length !== oldValue.length) { 107 | changeCount++; 108 | oldValue.length = newValue.length; 109 | } 110 | _.forEach(newValue, function(newItem, i) { 111 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 112 | if (!bothNaN && newItem !== oldValue[i]) { 113 | changeCount++; 114 | oldValue[i] = newItem; 115 | } 116 | }); 117 | } else { 118 | if (!_.isObject(oldValue) || isArrayLike(oldValue)) { 119 | changeCount++; 120 | oldValue = {}; 121 | } 122 | _.forOwn(newValue, function(newVal, key) { 123 | var bothNaN = _.isNaN(newVal) && _.isNaN(oldValue[key]); 124 | if (!bothNaN && oldValue[key] !== newVal) { 125 | changeCount++; 126 | oldValue[key] = newVal; 127 | } 128 | }); 129 | } 130 | } else { 131 | if (!self.$$areEqual(newValue, oldValue, false)) { 132 | changeCount++; 133 | } 134 | oldValue = newValue; 135 | } 136 | return changeCount; 137 | }; 138 | ``` 139 | -------------------------------------------------------------------------------- /chapter4/Detecting-Non-Collection-Changes.md: -------------------------------------------------------------------------------- 1 | ### 检测非集合数据的变化 2 | 3 | 虽然 $watchCollection 的目的是监控数组和对象, 但是当 watch 函数返回非集合数据时, 它同样也可以工作. 在非集合数据的情况下相当于退回到之前的实现, 只需要简单地调用一下 $watch. 4 | 5 | 这里有一个用来验证这种行为的测试: 6 | 7 | ```js 8 | it('works like a normal watch for non-collections', function() { 9 | var valueProvided; 10 | scope.aValue = 42; 11 | scope.counter = 0; 12 | scope.$watchCollection( 13 | function(scope) { return scope.aValue; }, 14 | function(newValue, oldValue, scope) { 15 | valueProvided = newValue; 16 | scope.counter++; 17 | } 18 | ); 19 | scope.$digest(); 20 | expect(scope.counter).toBe(1); 21 | expect(valueProvided).toBe(scope.aValue); 22 | scope.aValue = 43; 23 | scope.$digest(); 24 | expect(scope.counter).toBe(2); 25 | scope.$digest(); 26 | expect(scope.counter).toBe(2); 27 | }); 28 | ``` 29 | 30 | 我们使用 $watchCollection 在 Scope 上监控一个数字. 在 listener 函数给 counter 变量做加 1, 并对一个本地变量赋新值. 然后我们断言这个监听器会用新值调用 listener 函数, 如同一个普通, 非集合的监听器做的一样. 31 | 32 | 在监听器调用期间, $watchCollection 首先调用原生的 watch 函数拿到我们想监控的值, 然后通过与之前的值比较来检查它的变化, 存储该值用于下次 digest 循环: 33 | 34 | ```js 35 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 36 | var newValue; 37 | var oldValue; 38 | var internalWatchFn = function(scope) { 39 | newValue = watchFn(scope); 40 | // Check for changes 41 | oldValue = newValue; 42 | }; 43 | var internalListenerFn = function() { 44 | }; 45 | return this.$watch(internalWatchFn, internalListenerFn); 46 | }; 47 | ``` 48 | 49 | 通过在内部的 watch 函数外声明变量保存 newValue 和 oldValue, 这样我们就可以在内部的 watch 函数和 listener 函数之间共享它们. 另外, 通过 $watchCollection 函数这种闭包形式, 它们同样可以在 digest 循环之间持续存在. 50 | 51 | 这对旧值是非常重要的, 因为我们需要在不同 digest 循环中对它进行比较. 52 | 53 | 内部的 listener 函数仅仅封装一下原生的 listener 函数, 传入 newValue 和 oldValue 作为参数: 54 | 55 | ```js 56 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 57 | var self = this; 58 | var newValue; 59 | var oldValue; 60 | var internalWatchFn = function(scope) { 61 | newValue = watchFn(scope); 62 | // Check for changes 63 | oldValue = newValue; 64 | }; 65 | var internalListenerFn = function() { 66 | listenerFn(newValue, oldValue, self); 67 | }; 68 | return this.$watch(internalWatchFn, internalListenerFn); 69 | }; 70 | ``` 71 | 72 | 回想 $digest 决定是否调用 listener 函数, 是通过比较 watch 函数连续返回的值. 在内部的 watch 函数, 当前并没有返回值, 因此 listener 函数将永远不会被调用. 73 | 74 | 那么, 内部的 watch 函数到底应该返回什么? Since nothing outside of $watchCollection will ever see it, it doesn’t make that much difference. The only important thing is that it is different between successive invocations if there have been changes. That is what will cause the listener to get called. 75 | 76 | Angular 的实现方式是通过引入一个 integer 类型的变量 counter 并且每当检测到变化时进行自增操作. 每个通过 $watchCollection 注册的监听器都会拥有自己的 counter 变量, 然后通过内部的 watch 函数返回它. 我们要确保 watch 函数被执行. 77 | 78 | 在非集合情况下, 我们只比较新旧值的引用: 79 | 80 | ```js 81 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 82 | var self = this; 83 | var newValue; 84 | var oldValue; 85 | var changeCount = 0; 86 | var internalWatchFn = function(scope) { 87 | newValue = watchFn(scope); 88 | if (newValue !== oldValue) { 89 | changeCount++; 90 | } 91 | oldValue = newValue; 92 | return changeCount; 93 | }; 94 | var internalListenerFn = function() { 95 | listenerFn(newValue, oldValue, self); 96 | }; 97 | return this.$watch(internalWatchFn, internalListenerFn); 98 | }; 99 | ``` 100 | 101 | 现在, 非集合测试通过了, 但是如果非集合测试中的值是 NaN 呢? 102 | 103 | ```js 104 | it('works like a normal watch for NaNs', function() { 105 | scope.aValue = 0/0; 106 | scope.counter = 0; 107 | scope.$watchCollection( 108 | function(scope) { return scope.aValue; }, 109 | function(newValue, oldValue, scope) { 110 | scope.counter++; 111 | } 112 | ); 113 | scope.$digest(); 114 | expect(scope.counter).toBe(1); 115 | scope.$digest(); 116 | expect(scope.counter).toBe(1); 117 | }); 118 | ``` 119 | 120 | 该测试失败的原因和第一章 NaN 中的是一样的: NaN 不等于任何其他值. 因此不要使用 !== 进行比较, 而是使用已存在的帮助函数 $$areEqual, 因为它已经帮我们处理好了 NaN. 121 | 122 | ```js 123 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 124 | var self = this; 125 | var newValue; 126 | var oldValue; 127 | var changeCount = 0; 128 | var internalWatchFn = function(scope) { 129 | newValue = watchFn(scope); 130 | if (!self.$$areEqual(newValue, oldValue, false)) { 131 | changeCount++; 132 | } 133 | oldValue = newValue; 134 | return changeCount; 135 | }; 136 | var internalListenerFn = function() { 137 | listenerFn(newValue, oldValue, self); 138 | }; 139 | return this.$watch(internalWatchFn, internalListenerFn); 140 | }; 141 | ``` 142 | 143 | $$areEqual 方法的最后一个参数是 false, 告诉我们它没有使用值比较, 在这种情况下, 我们只比较引用. 144 | 145 | 现在, 我们拥有 $watchCollection 的基本结构. 我们可以将注意力转移到对集合改变检测. -------------------------------------------------------------------------------- /chapter4/Detecting-Removed-Attributes-in-Objects.md: -------------------------------------------------------------------------------- 1 | ### 检测对象变化: 删除对象的属性 2 | 3 | 剩下的操作就是删除对象的属性: 4 | 5 | ```js 6 | it('notices when an attribute is removed from an object', function() { 7 | scope.counter = 0; 8 | scope.obj = {a: 1}; 9 | scope.$watchCollection( 10 | function(scope) { return scope.obj; }, 11 | function(newValue, oldValue, scope) { 12 | scope.counter++; 13 | } 14 | ); 15 | scope.$digest(); 16 | expect(scope.counter).toBe(1); 17 | delete scope.obj.a; 18 | scope.$digest(); 19 | expect(scope.counter).toBe(2); 20 | scope.$digest(); 21 | expect(scope.counter).toBe(2); 22 | }); 23 | ``` 24 | 25 | 对于删除数组元素, 我们通过截断内部数组, 并且同时遍历两个数组, 检测数组中所有元素是否相同. 对于删除对象上属性却不能这样做. 检测一个对象上的属性是否被删除, 我们需要第二个循环. 这次我们遍历旧对象上的属性, 看看属性是否还在新对象上. 26 | 如果不在, 说明该属性已经不存在了, 并且我们还要从内部对象上删除该属性. 27 | 28 | ```js 29 | var internalWatchFn = function(scope) { 30 | newValue = watchFn(scope); 31 | if (_.isObject(newValue)) { 32 | if (isArrayLike(newValue)) { 33 | if (!_.isArray(oldValue)) { 34 | changeCount++; 35 | oldValue = []; 36 | } 37 | if (newValue.length !== oldValue.length) { 38 | changeCount++; 39 | oldValue.length = newValue.length; 40 | } 41 | _.forEach(newValue, function(newItem, i) { 42 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 43 | if (!bothNaN && newItem !== oldValue[i]) { 44 | changeCount++; 45 | oldValue[i] = newItem; 46 | } 47 | }); 48 | } else { 49 | if (!_.isObject(oldValue) || isArrayLike(oldValue)) { 50 | changeCount++; 51 | oldValue = {}; 52 | } 53 | _.forOwn(newValue, function(newVal, key) { 54 | var bothNaN = _.isNaN(newVal) && _.isNaN(oldValue[key]); 55 | if (!bothNaN && oldValue[key] !== newVal) { 56 | changeCount++; 57 | oldValue[key] = newVal; 58 | } 59 | }); 60 | _.forOwn(oldValue, function(oldVal, key) { 61 | if (!newValue.hasOwnProperty(key)) { 62 | changeCount++; 63 | delete oldValue[key]; 64 | } 65 | }); 66 | } 67 | } else { 68 | if (!self.$$areEqual(newValue, oldValue, false)) { 69 | changeCount++; 70 | } 71 | oldValue = newValue; 72 | } 73 | return changeCount; 74 | }; 75 | ``` 76 | -------------------------------------------------------------------------------- /chapter4/Detecting-Replaced-Or-Reordered-Items-in-Arrays.md: -------------------------------------------------------------------------------- 1 | ### 检测数组变化: 替换或重排元素时 2 | 3 | 这还有一类我们必须检测的变化: 当数组中的元素发生替换或重新排序且数组的长度并未发生时. 4 | 5 | ```js 6 | it('notices an item replaced in an array', function() { 7 | scope.arr = [1, 2, 3]; 8 | scope.counter = 0; 9 | scope.$watchCollection( 10 | function(scope) { return scope.arr; }, 11 | function(newValue, oldValue, scope) { 12 | scope.counter++; 13 | } 14 | ); 15 | scope.$digest(); 16 | expect(scope.counter).toBe(1); 17 | scope.arr[1] = 42; 18 | scope.$digest(); 19 | expect(scope.counter).toBe(2); 20 | scope.$digest(); 21 | expect(scope.counter).toBe(2); 22 | }); 23 | 24 | it('notices items reordered in an array', function() { 25 | scope.arr = [2, 1, 3]; 26 | scope.counter = 0; 27 | scope.$watchCollection( 28 | function(scope) { return scope.arr; }, 29 | function(newValue, oldValue, scope) { 30 | scope.counter++; 31 | } 32 | ); 33 | scope.$digest(); 34 | expect(scope.counter).toBe(1); 35 | scope.arr.sort(); 36 | scope.$digest(); 37 | expect(scope.counter).toBe(2); 38 | scope.$digest(); 39 | expect(scope.counter).toBe(2); 40 | }); 41 | ``` 42 | 43 | 检测像这样的变化, 需要遍历整个数组并在每一个索引上比较新旧数组的元素. 这样做会关注到数组中元素替换和重新排序的值. 当遍历时, 我们还需要同步旧值和新值的内容. 44 | 45 | ```js 46 | var internalWatchFn = function(scope) { 47 | newValue = watchFn(scope); 48 | if (_.isObject(newValue)) { 49 | if (_.isArray(newValue)) { 50 | if (!_.isArray(oldValue)) { 51 | changeCount++; 52 | oldValue = []; 53 | } 54 | if (newValue.length !== oldValue.length) { 55 | changeCount++; 56 | oldValue.length = newValue.length; 57 | } 58 | _.forEach(newValue, function(newItem, i) { 59 | if (newItem !== oldValue[i]) { 60 | changeCount++; 61 | oldValue[i] = newItem; 62 | } 63 | }); 64 | } else { 65 | 66 | } 67 | } else { 68 | if (!self.$$areEqual(newValue, oldValue, false)) { 69 | changeCount++; 70 | } 71 | oldValue = newValue; 72 | } 73 | return changeCount; 74 | }; 75 | ``` 76 | 77 | 我们使用 LoDash 的 _.forEach 方法遍历新数组. 它为我们提供了每次循的元素和索引. 我们使用索引从旧数组获取相应的元素. 78 | 79 | 在章节 1 中, 我们看到 NaN 是如何成为问题, 因为 NaN 不等于 NaN. 我们已经普通的 watch 中特殊处理了. 同样也得在 watchCollection 中处理: 80 | 81 | ```js 82 | it('does not fail on NaNs in arrays', function() { 83 | scope.arr = [2, NaN, 3]; 84 | scope.counter = 0; 85 | scope.$watchCollection( 86 | function(scope) { return scope.arr; }, 87 | function(newValue, oldValue, scope) { 88 | scope.counter++; 89 | } 90 | ); 91 | scope.$digest(); 92 | expect(scope.counter).toBe(1); 93 | }); 94 | ``` 95 | 96 | 这个测试会抛出一个异常, 因为 NaN 永远都会触发一个改变, 因此无限 digest 循环. 我们来修复它吧! 97 | 98 | ```js 99 | var internalWatchFn = function(scope) { 100 | newValue = watchFn(scope); 101 | if (_.isObject(newValue)) { 102 | if (_.isArray(newValue)) { 103 | if (!_.isArray(oldValue)) { 104 | changeCount++; 105 | oldValue = []; 106 | } 107 | if (newValue.length !== oldValue.length) { 108 | changeCount++; 109 | oldValue.length = newValue.length; 110 | } 111 | _.forEach(newValue, function(newItem, i) { 112 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 113 | if (!bothNaN && newItem !== oldValue[i]) { 114 | changeCount++; 115 | oldValue[i] = newItem; 116 | } 117 | }); 118 | } else { 119 | } 120 | } else { 121 | if (!self.$$areEqual(newValue, oldValue, false)) { 122 | changeCount++; 123 | } 124 | oldValue = newValue; 125 | } 126 | return changeCount; 127 | }; 128 | ``` 129 | 130 | 当前的实现, 我们可以检测到可能发生在数组上的任何变化, 并且无需复制, 甚至遍历数组内任何嵌套的数据结构. -------------------------------------------------------------------------------- /chapter4/Handing-The-Old-Collection-Value-To-Listeners.md: -------------------------------------------------------------------------------- 1 | ### 对 listener 函数处理旧集合的值 2 | 3 | listener 函数的定义, 它会获取三个参数: watch 函数返回的 newValue, watch 函数上次返回的 oldValue, 当前 Scope. 4 | 本章, 我们已经按照定义提供这些值, 但是提供的值是有问题的, 特别是牵扯到 oldValue 时. 5 | 6 | 问题是在 innernalWatchFn 中维持的旧值, 在调用 listener 函数时已经被更新成新值. 传给 listener 函数的新旧值是一样的. 7 | 8 | 因此下面对非集合的测试用例是无法通过的: 9 | 10 | ```js 11 | it('gives the old non-collection value to listeners', function() { 12 | scope.aValue = 42; 13 | var oldValueGiven; 14 | scope.$watchCollection( 15 | function(scope) { return scope.aValue; }, 16 | function(newValue, oldValue, scope) { 17 | oldValueGiven = oldValue; 18 | } 19 | ); 20 | scope.$digest(); 21 | scope.aValue = 43; 22 | scope.$digest(); 23 | expect(oldValueGiven).toBe(42); 24 | }); 25 | ``` 26 | 27 | 这是一个对 Array 的测试用例: 28 | 29 | ```js 30 | it('gives the old array value to listeners', function() { 31 | scope.aValue = [1, 2, 3]; 32 | var oldValueGiven; 33 | scope.$watchCollection( 34 | function(scope) { return scope.aValue; }, 35 | function(newValue, oldValue, scope) { 36 | oldValueGiven = oldValue; 37 | } 38 | ); 39 | scope.$digest(); 40 | scope.aValue.push(4); 41 | scope.$digest(); 42 | expect(oldValueGiven).toEqual([1, 2, 3]); 43 | }); 44 | ``` 45 | 46 | 还有一个对 Object 的测试用例: 47 | 48 | ```js 49 | it('gives the old object value to listeners', function() { 50 | scope.aValue = {a: 1, b: 2}; 51 | var oldValueGiven; 52 | scope.$watchCollection( 53 | function(scope) { return scope.aValue; }, 54 | function(newValue, oldValue, scope) { 55 | oldValueGiven = oldValue; 56 | } 57 | ); 58 | scope.$digest(); 59 | scope.aValue.c = 3; 60 | scope.$digest(); 61 | expect(oldValueGiven).toEqual({a: 1, b: 2}); 62 | }); 63 | ``` 64 | 65 | 目前值的比较和复制的实现工作不错, 因此我们不想改变它. 我们引入一个变量, 叫它 veryOldValue, 它会保持一个旧集合的 copy, 并且不会 innernalWatchFn 改变它. 66 | 67 | 维护 veryOldValue 需要 copy 数组或对象, 这是非常昂贵的. 我们不用每次都 copy 整个集合. 只在我们需要的时候, 进行 copy. 68 | 69 | 我们检查看看用户传给我们的 listener 函数是否需要至少两个参数: 70 | 71 | ```js 72 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 73 | var self = this; 74 | var newValue; 75 | var oldValue; 76 | var oldLength; 77 | var veryOldValue; 78 | var trackVeryOldValue = (listenerFn.length > 1); 79 | var changeCount = 0; 80 | // ... 81 | }; 82 | ``` 83 | 84 | 函数的 length 属性包含函数声明的参数个数, 如果它大于 1, 也就是 (newValue, oldValue), 或 (newValue, oldValue, scope), 只有需要的时候, 我们才去追踪 85 | veryOldValue. 86 | 87 | 注意, 这不会引起复制 veryOldValue 的代价, 除非你在 listener 函数中声明 oldValue 参数, 也不会映射到 listener 函数的 arguments 对象中. 实际上, 你需要声明它 88 | 89 | 剩下的工作, 发生在 internalListenerFn, 用 veryOldValue 替换 oldValue, 接着需要 copy 当前值作为下一次的 veryOldValue. 90 | 91 | ```js 92 | var internalListenerFn = function() { 93 | listenerFn(newValue, veryOldValue, self); 94 | if (trackVeryOldValue) { 95 | veryOldValue = _.clone(newValue); 96 | } 97 | }; 98 | ``` 99 | 100 | 在章节 1 我们讨论了 oldValue 在 listener 函数第一次调用时值, 对这次调用, 它必须被定义成 newValue. 101 | 102 | ```js 103 | it('uses the new value as the old value on frst digest', function() { 104 | scope.aValue = {a: 1, b: 2}; 105 | var oldValueGiven; 106 | scope.$watchCollection( 107 | function(scope) { return scope.aValue; }, 108 | function(newValue, oldValue, scope) { 109 | oldValueGiven = oldValue; 110 | } 111 | ); 112 | scope.$digest(); 113 | expect(oldValueGiven).toEqual({a: 1, b: 2}); 114 | }); 115 | ``` 116 | 117 | 这个测试不会通过, 因为 oldValue 是 undefined 的, 因为在第一次调用前, 我们还没有给 veryOldValue 赋值, 需要设置一个标志标示我们是否在第一次调用中, 基于它对 listener 进行不同的调用: 118 | 119 | ```js 120 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 121 | var self = this; 122 | var newValue; 123 | var oldValue; 124 | var oldLength; 125 | var veryOldValue; 126 | var trackVeryOldValue = (listenerFn.length > 1); 127 | var changeCount = 0; 128 | var frstRun = true; 129 | // ... 130 | var internalListenerFn = function() { 131 | if (frstRun) { 132 | listenerFn(newValue, newValue, self); 133 | frstRun = false; 134 | } else { 135 | listenerFn(newValue, veryOldValue, self); 136 | } 137 | if (trackVeryOldValue) { 138 | veryOldValue = _.clone(newValue); 139 | } 140 | }; 141 | return this.$watch(internalWatchFn, internalListenerFn); 142 | }; 143 | ``` 144 | -------------------------------------------------------------------------------- /chapter4/Preventing-Unnecessary-Object-Iteration.md: -------------------------------------------------------------------------------- 1 | ### 阻止非必要的对象遍历 2 | 3 | 现在, 我们对 Object 进行了两次遍历, 对于一个非常巨大的 Object, 它的开销是非常昂贵的. 4 | Since we’re working within a watch function that gets executed in every single digest, we need to take care not to do too much work. 5 | 6 | 基于这个原因, 我们对 Object 变化检测应用一个重要的优化. 7 | 8 | 首先, 我们将持续跟踪新旧对象的大小: 9 | 10 | - 对于老对象, 我们保持一个变量, 当添加属性, 变量自增, 当删除属性, 变量自减. 11 | 12 | - 对于新对象, 我们计算它的大小, 在第一个循环中. 13 | 14 | 当第一次循环结束时, 我们就知道当前两个对象的大小. 然后, 如果旧对象的大小大于新对象的, 继续第二个循环. 如果大小相等, 跳过第二个循环. 15 | 16 | ```js 17 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 18 | var self = this; 19 | var newValue; 20 | var oldValue; 21 | var oldLength; 22 | var changeCount = 0; 23 | var internalWatchFn = function(scope) { 24 | var newLength; 25 | newValue = watchFn(scope); 26 | if (_.isObject(newValue)) { 27 | if (isArrayLike(newValue)) { 28 | if (!_.isArray(oldValue)) { 29 | changeCount++; 30 | oldValue = []; 31 | } 32 | if (newValue.length !== oldValue.length) { 33 | changeCount++; 34 | oldValue.length = newValue.length; 35 | } 36 | _.forEach(newValue, function(newItem, i) { 37 | var bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]); 38 | if (!bothNaN && newItem !== oldValue[i]) { 39 | changeCount++; 40 | oldValue[i] = newItem; 41 | } 42 | }); 43 | } else { 44 | if (!_.isObject(oldValue) || isArrayLike(oldValue)) { 45 | changeCount++; 46 | oldValue = {}; 47 | oldLength = 0; 48 | } 49 | newLength = 0; 50 | _.forOwn(newValue, function(newVal, key) { 51 | newLength++; 52 | if (oldValue.hasOwnProperty(key)) { 53 | var bothNaN = _.isNaN(newVal) && _.isNaN(oldValue[key]); 54 | if (!bothNaN && oldValue[key] !== newVal) { 55 | changeCount++; 56 | oldValue[key] = newVal; 57 | } 58 | } else { 59 | changeCount++; 60 | oldLength++; 61 | oldValue[key] = newVal; 62 | } 63 | }); 64 | if (oldLength > newLength) { 65 | changeCount++; 66 | _.forOwn(oldValue, function(oldVal, key) { 67 | if (!newValue.hasOwnProperty(key)) { 68 | oldLength--; 69 | delete oldValue[key]; 70 | } 71 | }); 72 | } 73 | } 74 | } else { 75 | if (!self.$$areEqual(newValue, oldValue, false)) { 76 | changeCount++; 77 | } 78 | oldValue = newValue; 79 | } 80 | return changeCount; 81 | }; 82 | 83 | var internalListenerFn = function() { 84 | listenerFn(newValue, oldValue, self); 85 | }; 86 | 87 | return this.$watch(internalWatchFn, internalListenerFn); 88 | }; 89 | ``` 90 | 91 | 注意: 我们处理添加新属性和改变属性是不同的, 因为添加新属性, 需要为 oldLength 变量进行自增. 92 | -------------------------------------------------------------------------------- /chapter4/Setting-Up-The-Infrastructure.md: -------------------------------------------------------------------------------- 1 | ### 设置基础设施 2 | 3 | 让我们在 Scope 上创建一个 $watchCollection 函数存根. 4 | 5 | 该函数写法与 $watch 方法非常相似: 它需要 watch 函数(返回我们想要监控的数据)和 listener 函数(当监控的数据发生变化时, 该函数被调用)作为参数. 6 | 7 | 在它的内部, 用两个本地版本的 watch 函数和 listener 函数作为参数调用 $watch 函数. 8 | 9 | ```js 10 | Scope.prototype.$watchCollection = function(watchFn, listenerFn) { 11 | var internalWatchFn = function(scope) { 12 | }; 13 | var internalListenerFn = function() { 14 | }; 15 | return this.$watch(internalWatchFn, internalListenerFn); 16 | }; 17 | ``` 18 | 19 | 你或许会想 $watch 函数返回一个具有删除该监听器的函数. 通过返回的函数, $watchCollection 也具备这样的可能. 20 | 21 | 让我们为测试设置一个 describe 块, 就和之前的章节做的一样. 22 | 23 | ```js 24 | describe('$watchCollection', function() { 25 | var scope; 26 | beforeEach(function() { 27 | scope = new Scope(); 28 | }); 29 | }); 30 | ``` 31 | -------------------------------------------------------------------------------- /chapter5/Additional-Listener-Arguments.md: -------------------------------------------------------------------------------- 1 | ### listener 函数的额外参数 2 | 3 | 当我们发射(emit)或广播(broadcast)一个事件, 事件名称本身并不足以沟通所发生事情的一切. 为事件添加相关的额外参数是非常常见的. 你只需要在事件名称参数之后添加任意数量的参数就行. 4 | 5 | ```js 6 | aScope.$emit('eventName', 'and', 'additional', 'arguments'); 7 | ``` 8 | 9 | 我们需要把这些参数传递给 listener 函数. 它们应该接收到这些参数, 相应的, 额外的参数出现在事件对象参数之后. 10 | 11 | ```js 12 | it('passes additional arguments to listeners on '+method, function() { 13 | var listener = jasmine.createSpy(); 14 | scope.$on('someEvent', listener); 15 | scope[method]('someEvent', 'and', ['additional', 'arguments'], '...'); 16 | expect(listener.calls.mostRecent().args[1]).toEqual('and'); 17 | expect(listener.calls.mostRecent().args[2]).toEqual(['additional', 'arguments']); 18 | expect(listener.calls.mostRecent().args[3]).toEqual('...'); 19 | }); 20 | ``` 21 | 22 | 在 $emit 和 $broacast 中, 我们抓取传给函数的额外参数, 并将它们作为参数传递给 $$fireEventOnScope. 我们可以使用 LoDash 的 _.tail 函数获得额外的参数. 23 | 24 | ```js 25 | Scope.prototype.$emit = function(eventName) { 26 | var additionalArgs = _.tail(arguments); 27 | this.$$ reEventOnScope(eventName, additionalArgs); 28 | }; 29 | 30 | Scope.prototype.$broadcast = function(eventName) { 31 | var additionalArgs = _.tail(arguments); 32 | this.$$ reEventOnScope(eventName, additionalArgs); 33 | }; 34 | ``` 35 | 36 | 在 $$fireEventOnScope 中, 我们不能将额外的参数简单的传递给 listener 函数. 那是因为 listener 期望额外的参数不是一个简单的数组而是正常的函数参数. 因此, 我们需要使用 apply 调用 listener 函数. 37 | 38 | ```js 39 | Scope.prototype.$$fireEventOnScope = function(eventName, additionalArgs) { 40 | var event = {name: eventName}; 41 | var listenerArgs = [event].concat(additionalArgs); 42 | var listeners = this.$$listeners[eventName] || []; 43 | _.forEach(listeners, function(listener) { 44 | listener.apply(null, listenerArgs); 45 | }); 46 | }; 47 | ``` 48 | -------------------------------------------------------------------------------- /chapter5/Broadcasting-Down-The-Scope-Hierarchy.md: -------------------------------------------------------------------------------- 1 | ### 向下传播事件 2 | 3 | 基本上 $broadcast 就是 $emit 的镜像: 它也是调用当前 Scope 以及当前 Scope 直接的, 间接的, 无论是否隔离的子 Scope 上的 listener. 4 | 5 | ```js 6 | it('propagates down the scope hierarchy on $broadcast', function() { 7 | var scopeListener = jasmine.createSpy(); 8 | var childListener = jasmine.createSpy(); 9 | var isolatedChildListener = jasmine.createSpy(); 10 | scope.$on('someEvent', scopeListener); 11 | child.$on('someEvent', childListener); 12 | isolatedChild.$on('someEvent', isolatedChildListener); 13 | scope.$broadcast('someEvent'); 14 | expect(scopeListener).toHaveBeenCalled(); 15 | expect(childListener).toHaveBeenCalled(); 16 | expect(isolatedChildListener).toHaveBeenCalled(); 17 | }); 18 | ``` 19 | 20 | 为了测试的完整性, 在 $broadcast 传播事件时, 让我们也确保一个相同的事件对象传给所有的 listener 函数. 21 | 22 | ```js 23 | it('propagates the same event down on $broadcast', function() { 24 | var scopeListener = jasmine.createSpy(); 25 | var childListener = jasmine.createSpy(); 26 | scope.$on('someEvent', scopeListener); 27 | child.$on('someEvent', childListener); 28 | scope.$broadcast('someEvent'); 29 | var scopeEvent = scopeListener.calls.mostRecent().args[0]; 30 | var childEvent = childListener.calls.mostRecent().args[0]; 31 | expect(scopeEvent).toBe(childEvent); 32 | }); 33 | ``` 34 | 35 | 在 $broadcast 中遍历 Scope 不像在 $emit 中那么简单明确, 因为不是一条直接向下的路径, 反而所以的 Scope 分散在一个树状结构. 我需要做的是遍历这个树. 36 | 更准确的说, 我们需要使用与在 $$digestOnce 中一样的深度优先算法去遍历树. 我们可以重用第二章引入的 $$everyScope 方法. 37 | 38 | ```js 39 | Scope.prototype.$broadcast = function(eventName) { 40 | var event = {name: eventName}; 41 | var listenerArgs = [event].concat(_.tail(arguments)); 42 | this.$$everyScope(function(scope) { 43 | scope.$$fireEventOnScope(eventName, listenerArgs); 44 | return true; 45 | }); 46 | return event; 47 | }; 48 | ``` 49 | 50 | 现在很显然, 为什么 broadcast 一个事件可能会比 emit 一个事件昂贵: emit 在一个直线上向上传播事件, 通常 Scope 的层级也不会很深. 但是 broadcast 却要遍历一个树. 如果在 Root Scope 上 broadcast, 将要访问整个应用的每一个 Scope. 51 | -------------------------------------------------------------------------------- /chapter5/Broadcasting-Scope-Removal.md: -------------------------------------------------------------------------------- 1 | ### 广播 Scope 删除 2 | 3 | 有时候知道 Scope 被删除是很有用的. 一个典型的应用场景就是在指令中你可能设置了一些 DOM 事件 listener 和其他引用, 当指令的元素销毁时, 需要被清理. 解决方案是在指令的 Scope 中监听一个叫做 $destroy 的事件. 4 | 5 | $destroy 事件来自哪里呢? 当一个 Scope 被删除的时候, 也就是当某人调用 Scope 的 $destroy 方法时, 我们应当触发它. 6 | 7 | ```js 8 | it('fres $destroy when destroyed', function() { 9 | var listener = jasmine.createSpy(); 10 | scope.$on('$destroy', listener); 11 | scope.$destroy(); 12 | expect(listener).toHaveBeenCalled(); 13 | }); 14 | ``` 15 | 16 | 当一个 Scope 被删除的时候, 它的所有子 Scope 也被删除了, 它们的 listener 也应该接收到 $destroy 事件. 17 | 18 | ```js 19 | it('fres $destroy on children destroyed', function() { 20 | var listener = jasmine.createSpy(); 21 | child.$on('$destroy', listener); 22 | scope.$destroy(); 23 | expect(listener).toHaveBeenCalled(); 24 | }); 25 | ``` 26 | 27 | 我们如何让它工作呢? 正好有一个函数可以满足在 Scope 和它的子 Scope 中触发事件的需求: $broadcast. 我们需要在 $destroy 函数中使用 $broadcast 广播 $destroy 事件. 28 | 29 | ```js 30 | Scope.prototype.$destroy = function() { 31 | this.$broadcast('$destroy'); 32 | if (this.$parent) { 33 | var siblings = this.$parent.$$children; 34 | var indexOfThis = siblings.indexOf(this); 35 | if (indexOfThis >= 0) { 36 | siblings.splice(indexOfThis, 1); 37 | } 38 | } 39 | this.$$watchers = null; 40 | }; 41 | ``` 42 | -------------------------------------------------------------------------------- /chapter5/Dealing-with-Duplication.md: -------------------------------------------------------------------------------- 1 | ### 处理重复代码 2 | 3 | 我们定义了两个几乎相同的测试和两个相同的函数. 很明显两种事件传播机制会有许多相似. 让我们在进行下一步之前处理下重复代码: 4 | 5 | 对于事件方法本身, 我们可以抽取共同的行为作为一个函数在 $emit 和 $broadcast 中使用, 让我们称呼它为 $$fireEventOnScope: 6 | 7 | ```js 8 | Scope.prototype.$emit = function(eventName) { 9 | this.$$fireEventOnScope(eventName); 10 | }; 11 | 12 | Scope.prototype.$broadcast = function(eventName) { 13 | this.$$fireEventOnScope(eventName); 14 | }; 15 | 16 | Scope.prototype.$$fireEventOnScope = function(eventName) { 17 | var listeners = this.$$listeners[eventName] || []; 18 | _.forEach(listeners, function(listener) { 19 | listener(); 20 | }); 21 | }; 22 | ``` 23 | 24 | 这样好多了, 但是我们可以更进一步, 消除测试中的重复. 我们可以用一个循环包装公共的功能分别为 $emit 和 $broadcast 运行一次. 在循环体内, 我们可以动态的查找正确的函数, 代替之前添加的测试中的函数: 25 | 26 | ```js 27 | _.forEach(['$emit', '$broadcast'], function(method) { 28 | it('calls listeners registered for matching events on '+method, function() { 29 | var listener1 = jasmine.createSpy(); 30 | var listener2 = jasmine.createSpy(); 31 | scope.$on('someEvent', listener1); 32 | scope.$on('someOtherEvent', listener2); 33 | scope[method]('someEvent'); 34 | expect(listener1).toHaveBeenCalled(); 35 | expect(listener2).not.toHaveBeenCalled(); 36 | }); 37 | }); 38 | ``` 39 | 40 | 因为 Jasmine 的 describe 块也是函数, 我们可以在它们中运行任意代码. 我们的循环有效的为两个 describe 块定义了测试用例. 41 | -------------------------------------------------------------------------------- /chapter5/Deregistering-Event-Listeners.md: -------------------------------------------------------------------------------- 1 | ### 注销事件监听器 2 | 3 | 在我们进入 $emit 和 $broadcast 之间的区别之前, 让我们来看一个重要的共同需求: 不仅可以注册事件监听器, 而且可以注销事件监听器. 4 | 5 | 注销事件监听器的原理和注销 watcher 是一样的, 回到第一章的实现: The registration function returns a deregistration function. 一旦注销函数被调用, 事件监听器不会接收任何事件: 6 | 7 | ```js 8 | it('can be deregistered '+method, function() { 9 | var listener = jasmine.createSpy(); 10 | var deregister = scope.$on('someEvent', listener); 11 | deregister(); 12 | scope[method]('someEvent'); 13 | expect(listener).not.toHaveBeenCalled(); 14 | }); 15 | ``` 16 | 17 | 一个简单的删除实现就如同在 $watch 中做的一样 - 只要从集合中 splice 监听函数: 18 | 19 | ```js 20 | Scope.prototype.$on = function(eventName, listener) { 21 | var listeners = this.$$listeners[eventName]; 22 | if (!listeners) { 23 | this.$$listeners[eventName] = listeners = []; 24 | } 25 | listeners.push(listener); 26 | return function() { 27 | var index = listeners.indexOf(listener); 28 | if (index >= 0) { 29 | listeners.splice(index, 1); 30 | } 31 | }; 32 | }; 33 | ``` 34 | 35 | 这里有一个特殊的情况, 我们必须要小心, 无论如何: 在监听器触发时删除自己是非常常见的, 举例当我们只调用 listener 一次. 这种删除发生在循环 listeners 数组时, 会导致跳过一个 listener. 36 | 37 | ```js 38 | it('does not skip the next listener when removed on '+method, function() { 39 | var deregister; 40 | var listener = function() { 41 | deregister(); 42 | }; 43 | var nextListener = jasmine.createSpy(); 44 | deregister = scope.$on('someEvent', listener); 45 | scope.$on('someEvent', nextListener); 46 | scope[method]('someEvent'); 47 | expect(nextListener).toHaveBeenCalled(); 48 | }); 49 | ``` 50 | 51 | 意思就是你不能简单的直接过去删除掉 listener. 我们能做的是标识出 listener 已经删除了, null 就很好. 52 | 53 | ```js 54 | Scope.prototype.$on = function(eventName, listener) { 55 | var listeners = this.$$listeners[eventName]; 56 | if (!listeners) { 57 | this.$$listeners[eventName] = listeners = []; 58 | } 59 | listeners.push(listener); 60 | return function() { 61 | var index = listeners.indexOf(listener); 62 | if (index >= 0) { 63 | listeners[index] = null; 64 | } 65 | }; 66 | }; 67 | ``` 68 | 69 | 然后, 在循环 listener 时, 我们检测 listener 如果为 null, splices listener. 我们要做就是从使用 _.forEach 切换到手动 while 循环. 70 | 71 | ```js 72 | Scope.prototype.$$fireEventOnScope = function(eventName, additionalArgs) { 73 | var event = {name: eventName}; 74 | var listenerArgs = [event].concat(additionalArgs); 75 | var listeners = this.$$listeners[eventName] || []; 76 | var i = 0; 77 | while (i < listeners.length) { 78 | if (listeners[i] === null) { 79 | listeners.splice(i, 1); 80 | } else { 81 | listeners[i].apply(null, listenerArgs); 82 | i++; 83 | } 84 | } 85 | return event; 86 | }; 87 | ``` 88 | -------------------------------------------------------------------------------- /chapter5/Disabling-Listeners-On-Destroyed-Scopes.md: -------------------------------------------------------------------------------- 1 | ### 在销毁 Scope 时, 使 listener 失效 2 | 3 | 除了触发 $destroy 事件之外, 销毁 Scope 的另一个效果是它的事件监听器不再有效: 4 | 5 | ```js 6 | it('no longers calls listeners after destroyed', function() { 7 | var listener = jasmine.createSpy(); 8 | scope.$on('myEvent', listener); 9 | scope.$destroy(); 10 | scope.$emit('myEvent'); 11 | expect(listener).not.toHaveBeenCalled(); 12 | }); 13 | ``` 14 | 15 | 我们可以将 $$listeners 对象重新设置成一个空对象, 这样做实际上将丢掉所有已存在的 listener. 16 | 17 | ```js 18 | Scope.prototype.$destroy = function() { 19 | this.$broadcast('$destroy'); 20 | if (this.$parent) { 21 | var siblings = this.$parent.$$children; 22 | var indexOfThis = siblings.indexOf(this); 23 | if (indexOfThis >= 0) { 24 | siblings.splice(indexOfThis, 1); 25 | } 26 | } 27 | this.$$watchers = null; 28 | this.$$listeners = {}; 29 | }; 30 | ``` -------------------------------------------------------------------------------- /chapter5/Emitting-Up-The-Scope-Hierarchy.md: -------------------------------------------------------------------------------- 1 | ### 向上传播事件 2 | 3 | 现在我们终于到达 $emit 和 $broadcast 之间的差异这部分了: 事件在 Scope 层级上的传播方向是不同的. 4 | 5 | 当你发射(emit)一个事件, 该事件会传给当前 Scope 上对应的事件监听器, 然后向上继续传播给所有父级 Scope, 包括 Root Scope 上的事件监听器. 6 | 7 | 测试必须从早先创建的 forEach 循环中出来, 因为它只包含 $emit: 8 | 9 | ```js 10 | it('propagates up the scope hierarchy on $emit', function() { 11 | var parentListener = jasmine.createSpy(); 12 | var scopeListener = jasmine.createSpy(); 13 | parent.$on('someEvent', parentListener); 14 | scope.$on('someEvent', scopeListener); 15 | scope.$emit('someEvent'); 16 | expect(scopeListener).toHaveBeenCalled(); 17 | expect(parentListener).toHaveBeenCalled(); 18 | }); 19 | ``` 20 | 21 | 让我们尝试尽可能简单的实现, 在 $emit 方法中遍历 Scope. 我们可以使用第二章中引入的 $parent 属性获取每个 Scope 的父 Scope. 22 | 23 | ```js 24 | Scope.prototype.$emit = function(eventName) { 25 | var additionalArgs = _.tail(arguments); 26 | var scope = this; 27 | do { 28 | scope.$$fireEventOnScope(eventName, additionalArgs); 29 | scope = scope.$parent; 30 | } while (scope); 31 | }; 32 | ``` 33 | 34 | 上面的实现几乎没有问题, 除了破坏了之前约定的, 调用 $emit 返回事件对象, 而且还有我们讨论过的为每个 listener 函数传入相同的事件对象参数. 这个需求在不同的 Scope 上都是适用的, 但是下面的测试没有通过: 35 | 36 | ```js 37 | it('propagates the same event up on $emit', function() { 38 | var parentListener = jasmine.createSpy(); 39 | var scopeListener = jasmine.createSpy(); 40 | parent.$on('someEvent', parentListener); 41 | scope.$on('someEvent', scopeListener); 42 | scope.$emit('someEvent'); 43 | var scopeEvent = scopeListener.calls.mostRecent().args[0]; 44 | var parentEvent = parentListener.calls.mostRecent().args[0]; 45 | expect(scopeEvent).toBe(parentEvent); 46 | }); 47 | ``` 48 | 49 | 这意味着我们需要撤销之前去除重复代码的工作, 在 $emit 和 $broadcast 中构造事件对象, 然后传入到 $$fireEventOnScope 函数中. 既然已经这样了, 让我们将整个 listener 的参数的构造从 $$fireEventScope 中拉出来.这样的话, 我们不必为每个 Scope 构造事件对象了. 50 | 51 | ```js 52 | Scope.prototype.$emit = function(eventName) { 53 | var event = {name: eventName}; 54 | var listenerArgs = [event].concat(_.tail(arguments)); 55 | var scope = this; 56 | do { 57 | scope.$$freEventOnScope(eventName, listenerArgs); 58 | scope = scope.$parent; 59 | } while (scope); 60 | return event; 61 | }; 62 | 63 | Scope.prototype.$broadcast = function(eventName) { 64 | var event = {name: eventName}; 65 | var listenerArgs = [event].concat(_.tail(arguments)); 66 | this.$$freEventOnScope(eventName, listenerArgs); 67 | return event; 68 | }; 69 | 70 | Scope.prototype.$$freEventOnScope = function(eventName, listenerArgs) { 71 | var listeners = this.$$listeners[eventName] || []; 72 | var i = 0; 73 | while (i < listeners.length) { 74 | if (listeners[i] === null) { 75 | listeners.splice(i, 1); 76 | } else { 77 | listeners[i].apply(null, listenerArgs); 78 | i++; 79 | } 80 | } 81 | }; 82 | ``` 83 | 84 | 我们在这里引入一点重复代码, 但是还不是太糟糕. 85 | -------------------------------------------------------------------------------- /chapter5/Event-Objects.md: -------------------------------------------------------------------------------- 1 | ### 事件对象 2 | 3 | 当前, 我们调用 listener 函数时没有带任何参数, 但是 AngularJS 不是这样做的. 我们应该如何做: 为 listener 函数传入一个事件对象. 4 | 5 | 这个事件对象其实就是一个携带与事件相关的信息和行为的常规 JavaScript 对象. 我们会在该事件对象上添加一些属性, 但首先, 为事件对象添加一个记录事件的名称的 name 属性. 6 | 7 | ```js 8 | it('passes an event object with a name to listeners on '+method, function() { 9 | var listener = jasmine.createSpy(); 10 | scope.$on('someEvent', listener); 11 | scope[method]('someEvent'); 12 | expect(listener).toHaveBeenCalled(); 13 | expect(listener.calls.mostRecent().args[0].name).toEqual('someEvent'); 14 | }); 15 | ``` 16 | 17 | 事件对象的另一个重要方面是完全相同的事件对象作为参数被传入到每个 listener 函数中. 应用开发者可以向事件对象上添加额外的属性在 listener 函数之间进行通信. 18 | 19 | ```js 20 | it('passes the same event object to each listener on '+method, function() { 21 | var listener1 = jasmine.createSpy(); 22 | var listener2 = jasmine.createSpy(); 23 | scope.$on('someEvent', listener1); 24 | scope.$on('someEvent', listener2); 25 | scope[method]('someEvent'); 26 | var event1 = listener1.calls.mostRecent().args[0]; 27 | var event2 = listener2.calls.mostRecent().args[0]; 28 | expect(event1).toBe(event2); 29 | }); 30 | ``` 31 | 32 | 我们可以在 $$fireEventOnScope 函数中构造这个事件对象, 并将它作为参数传入 listener 函数: 33 | 34 | ```js 35 | Scope.prototype.$$fireEventOnScope = function(eventName) { 36 | var event = {name: eventName}; 37 | var listeners = this.$$listeners[eventName] || []; 38 | _.forEach(listeners, function(listener) { 39 | listener(event); 40 | }); 41 | }; 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /chapter5/Handling-Exceptions.md: -------------------------------------------------------------------------------- 1 | ### 异常处理 2 | 3 | 还有一件事情需要我们去做. 就是处理异常的发生. 当一个 listener 做一些引起异常的事情, 此时不应该停止事件的传播. 当前的实现不但没有那样做, 而且还将所有异常抛出. 4 | 5 | ```js 6 | it('does not stop on exceptions on '+method, function() { 7 | var listener1 = function(event) { 8 | throw 'listener1 throwing an exception'; 9 | }; 10 | var listener2 = jasmine.createSpy(); 11 | scope.$on('someEvent', listener1); 12 | scope.$on('someEvent', listener2); 13 | scope[method]('someEvent'); 14 | expect(listener2).toHaveBeenCalled(); 15 | }); 16 | ``` 17 | 18 | 就和 watch, $evalAsync 和 $$postDigest 函数一样, 需要用 try..catch 语句块包裹每一个 listener 的调用并处理异常. 目前仅仅在 console 中打印出来. 19 | 20 | ```js 21 | Scope.prototype.$$freEventOnScope = function(eventName, listenerArgs) { 22 | var listeners = this.$$listeners[eventName] || []; 23 | var i = 0; 24 | while (i < listeners.length) { 25 | if (listeners[i] === null) { 26 | listeners.splice(i, 1); 27 | } else { 28 | try { 29 | listeners[i].apply(null, listenerArgs); 30 | } catch (e) { 31 | console.error(e); 32 | } 33 | i++; 34 | } 35 | } 36 | }; 37 | ``` 38 | -------------------------------------------------------------------------------- /chapter5/Including-The-Current-And-Target-Scopes-in-The-Event-Object.md: -------------------------------------------------------------------------------- 1 | ### 在事件对象中包含当前 Scope 和目标 Scope 2 | 3 | 此时, 我们的事件对象仅仅包含一个属性: 事件名称, 下一步, 我们准备在事件对象上捆绑更多信息. 4 | 5 | 如果你对浏览器中的 DOM 事件非常熟悉, 你会知道事件对象会有几个非常有用的属性: target 定义发生事件的 DOM 元素, currentTarget 定义注册事件处理的 DOM 元素. 6 | 7 | 因为 DOM 事件在 DOM 树上传播, 所以 target 和 currentTarget 有可能是不一样的. 8 | 9 | Angular Scope 有一对类似的属性: targetScope 定义事件发生的 Scope, currentScope 定义注册 listener 的 Scope. 并且因为 Scope 事件在 Scope 树上下传播, 所以 targetScope 和 currentScope 也有可能是不同的. 10 | 11 | | | Event originated in | Listener attached in | 12 | | ------------ | --------------------- | ---------------------- | 13 | | DOM Events | target | currentTarget | 14 | | Scope Events | targetScope | currentScope | 15 | 16 | 开始 targetScope, 它指向相同的 Scope 与注册的 listener 无关. 17 | 18 | 对于 $emit: 19 | 20 | ```js 21 | it('attaches targetScope on $emit', function() { 22 | var scopeListener = jasmine.createSpy(); 23 | var parentListener = jasmine.createSpy(); 24 | scope.$on('someEvent', scopeListener); 25 | parent.$on('someEvent', parentListener); 26 | scope.$emit('someEvent'); 27 | expect(scopeListener.calls.mostRecent().args[0].targetScope).toBe(scope); 28 | expect(parentListener.calls.mostRecent().args[0].targetScope).toBe(scope); 29 | }); 30 | ``` 31 | 32 | 对于 $broadcast: 33 | 34 | ```js 35 | it('attaches targetScope on $broadcast', function() { 36 | var scopeListener = jasmine.createSpy(); 37 | var childListener = jasmine.createSpy(); 38 | scope.$on('someEvent', scopeListener); 39 | child.$on('someEvent', childListener); 40 | scope.$broadcast('someEvent'); 41 | expect(scopeListener.calls.mostRecent().args[0].targetScope).toBe(scope); 42 | expect(childListener.calls.mostRecent().args[0].targetScope).toBe(scope); 43 | }); 44 | ``` 45 | 46 | 为了让测试通过, 我们所要做的是, 在 $emit 和 $broadcast 中, 将 this 作为 targetScope 添加到事件对象中. 47 | 48 | ```js 49 | Scope.prototype.$emit = function(eventName) { 50 | var event = {name: eventName, targetScope: this}; 51 | var listenerArgs = [event].concat(_.tail(arguments)); 52 | var scope = this; 53 | do { 54 | scope.$$ reEventOnScope(eventName, listenerArgs); 55 | scope = scope.$parent; 56 | } while (scope); 57 | return event; 58 | }; 59 | 60 | Scope.prototype.$broadcast = function(eventName) { 61 | var event = {name: eventName, targetScope: this}; 62 | var listenerArgs = [event].concat(_.tail(arguments)); 63 | this.$$everyScope(function(scope) { 64 | scope.$$ reEventOnScope(eventName, listenerArgs); 65 | return true; 66 | }); 67 | return event; 68 | }; 69 | ``` 70 | 71 | 相反地, currentScope 的不同, 基于 listener 注册在什么 Scope 上. 它应该指向准确的 Scope. 一种方式是当事件在 Scope 层级中上下传播时, currentScope 指向当前正在传播的 Scope. 72 | 73 | 在这个用例中, 我们不能对测试使用 Jasmine spy, 因为 spy 只能调用后验证. currentScope 在遍历 Scope 时是一直在改变, 所以我们当 listener 调用时, 需要随时记录它的值. 74 | 75 | 我们可以用自己的 listener 函数和本地变量搞定: 76 | 77 | 对于 $emit: 78 | 79 | ```js 80 | it('attaches currentScope on $emit', function() { 81 | var currentScopeOnScope, currentScopeOnParent; 82 | var scopeListener = function(event) { 83 | currentScopeOnScope = event.currentScope; 84 | }; 85 | var parentListener = function(event) { 86 | currentScopeOnParent = event.currentScope; 87 | }; 88 | scope.$on('someEvent', scopeListener); 89 | parent.$on('someEvent', parentListener); 90 | scope.$emit('someEvent'); 91 | expect(currentScopeOnScope).toBe(scope); 92 | expect(currentScopeOnParent).toBe(parent); 93 | }); 94 | ``` 95 | 96 | 对于 $broadcast: 97 | 98 | ```js 99 | it('attaches currentScope on $broadcast', function() { 100 | var currentScopeOnScope, currentScopeOnChild; 101 | var scopeListener = function(event) { 102 | currentScopeOnScope = event.currentScope; 103 | }; 104 | var childListener = function(event) { 105 | currentScopeOnChild = event.currentScope; 106 | }; 107 | scope.$on('someEvent', scopeListener); 108 | child.$on('someEvent', childListener); 109 | scope.$broadcast('someEvent'); 110 | expect(currentScopeOnScope).toBe(scope); 111 | expect(currentScopeOnChild).toBe(child); 112 | }); 113 | ``` 114 | 115 | 幸运的是实际的实现要比测试写法简单直接的多. 所有我们需要做的是把当前遍历的 Scope 赋值给 currentScope 116 | 117 | 118 | ```js 119 | Scope.prototype.$emit = function(eventName) { 120 | var event = {name: eventName, targetScope: this}; 121 | var listenerArgs = [event].concat(_.tail(arguments)); 122 | var scope = this; 123 | do { 124 | event.currentScope = scope; 125 | scope.$$fireEventOnScope(eventName, listenerArgs); 126 | scope = scope.$parent; 127 | } while (scope); 128 | return event; 129 | }; 130 | 131 | Scope.prototype.$broadcast = function(eventName) { 132 | var event = {name: eventName, targetScope: this}; 133 | var listenerArgs = [event].concat(_.tail(arguments)); 134 | this.$$everyScope(function(scope) { 135 | event.currentScope = scope; 136 | scope.$$fireEventOnScope(eventName, listenerArgs); 137 | return true; 138 | }); 139 | return event; 140 | }; 141 | ``` 142 | 143 | 因为 currentScope 是用来沟通事件传播的当前状态, 事件传播结束后应该被清除掉. 144 | 145 | ```js 146 | it('sets currentScope to null after propagation on $emit', function() { 147 | var event; 148 | var scopeListener = function(evt) { 149 | event = evt; 150 | }; 151 | scope.$on('someEvent', scopeListener); 152 | scope.$emit('someEvent'); 153 | expect(event.currentScope).toBe(null); 154 | }); 155 | 156 | it('sets currentScope to null after propagation on $broadcast', function() { 157 | var event; 158 | var scopeListener = function(evt) { 159 | event = evt; 160 | }; 161 | scope.$on('someEvent', scopeListener); 162 | scope.$broadcast('someEvent'); 163 | expect(event.currentScope).toBe(null); 164 | }); 165 | ``` 166 | 167 | 通过在 $emit 方法的最后简单设置 currentScope 为 null 就可以完成. 168 | 169 | ```js 170 | Scope.prototype.$emit = function(eventName) { 171 | var event = {name: eventName, targetScope: this}; 172 | var listenerArgs = [event].concat(_.tail(arguments)); 173 | var scope = this; 174 | do { 175 | event.currentScope = scope; 176 | scope.$$fireEventOnScope(eventName, listenerArgs); 177 | scope = scope.$parent; 178 | } while (scope); 179 | event.currentScope = null; 180 | return event; 181 | }; 182 | ``` 183 | 184 | 在 $broadcast 中做同样的处理. 185 | 186 | ```js 187 | Scope.prototype.$broadcast = function(eventName) { 188 | var event = {name: eventName, targetScope: this}; 189 | var listenerArgs = [event].concat(_.tail(arguments)); 190 | this.$$everyScope(function(scope) { 191 | event.currentScope = scope; 192 | scope.$$fireEventOnScope(eventName, listenerArgs); 193 | return true; 194 | }); 195 | event.currentScope = null; 196 | return event; 197 | }; 198 | ``` 199 | 200 | 现在事件监听器可以基于在 Scope 层级上的事件的来源于哪里, 在哪里被监听做出决定. 201 | -------------------------------------------------------------------------------- /chapter5/Preventing-Default-Event-Behavior.md: -------------------------------------------------------------------------------- 1 | ### 阻止事件默认行为 2 | 3 | 除了 stopPropagation 之外, DOM 事件也可以被用另一种方式取消, 这种方式就是阻止事件的默认行为. DOM 事件有一个叫做 preventDefault 的函数就是阻止默认事件的. 4 | 它的目的是阻止浏览器原生事件, 但是仍然允许注册的监听事件执行. 举例, 当 preventDefault 在超链接的 click 事件触发时被调用, 浏览器不会执行超链接的跳转行为, 但是注册的 click 事件仍然会被执行. 5 | 6 | Scope 的事件也有一个 preventDefault 函数. 然而, Scope 事件没有内置的默认行为, 所以调用 preventDefault 函数的作用很小. 它做了一件事, 在事件对象上设置一个叫做 defaultPrevented 的布尔标识. 7 | 这个标识没有改变 Scope 事件系统的行为, 但是它被用于自定义指令中一旦事件完成传播决定是否触发默认行为. 当广播 location 事件时, AngularJS 的 $locationService 就是这样做的. 8 | 9 | 因此, 我们需要一个测试: 当一个 listener 函数, 在 event 对象上调用 preventDefault(), 它的 defaultPrevented 标识会被设置. 这个行为在 $emit 和 $broadcast 中是一致的. 10 | 11 | ```js 12 | it('is sets defaultPrevented when preventDefault called on '+method, function() { 13 | var listener = function(event) { 14 | event.preventDefault(); 15 | }; 16 | scope.$on('someEvent', listener); 17 | var event = scope[method]('someEvent'); 18 | expect(event.defaultPrevented).toBe(true); 19 | }); 20 | ``` 21 | 22 | 这里的实现与 stopPropagation 中我们所做的非常相似: 有一个函数设置布尔标识并将它添加到事件对象上. 不同的是这次布尔标识也被添加到事件对象上, 并且这次不会基于该布尔值做任何决定. 23 | 24 | 对于 $emit: 25 | 26 | ```js 27 | Scope.prototype.$emit = function(eventName) { 28 | var propagationStopped = false; 29 | var event = { 30 | name: eventName, 31 | targetScope: this, 32 | stopPropagation: function() { 33 | propagationStopped = true; 34 | }, 35 | preventDefault: function() { 36 | event.defaultPrevented = true; 37 | } 38 | }; 39 | 40 | var listenerArgs = [event].concat(_.tail(arguments)); 41 | var scope = this; 42 | 43 | do { 44 | event.currentScope = scope; 45 | scope.$$freEventOnScope(eventName, listenerArgs); 46 | scope = scope.$parent; 47 | } while (scope && !propagationStopped); 48 | 49 | return event; 50 | }; 51 | ``` 52 | 53 | 对于 $broadcast: 54 | 55 | ```js 56 | Scope.prototype.$broadcast = function(eventName) { 57 | var event = { 58 | name: eventName, 59 | targetScope: this, 60 | preventDefault: function() { 61 | event.defaultPrevented = true; 62 | } 63 | }; 64 | 65 | var listenerArgs = [event].concat(_.tail(arguments)); 66 | this.$$everyScope(function(scope) { 67 | event.currentScope = scope; 68 | scope.$$freEventOnScope(eventName, listenerArgs); 69 | return true; 70 | }); 71 | return event; 72 | }; 73 | ``` 74 | -------------------------------------------------------------------------------- /chapter5/Publish-Subscribe-Messaging.md: -------------------------------------------------------------------------------- 1 | ### 发布和订阅消息 2 | 3 | Scope 的事件系统本质上是被广泛使用的发布和订阅消息(publish-subscribe messaging)模式的实现: 当事情发生时, 你可以将其作为一个事件在 Scope 上发布信息. 应用的其他部分或许已经订阅并接收该事件, 在这种情况下, 它们会得到通知. 4 | 作为一个发布者(publisher), 你不知道有多少事件订阅者(subscriber), 如果有的话, 该订阅者(subscriber)正在接收事件. 而作为一个订阅者(subscriber), 你不知道事件的来源. Scope 作为一个中间人, 将发布者(publisher)从订阅者(subscriber)中解耦. 5 | 6 | 这种模式有着悠久的历史,并且还在 JavaScript 应用中被广泛采用。 例如,jQuery 提供了一个自定义事件系统,Backbone.js 也是一样。两者都被用于发布和订阅消息. 7 | 8 | Angular 的发布与订阅的实现是在多个方面类似于其它实现的, 但是有一个关键的不同. Angular 的事件系统是合并在 Scope 层级中的. 而不是只有一个所有事件流都通过的单点, 我们有一个 Scope 树, 事件可以进行上下传播. 9 | 10 | 当你在 Scope 发布一个事件, 你可以在两种传播模式中选择: 向上传播还是向下传播. 当你选择向上时, 当前 Scope 和它祖先的 Scope 上的订阅者(subscriber)会得到通知, 这被称为发射(emit)事件. 当你选择向下时, 当前 Scope 和它后代的 Scope 上的订阅者(subscriber)会得到通知, 这被称为广播(broadcast)事件. 11 | 12 | 在本章, 我们将实现这两种传播模式, 实际上, 两者非常相似, 我们将先后实现它们. 这样做突出它们之间的差异. 13 | -------------------------------------------------------------------------------- /chapter5/Registering-Event-Listeners-$on.md: -------------------------------------------------------------------------------- 1 | ### 注册事件监听器: $on 2 | 3 | 你需要注册一个事件, 才能得到事件通知. AngularJS 中的注册完成是通过调用 Scope 对象上的 $on 方法. 这个方法需要两个参数: 事件名称和当事件发生时被调用的监听器函数. 4 | 5 | 通过 $on 注册的监听器可以接收发射和广播事件. 实际上除了事件名称, 对事件的接收没有任何方式的限制. 6 | 7 | $on 方法应该做些什么呢? 它应该在某个地方存放监听器函数, 当事件发生时, 可以找到对应的监听器函数. 对于如何存放, 我们将一个对象放在 $$listeners 属性上, 对象的键是事件名称, 值是一个存放为特定事件注册的监听器函数的数组. 所以, 我们可以测试用 $on 注册的函数是否被相应存储: 8 | 9 | ```js 10 | it('allows registering listeners', function() { 11 | var listener1 = function() { }; 12 | var listener2 = function() { }; 13 | var listener3 = function() { }; 14 | scope.$on('someEvent', listener1); 15 | scope.$on('someEvent', listener2); 16 | scope.$on('someOtherEvent', listener3); 17 | expect(scope.$$listeners).toEqual({ 18 | someEvent: [listener1, listener2], 19 | someOtherEvent: [listener3] 20 | }); 21 | }); 22 | ``` 23 | 24 | 我们需要将 $$listeners 对象添加到 Scope 上. 让我们在 constructor 设置一下: 25 | 26 | ```js 27 | function Scope() { 28 | this.$$watchers = []; 29 | this.$$lastDirtyWatch = null; 30 | this.$$asyncQueue = []; 31 | this.$$applyAsyncQueue = []; 32 | this.$$applyAsyncId = null; 33 | this.$$postDigestQueue = []; 34 | this.$root = this; 35 | this.$$children = []; 36 | this.$$listeners = {}; 37 | this.$$phase = null; 38 | } 39 | ``` 40 | 41 | $on 函数应该检查在给定事件上是否已经存在一个 listener 集合, 如果没有, 初始化一个. 有的话, 在集合上添加一个新的 listener 函数. 42 | 43 | ```js 44 | Scope.prototype.$on = function(eventName, listener) { 45 | var listeners = this.$$listeners[eventName]; 46 | if (!listeners) { 47 | this.$$listeners[eventName] = listeners = []; 48 | } 49 | listeners.push(listener); 50 | }; 51 | ``` 52 | 53 | 因为在 Scope 层级上的每个监听器位置是相同的, 当前的 $$listeners 的实现确实有一个小问题: 在整个 Scope 层级上的所有监听器会被添加到同一个 $$listeners 集合中. 相反的, 我们需要为每个 Scope 添加单独的 $$listeners 集合. 54 | 55 | ```js 56 | it('registers different listeners for every scope', function() { 57 | var listener1 = function() { }; 58 | var listener2 = function() { }; 59 | var listener3 = function() { }; 60 | scope.$on('someEvent', listener1); 61 | child.$on('someEvent', listener2); 62 | isolatedChild.$on('someEvent', listener3); 63 | expect(scope.$$listeners).toEqual({someEvent: [listener1]}); 64 | expect(child.$$listeners).toEqual({someEvent: [listener2]}); 65 | expect(isolatedChild.$$listeners).toEqual({someEvent: [listener3]}); 66 | }); 67 | ``` 68 | 69 | This test fails because both scope and child actually have a reference to the same $$listeners 70 | collection and isolatedChild doesn’t have one at all. We need to tweak the child scope constructor to explicitly give each new child scope its own $$listeners collection. For a non-isolated scope it will shadow the one in its parent. This is exactly the same solution as we used for 71 | $$watchers in Chapter 2: 72 | 73 | 这个测试失败了, 因为 Scope 和子 Scope 实际上共同拥有相同 $$listeners 集合的引用, 并且隔离的子 Scope 完全没有. 我们需要调整一下子 Scope 的构造函数: 显示地给每个新的子 Scope 添加一个它自己的 $$listeners 集合. 对于非隔离 scope, $$listeners 属性会遮蔽它父 Scope 的. 这与第二章中我们为 $$watcher 使用的解决方案是一样的. 74 | 75 | ```js 76 | Scope.prototype.$new = function(isolated, parent) { 77 | var child; 78 | parent = parent || this; 79 | if (isolated) { 80 | child = new Scope(); 81 | child.$root = parent.$root; 82 | child.$$asyncQueue = parent.$$asyncQueue; 83 | child.$$postDigestQueue = parent.$$postDigestQueue; 84 | child.$$applyAsyncQueue = this.$$applyAsyncQueue; 85 | } else { 86 | var ChildScope = function() { }; 87 | ChildScope.prototype = this; 88 | child = new ChildScope(); 89 | } 90 | parent.$$children.push(child); 91 | child.$$watchers = []; 92 | child.$$listeners = {}; 93 | child.$$children = []; 94 | child.$parent = parent; 95 | return child; 96 | }; 97 | ``` 98 | -------------------------------------------------------------------------------- /chapter5/Returning-The-Event-Object.md: -------------------------------------------------------------------------------- 1 | ### 返回事件对象 2 | 3 | $emit 和 $broadcast 都有的一个额外的特性就是它们都返回自己构建的事件对象, 因此事件的发起者在事件完成传播后可以检测事件的状态. 4 | 5 | ```js 6 | it('returns the event object on '+method, function() { 7 | var returnedEvent = scope[method]('someEvent'); 8 | expect(returnedEvent).toBeDe ned(); 9 | expect(returnedEvent.name).toEqual('someEvent'); 10 | }); 11 | ``` 12 | 13 | 该实现非常简单, 仅仅返回事件对象: 14 | 15 | ```js 16 | Scope.prototype.$emit = function(eventName) { 17 | var additionalArgs = _.tail(arguments); 18 | return this.$$fireEventOnScope(eventName, additionalArgs); 19 | }; 20 | 21 | Scope.prototype.$broadcast = function(eventName) { 22 | var additionalArgs = _.tail(arguments); 23 | return this.$$fireEventOnScope(eventName, additionalArgs); 24 | }; 25 | 26 | Scope.prototype.$$fireEventOnScope = function(eventName, additionalArgs) { 27 | var event = {name: eventName}; 28 | var listenerArgs = [event].concat(additionalArgs); 29 | var listeners = this.$$listeners[eventName] || []; 30 | _.forEach(listeners, function(listener) { 31 | listener.apply(null, listenerArgs); 32 | }); 33 | return event; 34 | }; 35 | ``` 36 | -------------------------------------------------------------------------------- /chapter5/Setup.md: -------------------------------------------------------------------------------- 1 | ### 准备 2 | 3 | 我们仍然将继续在 Scope 对象上工作, 所以所有的代码都将写到 src/scope.js 和 test/scope_spec.js 中. 让我们开始吧! 4 | 5 | ```js 6 | describe('Events', function() { 7 | var parent; 8 | var scope; 9 | var child; 10 | var isolatedChild; 11 | beforeEach(function() { 12 | parent = new Scope(); 13 | scope = parent.$new(); 14 | child = scope.$new(); 15 | isolatedChild = scope.$new(true); 16 | }); 17 | }); 18 | ``` 19 | 20 | 关于这次实现, 我们有好多与 Scope 继承层级相关的事情要做, 因此为了方便, 我们在 beforeEach 函数中做了些配置: 一个父 Scope, 两个子 Scope, 其中一个是隔离 Scope 的. 这应当可以覆盖关于继承所需要的测试了. 21 | 22 | -------------------------------------------------------------------------------- /chapter5/Stopping-Event-Propagation.md: -------------------------------------------------------------------------------- 1 | ### 阻止事件传递 2 | 3 | DOM 事件的另一个非常常用的特性, 就是阻止事件传播特性. DOM 事件对象有一个叫做 stopPropagation 的函数就是这个目的, 它经常被用于在多个层级 DOM 上 handler, 又不想触发特定事件的所有 handler 的情景. 4 | 5 | Scope 事件也有一个 stopPropagation 方法, 但是只能当 emit 事件时使用. broadcast 时事件不能被阻止. 6 | 7 | 这里的意思是当你 emit 一个事件, 所有注册 listener 函数中的一个, 阻止事件的传播, 父 Scope 上 listener 将不会看到这些事件. 8 | 9 | ```js 10 | it('does not propagate to parents when stopped', function() { 11 | var scopeListener = function(event) { 12 | event.stopPropagation(); 13 | }; 14 | var parentListener = jasmine.createSpy(); 15 | scope.$on('someEvent', scopeListener); 16 | parent.$on('someEvent', parentListener); 17 | scope.$emit('someEvent'); 18 | expect(parentListener).not.toHaveBeenCalled(); 19 | }); 20 | ``` 21 | 22 | 因此事件不会向父 Scope 传播, 但是, 关键是事件仍然会传递给当前 Scope 中所有剩余的 listener, 只是阻止向父 Scope 穿破的事件. 23 | 24 | ```js 25 | it('is received by listeners on current scope after being stopped', function() { 26 | var listener1 = function(event) { 27 | event.stopPropagation(); 28 | }; 29 | var listener2 = jasmine.createSpy(); 30 | scope.$on('someEvent', listener1); 31 | scope.$on('someEvent', listener2); 32 | scope.$emit('someEvent'); 33 | expect(listener2).toHaveBeenCalled(); 34 | }); 35 | ``` 36 | 37 | 第一件事就是我们需要一个表示是否调用 stopPropagation 方法布尔标志. 我们可以在 $emit 中引入该标志. 然后就是需要 stopPropagation 函数自己并将它添加到事件对象里. 最后在 do ... while 循环中进入父 Scope 之前检查布尔标志的状态. 38 | 39 | ```js 40 | Scope.prototype.$emit = function(eventName) { 41 | var propagationStopped = false; 42 | var event = { 43 | name: eventName, 44 | targetScope: this, 45 | stopPropagation: function() { 46 | propagationStopped = true; 47 | } 48 | }; 49 | var listenerArgs = [event].concat(_.tail(arguments)); 50 | var scope = this; 51 | do { 52 | event.currentScope = scope; 53 | scope.$$fireEventOnScope(eventName, listenerArgs); 54 | scope = scope.$parent; 55 | } while (scope && !propagationStopped); 56 | return event; 57 | }; 58 | ``` 59 | -------------------------------------------------------------------------------- /chapter5/The-basics-of-$emit-and-$broadcast.md: -------------------------------------------------------------------------------- 1 | ### $emit 和 $broadcast 的基础 2 | 3 | 现在, 我们拥有注册的事件监听器, 我们可以触发事件去使用它们, 正如我们讨论过的: 有两个方法可以触发事件: $emit 和 $broadcast. 4 | 5 | 这两个函数的基本功能是当你用一个事件名称作为参数调用它们的时候, 它们会调用所有用该事件名注册的所有的事件监听器. 相应地, 当然它们不会调用其他事件名称注册的事件监听器. 6 | 7 | ```js 8 | it('calls the listeners of the matching event on $emit', function() { 9 | var listener1 = jasmine.createSpy(); 10 | var listener2 = jasmine.createSpy(); 11 | scope.$on('someEvent', listener1); 12 | scope.$on('someOtherEvent', listener2); 13 | scope.$emit('someEvent'); 14 | expect(listener1).toHaveBeenCalled(); 15 | expect(listener2).not.toHaveBeenCalled(); 16 | }); 17 | 18 | it('calls the listeners of the matching event on $broadcast', function() { 19 | var listener1 = jasmine.createSpy(); 20 | var listener2 = jasmine.createSpy(); 21 | scope.$on('someEvent', listener1); 22 | scope.$on('someOtherEvent', listener2); 23 | scope.$broadcast('someEvent'); 24 | expect(listener1).toHaveBeenCalled(); 25 | expect(listener2).not.toHaveBeenCalled(); 26 | }); 27 | ``` 28 | 29 | 为了使测试通过, 我们引入 $emit 和 $broadcast 方法: 目前, 它们的行为是一致的. 找到对应事件名称的事件监听器, 成功调用这些监听器: 30 | 31 | ```js 32 | Scope.prototype.$emit = function(eventName) { 33 | var listeners = this.$$listeners[eventName] || []; 34 | _.forEach(listeners, function(listener) { 35 | listener(); 36 | }); 37 | }; 38 | 39 | Scope.prototype.$broadcast = function(eventName) { 40 | var listeners = this.$$listeners[eventName] || []; 41 | _.forEach(listeners, function(listener) { 42 | listener(); 43 | }); 44 | }; 45 | ``` 46 | --------------------------------------------------------------------------------