Build Your Own Angularjs 读书笔记
项目配置[编辑]
- npm package.json
- Lo-Dash,jQuery:不依赖,如果有就代理使用
- _.template("Hello,<%= name%>!")({name: to}); //var _ = require('lodash');
- _.forEach
- _.bind //等价于ES5 Function.prototype.bind
- _.isArray _.isObject
- _.forOwn //遍历object的属性
- 用jasmine写测试(describe-it-expect)
作用域[
看到$,就会想到PHP、Perl这些脚本语言,程序员手里敲着代码眼里看的是美元符号:-D
digest cycle与脏检查:$watch,$digest,$apply
- watch表达式(内部编译为一个函数,这个看起来有点意思!)
- scope.$watch( watchFn: scope -> value,listener: (old,new,scope) -> void) //var watchFn = jasmine.createSpy();
- 将old value缓存到watcher对象上?这边有一个小问题:watcher.last要不要初始化为当前value呢?
- => 初始化为function initWatchVal() {},用于表示app级别“不存在的”对象取值,绝妙~ 但这里js变量的类型前后是不一样的(也许不需要这么严格要求?)
- 简单实现不能捕获listener对scope的每次修改!(除非像Vue那样重载js object的setter函数)--> 一个digest周期内listener改变了属性值,则补充再迭代,可能多次
- 防止无限迭代:TTL
- $digest迭代watcher,而不是scope的所有属性(不过假如多个watcher监视相同的属性变化?)跟react的virtual dom diff原则不同
- $digest:迭代执行一遍所有的$watch
- $$前缀代表angular内部的私有变量
- 基于value的脏检查(而不是仅仅基于引用):数组和对象的元素diff算法
- 为了提供value的old、new,需要deep copying...
- NaN!= NaN,特殊处理
- $eval
- $apply:集成外部代码到digest cycle
- $evalAsync(推迟,但仍然在当前digest cycle)
- vs $timeout(后者是setTimeout(,0)的封装)
- []用作defer队列:var asyncTask = this.$$asyncQueue.shift();
- scope phases(见鬼,从这个地方有点不太好懂了:不都是在一个digest cycle里面吗?)
- => 为了确保从外部调用$evalAsync后,会调度一次digest执行(setTimeout)
- $applyAsync(原始动机:处理HTTP response,但不触发完整的digest)
- var self = this; //当注册callback时,引用当前的this,以防止JS的动态作用域特性引用错误的this(实际上相当于一个独立的函数,而非对象方法了)
- async callbacks的batch执行:用一个新的lambda将queue里的所有callback对象包起来一起执行
- 惯用法:
- self.$ $applyAsyncId = setTimeout(function(){... self.$ $applyAsyncId = null;},0) //跟踪setTimeout是否已经被浏览器调度执行
- $$postDigest(略)
- 异常处理
- 删除watch
- var destroyWatch = scope.$watch(...)
- destroyWatch(); //调用数组的splice(index,n)方法删除元素
- $watchGroup
作用域继承
- $rootScope
- $new:创建新的子scope
- child直接看得到parent的属性,但同名的情况下发生JS Prototype链上的shadowing(看到这里,会想起SICP里实现一个解释器的内容)
- digest过程现在从rootScope触发,递归执行; $$children
- 注意:Angular.js本身用“prev/next-Sibling、$head/tail-Child”来组织一棵scope层次树
- scope隔离:仍然挂在scope树上,但是断开到parent的原型链(这样属性查找只在当前child中?)
- directive scope linking:有选择地引用parent的属性
- 隔离情况下仍然想让scope共用root的属性,则在$new创建时执行一个引用copy操作...
- 替换parent(略)
- 这里的隐含约束是:当scope层次树的结构动态修改后,相应的watch、apply机制仍然能够properly起作用,相当于代码灵活性的要求
- $destroy:从$parent断开引用
用于集合对象(array、object)的高效脏检查
- $watchCollection:每次!$areEqual的时候counter++ ?
- 注意,之前oldValue是deep copy出来的,所以这里的核心算法就是针对array或object的diff操作而已...(但是不如React的virtual dom那么激进)
- array-like(typeof .length === "number")
- arguments
- DOM NodeList
- ?String不是Object
- Trick:如何检测包含了length作为key的object(与array-like区分)
- Function对象的.length属性保存了形参的个数?//作用:需不需要记住oldValue传给listenerFn
(Scope Events)事件系统:$on,$emit,$broadcast
- Pub:emitting:当前的及其祖先scopes得到通知;往下则为广播(注意:跟DOM事件无关)
- $on:注册事件listener
- $emit,$broadcast
- var listeners = this.$$listeners[eventName] || [];
- var additionalArgs = _.rest(arguments);
- var listenerArgs = [event].concat(additionalArgs); listener.apply(null,listenerArgs);
- 仿照DOM事件的target与currentTarget属性,有targetScope与currentScope(分别代表触发和处理事件的scope)
- 实现event.stopPropagation()
- 实现preventDefault //?这有意义吗?
- this.$broadcast('$destroy'); //通知directives其所在scope已被移除
表达式与过滤器[
expr parser支持CSP(从编译模式切换到解释模式?)
常量表达式
- “a+b" 编译为 function(scope){return scope.a+scope.b}
- Lexer --> Parser { AST Builder --> Compiler }
- AST Node: {type: AST.Literal,value: 42} //直接算出值
- String类型
- escape:\r \n
- Unicode escape:\uXXXX
- return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
- peek / consume
- primary函数?parse value类型的子表达式?
- var key = property.key.type === AST.Identifier? property.key.name: this.escape(property.key.value); //JSON key可能是标识符或字符串
- 这里compiler不需要考虑生成临时变量(特别是嵌套作用域下的shadowing)的问题?watch表达式直接对着翻译?但是后面还有一个高级的|管道过滤操作符呢
- nextId()
Lookup与函数调用
- this(=当前的scope)
- var fn = parse('aKey.secondKey.thirdKey.fourthKey'); expect(fn({aKey: {}})).toBeUndefined();
- 这里有个错误,这里应该会抛TypeError异常,而不是返回undefined
- AST.MemberExpression:fourthKey属性名称在AST树的上一级?嗯,符合“先计算的在AST底层”的原则
- locals
- Computed Attribute Lookup(a["b"])——这与a.b有什么区别??
- 方法*(a.m()也不代表m一定是个方法吧?m也有可能是个函数)
- call context(bind to this)
- 赋值*
- 安全的成员访问
- aFunction.constructor("return window;")()
- __proto__ //非标准已过世?
- __defineGetter__,__lookupGetter__,__defineSetter__,and __lookupSetter__ //非标准
- ensureSafeMemberName:屏蔽对这些不安全成员属性的访问,虽然是解决了问题,但总感觉不够灵活?与其抛出异常,不如来个静悄悄的失败?
- 确保安全的对象(不要把window关联到Scope):obj.window === obj
- DOM对象呢?
- 检查window不在赋值语句的右边,或函数的实参中
- Function(obj.constructor === obj)、Object
- 对函数对象,不允许call/apply(this rebind to other obj)
- ?怎么把这些ensureSafeXxx函数传递进runtime?
操作符
- Multiplicative Operators
- 优先级怎么处理?
- Additive Operators
- 这里的parser代码实现是严格递归下降的,感觉无法处理“a+b+c-d”这种情况?
- 关系比较(最低优先级?还有逻辑&& ||)
- 三元运算符:test?t:f
过滤器
- a | f | g 相当于g(f(a))
- 附加的过滤器参数(生成一个partial函数?)
- filter(实际上是个高阶函数)
- arr | filter:isOdd
- arr | filter:"!o"
- arr | filter:{name: "o"} //允许深度嵌套,模式匹配??
- 'arr | filter:{user: {name: "Bob"}}'
- 'arr | filter:{$: "o"}' //任意属性位置
- 定制的比较函数:arr | filter:{$: "o"}:myComparator //?
表达式与watches
- literal vs constant
- ast.constant 属性文法?
- 一次表达式:
- { {user.firstName}} { {user.lastName}}//final变量,单赋值
- one-time binding:{{::user.firstName}} {{::user.lastName}}
- 输入跟踪(只有输入变量发生变化时才触发重新计算)
- inputsWatchDelegate:watch inputs中的每个变量?
- 遍历AST,决定表达式的inputs:即所有非常量的元素
- 注意不要分别watch逻辑表达式的左右两边,以防破坏|| &&的短路特性
- ?this.state.computing = 'fn';
- 有状态的过滤器
- ast.toWatch = stateless? argsToWatch: [ast];
- 外部赋值
- var exprFn = parse('a.b'); var scope = {a: {b: 42}}; exprFn.assign(scope,43); //用于实现2-路绑定,有点像是ES6里的解构赋值
- 但假如watch编译后的函数返回值无法追踪到scope中的变量引用呢?
- fn.assign = function(scope,value,locals) { ... }
- AST.NGValueParameter?
模块与依赖注入[
模块与注入器
- modules是局部变量,所以确保函数只定义一次?f=f||function(){...}
- 不允许注册'hasOwnProperty'名称的模块
- injector加载模块,通过“invoke queue”
- 备注:当整个webapp固化之后,防御性的代码是不是可以去掉了?因为angular库的使用者就是webapp的js代码
- module.requires
- DI: 期望injector自动找到模块依赖的参数并在构造时传递进来
- 显式指出:fn.$inject
- 绑定this:invoke:... return fn.apply(self,args);
- 给invoke提供额外的locals局部变量绑定
- 数组风格的依赖标注
- annotate:fn.slice(0,fn.length - 1) 或 fn.$inspect
- 依赖标注从形参(这是Angular 1.x最有意思的特性了,我当初看到简直就是大吃一惊啊!)
- 原理:(function(a,b) { }).toString() // => "function (a,b) { }" 靠!不过,为了取得形参的名字执行一个toString序列化有点太夸张了 -_-
- 去除注释:var STRIP_COMMENTS = /\/\*.*?\*\//g;
- 进一步改进:var STRIP_COMMENTS = /(\/\/.*$)|(\/\*.*?\*\/)/mg;
- 当JS代码被混淆压缩后,此机制会失效! => 严格模式
- 用DI实例化对象
- Type.$inject = ['a','b']; var instance = injector.instantiate(Type); //关键是取得a,b的实际取值!这里;
- var instance =Object.create(UnwrappedType.prototype); //ES 5.1
- invoke(Type,instance); //将当前scope绑定到this进行调用
Providers
- 任何提供了$get函数的对象,其返回值作为依赖:
- 给$get提供DI参数:cache[key] = invoke(provider.$get,provider); //直接调用=>invoke()
- 依赖的懒惰实例化(a的依赖b可以在a后面注册)
- cache拆成2个:var providerCache = {}; var instanceCache = {};
- providerCache[key + 'Provider'] = provider; //dirty trick
- “everything in Angular is a singleton”
- var instance = instanceCache[name] = invoke(provider.$get);
- 检测循环依赖
- 初始化DI之前,放置一个marker对象:if (instanceCache[name] === INSTANTIATING) { throw ... }
- 显示循环依赖的路径:var path = []; //使用一个name栈...
- Provider Constructors
- Angular doesn't care.
- 真正需要做的是多做一个类型检查:if (_.isFunction(provider)) { provider = instantiate(provider); }
- Two Injectors: The Provider Injector and The Instance Injector
- ?不能inject an instance to another provider’s constructor
- ?类似的,反之也不行:this.$get = function(aProvider) { return aProvider.$get(); }; X
- 不能通过injector.get获得provider的引用(recall:provider的存在只是为了调用$get获得其返回value)
- 常量:总是push(unshift)到invoke队列到前面
- invokeLater:invokeQueue[arrayMethod || 'push']([method,arguments]);
- constant: invokeLater('constant','unshift'),//常量:push改成unshift
高级DI特性
- Injecting The $injectors(以$injector提供)
- 最有用的是动态get方法:var aProvider = $injector.get('aProvider');
- 注入$provide *
- 配置块(在模块加载时执行任意‘配置函数’)
- $routeProvider.when('/someUrl',{
-
templateUrl: '/my/view.html',
-
controller: 'MyController'
-
});
- 运行块:injector构造时调用(module loader/injector --> module instance --> run blocks)
- module.provider('a',{$get: _.constant(42)});
- module.run(function(a) { ... })
- => moduleInstance._runBlocks.push(fn);
- 调用时机:需要等所有模块都加载完毕后执行(一次),trick:先收集到一个数组里,最后执行
- 函数模块
- hashKey:从JS对象返回一个string key
- expect(hashKey(null)).toEqual('object:null'); //格式:type:valueStr,不基于value语义,2个引用value相同,但是hash key不同?
- value.$$hashKey = _.uniqueId();
- HashMap:key可以是任意JS对象(普通js object key只能是string)
- Function Modules Redux
- var loadedModules = new HashMap(); //modules直接作为key?感觉更像是一个HashSet(用于判断指定module有没有被加载)
- 工厂
- 可以注入实例依赖:
- module.factory('a',function() { return 1; });
- module.factory('b',function(a) { return a + 2; });
- factory: function(key,factoryFn) { this.provider(key,{$get: factoryFn}); } //工厂就是一个provider
- enforceReturnValue(factoryFn):可以return null,但不能是undefined
- Values
- values are not available to providers or config blocks(只用于instances)
- 实现:value: function(key,value) { this.factory(key,_.constant(value)); }
- Services(构造器函数)
- module.service('aService',function MyService(theValue) {
-
this.getValue = function() { return theValue; }; });
- Decorators
- module.decorator('aValue',function($delegate) { $delegate.decoratedKey = 43; }); //这里aValue是一个工厂,$delegate指代它返回的对象
- 多个装饰器:顺序(从内往外依次封装)应用:
- var instance = instanceInjector.invoke(original$get,provider);
- //对instance进行修改...
- instanceInjector.invoke(decoratorFn,null,{$delegate: instance});
- 集成Scopes、表达式、过滤器、&注入控制器
- ?this.register('filter',require('./filter_filter'));
- ?当注册一个my过滤器时,它还以myFilter的名字作为一个正常工厂提供
- setupModuleLoader(window); //DI injector在这里配置好?
- var ngModule = angular.module('ng',[]);
- ngModule.provider('$filter',require('./filter')); //ng基础服务
- ngModule.provider('$parse',require('./parse'));
- ngModule.provider('$rootScope',require('./scope'));
- $rootScopeProvider: 配置$rootScope TTL
辅助函数[
Promises
- $q服务,Q库的裁剪版?Promises/A+兼容(jQuery 3.0+支持)
Promises/A+规范是不是先多个then注册successCallback,最后来一个catch注册errorCallback?而ES6 Promise则在一个then里面同时注册2个callback??
- 与digest loop深度集成,可以使用$evalAsync,而不是setTimeout,来触发异步回调
- ngModule.provider('$q',require('./q'));
- 创建Deferreds
- Deferred是数据的生产者,而Promise就是消费者
- $q:function defer() { return new Deferred(); }
- d:this.promise = new Promise();
- 解析:
- promise.then(promiseSpy);
- deferred.resolve('ok'); //当调用resolve时,Promise并不立即触发(只是push到一个异步回调队列里,对于ES6 Promise来说这套机制是原生的)
- 注意,Promise也允许先resolve再then注册异步回调
- 触发digest:$rootScope.$apply();
- 防止多次解析
- 实现:维护内部的状态机!
- 确保回调被触发
- 在then注册异步回调的时候检查一下状态机特殊处理即可
- 注册多个异步回调(then链,这里实际上针对的是同一个Promise对象!)
- 否决Deferreds及捕获‘否决’
- resolve / reject
- Promise.prototype.catch = function(onRejected) { return this.then(null,onRejected); }
- finally: both then & catch
- Promise链
- 每个then调用返回一个新的Promise(或句话说,创建了另外一个新的状态机)
- value在Promise链中是传递的:one.then(two).catch(three) 这里one、two的异常都会被three捕获
- catch的返回值将会当作下一个resolve(!)
- 异常处理
- 异常不是reject到当前Promise的catch,而是传递到下一个Promise
- Promise回调返回Promise的情况
- => 将其与下一个Promise连接!
- resolve中需要对value特殊检查:if (value && _.isFunction(value.then)) {
-
value.then( _.bind(this.resolve,this),_.bind(this.reject,this));
- finally的返回值需要被忽略(不在链中进一步传播)
- finally中返回一个Promise的情况:需要同步等待此Promise被resolve!**(这里有点不大好懂,需要找时间看第2遍)
- 进度通知
- defer.promise.then(null,progressNotifyCb);
- defer.notify('working...'); //哦,每个then都会返回新的Deferred对象及其内部Promise
- 但这里调用notify的线索条件来自哪里呢?
- 注意,defer.resolve之后,notify不起作用
- 立即否决:$q.reject,创建一个状态已经被否决的Promise
- 立即解析:$q.when,创建一个状态已经被解析的Promise
- Promise集合:$q.all,创建一个新的汇聚Promise,当输入的所有Promise都已经被完成后,解析自己
- var promise = $q.all([$q.when(1),$q.when(2),$q.when(3)]);
- 注意,$q.all等待的是所有Promise都成功resolve,若有一个被reject,则立即返回
- 备注:既然有$q.all,是不是也可以有$q.any/race?
- ES6风格的Promises
- 没有Deferreds这样的概念;同时,不是注册2个callback分别对应resolve/reject,而是一个callback:function(value,error) {...}
- 如何用Q来模仿ES6 Promise API:略
- 无$digest集成:$$q
- var d = $$q.defer(); //后面一样,这应该是用setTimeout异步调度的了
$http
- vs $httpBackend
- SinonJS:模仿一个虚假的XMLHttpRequest请求 //注意本书代码的风格是TDD的
- xhr = sinon.useFakeXMLHttpRequest(); ==> requests[0].respond(200,{},'Hello');
- 302(服务器端重定向)被浏览器内部直接处理,不会到达JS代码
- 默认请求参数配置
- 请求头部
- 响应头部
- 允许CORS:withCredentials=true
- 请求变换:transformRequest:[f1,f2,...] //data
- 设置进默认配置:$http.defaults.transformRequest = ...
- 响应变换
- transformResponse: function(data,headers) { ... } //根据Content-Type?有必要这么做吗?
- JSON序列化与解析
- 二进制数据:var bb = new BlobBuilder(); bb.append(...); var data = bb.getBlob('text/plain');
- 跳过FormData
- if ((contentType && contentType.indexOf('application/json') === 0) || isJsonLike(data)) { return JSON.parse(data); }
- isJsonLike: 字符串以{或[开始?
- URL参数
- url: "...",params: { a:1,... } //车机webapp代码目前是硬拼字符串
- name有多个value的情况:a=1&a=2&a=...
- 定制的参数序列化:paramSerializer: serializeParams
- $httpParamSerializerJQLike:a: {b: {c: 42}} 被序列化为 a[b][c]=42(方括号被进一步url编码)
- Shorthand方法
- Interceptors
- var interceptors = _.map(interceptorFactories,function(fn) { return fn(); }); //又是工厂模式 -_-
- refine: return $injector.invoke(fn);
- Interceptors make heavy use of Promises. (靠!)
- serverRequest(config):return sendReq(config,reqData).then(transformResponse,transformResponse);
- 现在需要$http()之后调用$rootScope.$apply()
- Interceptors are objects that have one or more of 4 keys: request,requestError,response,and responseError.
- request返回一个修改过的request,也可以返回一个Promise,这意味着它比transform机制强大的多
- request请求拦截器核心代码如下://主要是修改config?
- var promise = $q.when(config); _.forEach(interceptors,function(interceptor) {
-
promise = promise.then(interceptor.request,interceptor.requestError);
- });
- return promise.then(serverRequest);
- _.forEachRight(interceptors,function(interceptor) {
-
promise = promise.then(interceptor.response,interceptor.responseError);
- Promise扩展
- $http.get('http://teropa.info').success/error(...)
- 请求超时
- 挂起的请求
- $http.pendingRequests: 请求已发送,但响应还未接受到
- 集成$http与1.3 $applyAsync
- 优化:使得原先的多个$apply发生在一个digest中执行
- 启用:$httpProvider.useApplyAsync(true);
- 实现:if (useApplyAsync) { $rootScope.$applyAsync(resolvePromise); }
指令[
DOM编译与基本指令
- "DOM编译"?为什么不叫“DOM转换”
- directive: invokeLater('$compileProvider','directive'),//=> $provide.factory(name + 'Directive',directiveFactory);
- $compile
- 指令应用:<my-directive> ... </my-directive>
- 注意,Angularjs不依赖jQuery实现低级DOM操纵,它有一个自己的实现:jqLite //DOM esoterica is not the focus of this book
- 对DOM元素的属性应用指令
- 应用指令到class
- 应用指令到注释(囧)
- 显示指出匹配限制:ECMA
- 优先级
- 中止编译
- terminal:true ==> 如 < div ng-if="condition"> ... </div>,用于对child节点是否继续compile的控制
- 跨越多个节点应用指令
指令属性
- ng-attr- (类似于data-)
- 设置属性:attrs.$set(k,v)
- var attrs = new Attributes($(node)); ...
- 去规范化属性名称
- attrs.$set('someAttribute',43); ==> element.attr('some-attribute')
- 持续观察属性
- Attributes.prototype.$observe = function(key,fn) {
-
this.$$observers = this.$$observers || Object.create(null); //经常看到这种 a = a || b; 的写法(避免a被重复初始化)
- 备注:在Angular DI的编码风格下,JS对象的底层内存管理有什么差别吗?
- while ((match = /([\d\w\-_]+)(?:\:([^;]+))?;?/.exec(className))) { className = className.substr(match.index + match[0].length); }
- Adding Comment Directives As Attributes *
- attrs.$addClass/$removeClass/$updateClass
指令链接与作用域
- Linkingis the process of combining the compiled DOM tree with scope objects.
- var linkFn = $compile(el); //返回一个函数对象的写法其实算很高级了 -_-
- linkFn将一个scope对象关联到DOM节点子树:$compileNodes.data('$scope',scope); //调试信息,产品build并不需要?
- Directive Link Functions(DLF,每个指令有它自己的link函数?)
- 普通DLF
- compile什么也不做,把工作推迟到link内执行?(真的很绝妙,虽然是源代码级别的转换,但是体现了JIT编译器的思想...)
- linking子节点
- 最底层的DOM节点上的指令首选链接:childLinkFn = compileNodes(node.childNodes); //就是个递归
- Pre- And Post-Linking
- 保持linking节点的稳定性
- 即child nodes先用一个var stableNodeList = [];存起来...
- 跨越多个nodes的指令linking
- groupElementsLinkFnWrapper: 这种情况实际上使得函数的参数不是稳定的一种类型,而可能变成是t或者[t]两种类型
- linking与作用域继承
- 更common的情况是指令创建它们自己的scope...(a inherited scope?这似乎指的是直接引用$parent的scope?)
- => scope = scope.$new(); //创建一个新的scope,但从$parent继承
- element将得到一个ng-scope class
- 隔离的scopes(不继承的)
- 隔离的属性绑定
- ‘@’
- 可以指定不同的名字(映射):scope: { aScopeAttr: '@anAttr' },...
- 双向数据绑定(React好像是单向传递?)
- ‘=’
- 子scope的属性改变自动更新到parent scope,在link后起作用
- case '=': var parentGet = $parse(attrs[attrName]); isolateScope[scopeName] = parentGet(scope);
-
scope.$watch(parentGet,function(newValue) { isolateScope[scopeName] = newValue; })
- var el = $('< div my-directive my-attr="parentAttr"></div>'); //对myAttr的赋值将更新到parent scope的parentAttr属性上去
- 一个digest中父子属性同时改变时,父修改优先
- =* 处理my-attr="parentFn()"收到一个集合对象的情况?$watchCollection
- =?如果DOM属性不存在,则不创建watcher
- 表达式绑定
- &
- 绑定行为,而不是数据(?)如ngClick
- expression在parent scope的context中被调用,而不是当前的isolate scope,原因很简单:表达式中的函数是由当前的指令使用者定义的
- case '&': var parentExpr = $parse(attrs[attrName]);
-
isolateScope[scopeName] = function() { return parentExpr(scope); };
- 允许传参
- 例:< div my-expr="parentFunction(a,b)"></div>
- 使用命名参数:scope.myExpr({a: 1,b: 2}); //Angular中的参数使用$前缀,如$event
- 实现:引入locals参数(<-- recall back!)
控制器
- $controller服务及其provider
- var $controller = injector.get('$controller'); //加载ng的内建控制器模块
- var controller = $controller(MyController,{aDep: 42}); //初始化特定控制器的实例(function MyController(aDep) {...})
- 注册
- $controllerProvider上的register方法并不那么常见,通常使用module上的:
-
module.controller('MyController',function MyController() { });
- 全局的控制器查找(不推荐,略)
- 指令控制器
- 关联到scope *
- 在指令上指定一个controllerAs属性(app开发者用不到,compiler内部使用)
- Controllers on Isolate Scope Directives
- 当指令has isolate scope时,$scope指的是当前isolate scope,而不是surrounding scope(parent)
- bindToController
- 嗯... 同名但使用时类型会改变的变量,可以理解为2个类型变量的一个union组合变量
- Requiring Controllers
- 当directive.require指定对另一个指令的依赖时,将对应到link函数的第4个参数,并可从中得到依赖指令的controller(其实,对JS来说,不存在私有变量)
- Requiring Multiple Controllers:略
- Self-Requiring Directives:当一个指令定义了自己的controller,而不是依赖于其他指令的
- Requiring Controllers in Multi-Element Directives
- Requiring Controllers from Parent Elements
- require: '^myDirective'
- ^^ 直接从parent寻找,而不是先从siblings开始
- 可选的Requiring:找不到controller不报错
- require: '?noSuchDirective'
- ngController指令
- < div ng-controller="TodoController as todoCtrl"> //注意,这里使用正则表达式来提取嵌入的变量名
- 从scope中查找控制器的constructor:locals => global(window)
指令模板
- replace: ture //此做法已废弃
- template: '< div my-other-directive></div>' //使得可以组件化的风格编写webapp
- 模板函数 //实际上,之前的模板字符串就被编译为一个函数
- 模板内的指令将接受到一个isolate scope **
- 异步模板:templateUrl
- 如何暂停-重启编译??
- 需要记住当前还没有应用的指令(保存状态):applyDirectivesToNode
- templateUrl也可以是一个函数:Template URL Functions(函数的返回值就是一个url?)
- 一个元素不允许超过1个templateUrl(技术实现的限制??)
- var templateDirective = prevIoUsCompileContext.templateDirective; //这是什么意思呢??-_-
- 链接异步指令(这将构造异步回调链)
- trick 1: 返回一个特殊的“delayed node link function”(?这个时候模板内容可能还没有成功下载下来吧)
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives,$compileNode,attrs,prevIoUsCompileContext);
- afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes);
- trick 2:让link在template接受到之前发生
- 引入var linkQueue = []; //所以,对于一般的异步回调链而言,需要检查2种情况?
- Linking Directives that Were Compiled Earlier
- 保留isolate scope指令 *
- 保留控制器指令 *
Directive Transclusion(k,感觉这部分内容是本书最复杂的...)
- 依我的理解,Transclusion指的是指令模板在被嵌套包含的情况下,其binding上下文可以自动对接的意思???
- ‘Transclusion’也可以被cloned:transclude: ’element’
- 基本Transclusion(即没有scope binding的情况)
- transclude: true
- var $transcludedNodes = $compileNode.clone().contents(); //注意这里的clone调用
- link: function(scope,ctrl,transclude) { element.find('[in-template]').append(transclude()); } //
- ... At its core,the transclusion function is really a link function. //Yes,of course
- Transclusion And Scopes
- 绑定的scope是定义时的(文法作用域),而不是使用时的(动态作用域)
- boundTranscludeFn = function() { return linkFn.nodeLinkFn.transclude(scope); } //到处是这种引用定义时上下文的闭包函数...
- 修改scope.$new(),指定parentE
-
We should create a special transclusion scope that prototypally inherits from the surrounding scope,but whose $parent is set to the Scope of the transclusion directive.
-
!the
scope-bound transclusion function(靠~ 这个地方真的有点复杂,它涉及到闭包下的对象生命周期控制问题...)
- :boundTranscludeFn = function(containingScope) { var transcludedScope = scope.$new(false,containingScope); ...
- Transclusion from Descendant Nodes
- ???
- Transclusion in Controllers
- The Clone Attach Function
- link时先clone,并可修改;与Transclusion不同(?)
- Transclusion with Template URLs
- Transclusion with Multi-Element Directives
- ngTransclude指令
- 提供一个内容替换掉stub位置(实现非常简单:...)
- Full Element Transclusion
- Requiring Controllers from Transcluded Directives
- $linkNodes.data('$' + name + 'Controller',controller.instance); //部分构造
插值: { { expr }}
- $interpolate服务(app开发人员很少使用,直接集成在$compile里面)
- 字符串插值
- var interpolateFn = $interpolate('{ {a}} and { {b}}'); interpolateFn({a: 1,b: 2}); // => '1 and 2'
- parts.push(text / expFn ); //expFn = $parse(exp);
- Value Stringification
- 转义:'\\{\\{expr\\}\\}' //注意这里\还有JS字符串转义
- 文本节点插值
- 属性插值
- addAttrInterpolateDirective:基本情况同Text Node的
- 与元素上其他指令交互:
- 其他指令可能通过Attributes.$observe在观察元素上的属性变化,当属性由于插值变化时,需要触发这些observers:
-
不适用jQuery的attr(),改为attrs.$set(name,newValue);
- 指令希望收到的是插值完成后的属性值:
- 当前插值发生在first digest after linking,但实际上应该是before any digests
-
优先级设为100(高)+ 挂载到pre-link时机
- isolate scope指令的问题
-
case '@': ... destination[scopeName] = $interpolate(attrs[attrName])(scope); ...
- 指令应用时可能删除元素属性(或修改其值),此种情况下需要移除插值调用
- 需要防止用户在事件处理器里进行插值,如onclick:<button onclick="{ {myFunction()}}">
- Optimizing Interpolation Watches With A Watch Delegate
- Making Interpolation Symbols Configurable(默认是2个大括号对)
- $interpolateProvider.startSymbol('FOO').endSymbol('OOF');
- 需要动态构造RegExp:new RegExp(startSymbol.replace(/./g,escapeChar),'g');
自启
- 实现ngClick
- Angularjs框架有点像是一个编译器+解释器框架,而编写定制指令则有点像一个编译器后端优化Pass...
- var button = $('<button ng-click="doSomething()"></button>');
- link: function(scope,element) { element.on('click',function(evt) { scope.$apply(attrs.ngClick); }); }
- 注入$event:scope.$eval(attrs.ngClick,{$event: evt});
- 注册:ngModule.directive('ngClick',require('./directives/ng_click'));
- Bootstrapping Angular Applications Manually
- 手工自启
- injector.invoke(['$compile','$rootScope',function($compile,$rootScope) { $compile($element)($rootScope); }]); //DOM不仅仅被compile,而且被link到root scope
- 运行初始digest:=> $rootScope.$apply(function() { $compile($element)($rootScope); });
- 严格的DI?window.angular.bootstrap(element,['myModule'],{strictDi: true});
- 自动(via ng-app属性)
- $(document).ready(function() { ... });
- window.angular.bootstrap(foundAppElement,foundModule? [foundModule]: []);
- Building The Production Bundle
- ./node_modules/browserify/bin/cmd.js src/bootstrap.js > myangular.js //递归扫描所有的require,打包到一个js文件里面去
- npm install --save-dev uglifyjs
- package.json: "build:minified": "browserify src/bootstrap.js | uglifyjs -mc > myangular.min.js"
- Running An Example App
- app.js: angular.module('myExampleApp',[]); //?
原文链接:https://www.f2er.com/angularjs/147183.html
- watch表达式(内部编译为一个函数,这个看起来有点意思!)
- scope.$watch( watchFn: scope -> value,listener: (old,new,scope) -> void) //var watchFn = jasmine.createSpy();
- 将old value缓存到watcher对象上?这边有一个小问题:watcher.last要不要初始化为当前value呢?
- => 初始化为function initWatchVal() {},用于表示app级别“不存在的”对象取值,绝妙~ 但这里js变量的类型前后是不一样的(也许不需要这么严格要求?)
- 简单实现不能捕获listener对scope的每次修改!(除非像Vue那样重载js object的setter函数)--> 一个digest周期内listener改变了属性值,则补充再迭代,可能多次
- 防止无限迭代:TTL
- $digest迭代watcher,而不是scope的所有属性(不过假如多个watcher监视相同的属性变化?)跟react的virtual dom diff原则不同
- 将old value缓存到watcher对象上?这边有一个小问题:watcher.last要不要初始化为当前value呢?
- scope.$watch( watchFn: scope -> value,listener: (old,new,scope) -> void) //var watchFn = jasmine.createSpy();
- $digest:迭代执行一遍所有的$watch
- $$前缀代表angular内部的私有变量
- 基于value的脏检查(而不是仅仅基于引用):数组和对象的元素diff算法
- 为了提供value的old、new,需要deep copying...
- NaN!= NaN,特殊处理
- $eval
- $apply:集成外部代码到digest cycle
- $evalAsync(推迟,但仍然在当前digest cycle)
- vs $timeout(后者是setTimeout(,0)的封装)
- []用作defer队列:var asyncTask = this.$$asyncQueue.shift();
- scope phases(见鬼,从这个地方有点不太好懂了:不都是在一个digest cycle里面吗?)
- => 为了确保从外部调用$evalAsync后,会调度一次digest执行(setTimeout)
- $applyAsync(原始动机:处理HTTP response,但不触发完整的digest)
- var self = this; //当注册callback时,引用当前的this,以防止JS的动态作用域特性引用错误的this(实际上相当于一个独立的函数,而非对象方法了)
- async callbacks的batch执行:用一个新的lambda将queue里的所有callback对象包起来一起执行
- 惯用法:
- self.$ $applyAsyncId = setTimeout(function(){... self.$ $applyAsyncId = null;},0) //跟踪setTimeout是否已经被浏览器调度执行
- $$postDigest(略)
- 异常处理
- 删除watch
- var destroyWatch = scope.$watch(...)
- destroyWatch(); //调用数组的splice(index,n)方法删除元素
- $watchGroup
- $rootScope
- $new:创建新的子scope
- child直接看得到parent的属性,但同名的情况下发生JS Prototype链上的shadowing(看到这里,会想起SICP里实现一个解释器的内容)
- digest过程现在从rootScope触发,递归执行; $$children
- 注意:Angular.js本身用“prev/next-Sibling、$head/tail-Child”来组织一棵scope层次树
- scope隔离:仍然挂在scope树上,但是断开到parent的原型链(这样属性查找只在当前child中?)
- directive scope linking:有选择地引用parent的属性
- 隔离情况下仍然想让scope共用root的属性,则在$new创建时执行一个引用copy操作...
- 替换parent(略)
- 这里的隐含约束是:当scope层次树的结构动态修改后,相应的watch、apply机制仍然能够properly起作用,相当于代码灵活性的要求
- $destroy:从$parent断开引用
- $watchCollection:每次!$areEqual的时候counter++ ?
- 注意,之前oldValue是deep copy出来的,所以这里的核心算法就是针对array或object的diff操作而已...(但是不如React的virtual dom那么激进)
- array-like(typeof .length === "number")
- arguments
- DOM NodeList
- ?String不是Object
- Trick:如何检测包含了length作为key的object(与array-like区分)
- Function对象的.length属性保存了形参的个数?//作用:需不需要记住oldValue传给listenerFn
- Pub:emitting:当前的及其祖先scopes得到通知;往下则为广播(注意:跟DOM事件无关)
- $on:注册事件listener
- $emit,$broadcast
- var listeners = this.$$listeners[eventName] || [];
- var additionalArgs = _.rest(arguments);
- var listenerArgs = [event].concat(additionalArgs); listener.apply(null,listenerArgs);
- 仿照DOM事件的target与currentTarget属性,有targetScope与currentScope(分别代表触发和处理事件的scope)
- 实现event.stopPropagation()
- 实现preventDefault //?这有意义吗?
- this.$broadcast('$destroy'); //通知directives其所在scope已被移除
- “a+b" 编译为 function(scope){return scope.a+scope.b}
- Lexer --> Parser { AST Builder --> Compiler }
- AST Node: {type: AST.Literal,value: 42} //直接算出值
- String类型
- escape:\r \n
- Unicode escape:\uXXXX
- return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
- peek / consume
- primary函数?parse value类型的子表达式?
- var key = property.key.type === AST.Identifier? property.key.name: this.escape(property.key.value); //JSON key可能是标识符或字符串
- 这里compiler不需要考虑生成临时变量(特别是嵌套作用域下的shadowing)的问题?watch表达式直接对着翻译?但是后面还有一个高级的|管道过滤操作符呢
- nextId()
- this(=当前的scope)
- var fn = parse('aKey.secondKey.thirdKey.fourthKey'); expect(fn({aKey: {}})).toBeUndefined();
- 这里有个错误,这里应该会抛TypeError异常,而不是返回undefined
- AST.MemberExpression:fourthKey属性名称在AST树的上一级?嗯,符合“先计算的在AST底层”的原则
- locals
- Computed Attribute Lookup(a["b"])——这与a.b有什么区别??
- 方法*(a.m()也不代表m一定是个方法吧?m也有可能是个函数)
- call context(bind to this)
- 赋值*
- 安全的成员访问
- aFunction.constructor("return window;")()
- __proto__ //非标准已过世?
- __defineGetter__,__lookupGetter__,__defineSetter__,and __lookupSetter__ //非标准
- ensureSafeMemberName:屏蔽对这些不安全成员属性的访问,虽然是解决了问题,但总感觉不够灵活?与其抛出异常,不如来个静悄悄的失败?
- 确保安全的对象(不要把window关联到Scope):obj.window === obj
- DOM对象呢?
- 检查window不在赋值语句的右边,或函数的实参中
- Function(obj.constructor === obj)、Object
- 对函数对象,不允许call/apply(this rebind to other obj)
- ?怎么把这些ensureSafeXxx函数传递进runtime?
- Multiplicative Operators
- 优先级怎么处理?
- Additive Operators
- 这里的parser代码实现是严格递归下降的,感觉无法处理“a+b+c-d”这种情况?
- 关系比较(最低优先级?还有逻辑&& ||)
- 三元运算符:test?t:f
- a | f | g 相当于g(f(a))
- 附加的过滤器参数(生成一个partial函数?)
- filter(实际上是个高阶函数)
- arr | filter:isOdd
- arr | filter:"!o"
- arr | filter:{name: "o"} //允许深度嵌套,模式匹配??
- 'arr | filter:{user: {name: "Bob"}}'
- 'arr | filter:{$: "o"}' //任意属性位置
- 定制的比较函数:arr | filter:{$: "o"}:myComparator //?
- literal vs constant
- ast.constant 属性文法?
- 一次表达式:
- { {user.firstName}} { {user.lastName}}//final变量,单赋值
- one-time binding:{{::user.firstName}} {{::user.lastName}}
- 输入跟踪(只有输入变量发生变化时才触发重新计算)
- inputsWatchDelegate:watch inputs中的每个变量?
- 遍历AST,决定表达式的inputs:即所有非常量的元素
- 注意不要分别watch逻辑表达式的左右两边,以防破坏|| &&的短路特性
- ?this.state.computing = 'fn';
- 有状态的过滤器
- ast.toWatch = stateless? argsToWatch: [ast];
- 外部赋值
- var exprFn = parse('a.b'); var scope = {a: {b: 42}}; exprFn.assign(scope,43); //用于实现2-路绑定,有点像是ES6里的解构赋值
- 但假如watch编译后的函数返回值无法追踪到scope中的变量引用呢?
- fn.assign = function(scope,value,locals) { ... }
- AST.NGValueParameter?
- var exprFn = parse('a.b'); var scope = {a: {b: 42}}; exprFn.assign(scope,43); //用于实现2-路绑定,有点像是ES6里的解构赋值
模块与依赖注入[
模块与注入器
- modules是局部变量,所以确保函数只定义一次?f=f||function(){...}
- 不允许注册'hasOwnProperty'名称的模块
- injector加载模块,通过“invoke queue”
- 备注:当整个webapp固化之后,防御性的代码是不是可以去掉了?因为angular库的使用者就是webapp的js代码
- module.requires
- DI: 期望injector自动找到模块依赖的参数并在构造时传递进来
- 显式指出:fn.$inject
- 绑定this:invoke:... return fn.apply(self,args);
- 给invoke提供额外的locals局部变量绑定
- 数组风格的依赖标注
- annotate:fn.slice(0,fn.length - 1) 或 fn.$inspect
- 依赖标注从形参(这是Angular 1.x最有意思的特性了,我当初看到简直就是大吃一惊啊!)
- 原理:(function(a,b) { }).toString() // => "function (a,b) { }" 靠!不过,为了取得形参的名字执行一个toString序列化有点太夸张了 -_-
- 去除注释:var STRIP_COMMENTS = /\/\*.*?\*\//g;
- 进一步改进:var STRIP_COMMENTS = /(\/\/.*$)|(\/\*.*?\*\/)/mg;
- 当JS代码被混淆压缩后,此机制会失效! => 严格模式
- 用DI实例化对象
- Type.$inject = ['a','b']; var instance = injector.instantiate(Type); //关键是取得a,b的实际取值!这里;
- var instance =Object.create(UnwrappedType.prototype); //ES 5.1
- invoke(Type,instance); //将当前scope绑定到this进行调用
Providers
- 任何提供了$get函数的对象,其返回值作为依赖:
- 给$get提供DI参数:cache[key] = invoke(provider.$get,provider); //直接调用=>invoke()
- 依赖的懒惰实例化(a的依赖b可以在a后面注册)
- cache拆成2个:var providerCache = {}; var instanceCache = {};
- providerCache[key + 'Provider'] = provider; //dirty trick
- “everything in Angular is a singleton”
- var instance = instanceCache[name] = invoke(provider.$get);
- 检测循环依赖
- 初始化DI之前,放置一个marker对象:if (instanceCache[name] === INSTANTIATING) { throw ... }
- 显示循环依赖的路径:var path = []; //使用一个name栈...
- Provider Constructors
- Angular doesn't care.
- 真正需要做的是多做一个类型检查:if (_.isFunction(provider)) { provider = instantiate(provider); }
- Two Injectors: The Provider Injector and The Instance Injector
- ?不能inject an instance to another provider’s constructor
- ?类似的,反之也不行:this.$get = function(aProvider) { return aProvider.$get(); }; X
- 不能通过injector.get获得provider的引用(recall:provider的存在只是为了调用$get获得其返回value)
- 常量:总是push(unshift)到invoke队列到前面
- invokeLater:invokeQueue[arrayMethod || 'push']([method,arguments]);
- constant: invokeLater('constant','unshift'),//常量:push改成unshift
高级DI特性
- Injecting The $injectors(以$injector提供)
- 最有用的是动态get方法:var aProvider = $injector.get('aProvider');
- 注入$provide *
- 配置块(在模块加载时执行任意‘配置函数’)
- $routeProvider.when('/someUrl',{
-
templateUrl: '/my/view.html',
-
controller: 'MyController'
-
});
- 运行块:injector构造时调用(module loader/injector --> module instance --> run blocks)
- module.provider('a',{$get: _.constant(42)});
- module.run(function(a) { ... })
- => moduleInstance._runBlocks.push(fn);
- 调用时机:需要等所有模块都加载完毕后执行(一次),trick:先收集到一个数组里,最后执行
- 函数模块
- hashKey:从JS对象返回一个string key
- expect(hashKey(null)).toEqual('object:null'); //格式:type:valueStr,不基于value语义,2个引用value相同,但是hash key不同?
- value.$$hashKey = _.uniqueId();
- HashMap:key可以是任意JS对象(普通js object key只能是string)
- Function Modules Redux
- var loadedModules = new HashMap(); //modules直接作为key?感觉更像是一个HashSet(用于判断指定module有没有被加载)
- 工厂
- 可以注入实例依赖:
- module.factory('a',function() { return 1; });
- module.factory('b',function(a) { return a + 2; });
- factory: function(key,factoryFn) { this.provider(key,{$get: factoryFn}); } //工厂就是一个provider
- enforceReturnValue(factoryFn):可以return null,但不能是undefined
- Values
- values are not available to providers or config blocks(只用于instances)
- 实现:value: function(key,value) { this.factory(key,_.constant(value)); }
- Services(构造器函数)
- module.service('aService',function MyService(theValue) {
-
this.getValue = function() { return theValue; }; });
- Decorators
- module.decorator('aValue',function($delegate) { $delegate.decoratedKey = 43; }); //这里aValue是一个工厂,$delegate指代它返回的对象
- 多个装饰器:顺序(从内往外依次封装)应用:
- var instance = instanceInjector.invoke(original$get,provider);
- //对instance进行修改...
- instanceInjector.invoke(decoratorFn,null,{$delegate: instance});
- 集成Scopes、表达式、过滤器、&注入控制器
- ?this.register('filter',require('./filter_filter'));
- ?当注册一个my过滤器时,它还以myFilter的名字作为一个正常工厂提供
- setupModuleLoader(window); //DI injector在这里配置好?
- var ngModule = angular.module('ng',[]);
- ngModule.provider('$filter',require('./filter')); //ng基础服务
- ngModule.provider('$parse',require('./parse'));
- ngModule.provider('$rootScope',require('./scope'));
- $rootScopeProvider: 配置$rootScope TTL
辅助函数[
Promises
- $q服务,Q库的裁剪版?Promises/A+兼容(jQuery 3.0+支持)
Promises/A+规范是不是先多个then注册successCallback,最后来一个catch注册errorCallback?而ES6 Promise则在一个then里面同时注册2个callback??
- 与digest loop深度集成,可以使用$evalAsync,而不是setTimeout,来触发异步回调
- ngModule.provider('$q',require('./q'));
- 创建Deferreds
- Deferred是数据的生产者,而Promise就是消费者
- $q:function defer() { return new Deferred(); }
- d:this.promise = new Promise();
- 解析:
- promise.then(promiseSpy);
- deferred.resolve('ok'); //当调用resolve时,Promise并不立即触发(只是push到一个异步回调队列里,对于ES6 Promise来说这套机制是原生的)
- 注意,Promise也允许先resolve再then注册异步回调
- 触发digest:$rootScope.$apply();
- 防止多次解析
- 实现:维护内部的状态机!
- 确保回调被触发
- 在then注册异步回调的时候检查一下状态机特殊处理即可
- 注册多个异步回调(then链,这里实际上针对的是同一个Promise对象!)
- 否决Deferreds及捕获‘否决’
- resolve / reject
- Promise.prototype.catch = function(onRejected) { return this.then(null,onRejected); }
- finally: both then & catch
- Promise链
- 每个then调用返回一个新的Promise(或句话说,创建了另外一个新的状态机)
- value在Promise链中是传递的:one.then(two).catch(three) 这里one、two的异常都会被three捕获
- catch的返回值将会当作下一个resolve(!)
- 异常处理
- 异常不是reject到当前Promise的catch,而是传递到下一个Promise
- Promise回调返回Promise的情况
- => 将其与下一个Promise连接!
- resolve中需要对value特殊检查:if (value && _.isFunction(value.then)) {
-
value.then( _.bind(this.resolve,this),_.bind(this.reject,this));
- finally的返回值需要被忽略(不在链中进一步传播)
- finally中返回一个Promise的情况:需要同步等待此Promise被resolve!**(这里有点不大好懂,需要找时间看第2遍)
- 进度通知
- defer.promise.then(null,progressNotifyCb);
- defer.notify('working...'); //哦,每个then都会返回新的Deferred对象及其内部Promise
- 但这里调用notify的线索条件来自哪里呢?
- 注意,defer.resolve之后,notify不起作用
- 立即否决:$q.reject,创建一个状态已经被否决的Promise
- 立即解析:$q.when,创建一个状态已经被解析的Promise
- Promise集合:$q.all,创建一个新的汇聚Promise,当输入的所有Promise都已经被完成后,解析自己
- var promise = $q.all([$q.when(1),$q.when(2),$q.when(3)]);
- 注意,$q.all等待的是所有Promise都成功resolve,若有一个被reject,则立即返回
- 备注:既然有$q.all,是不是也可以有$q.any/race?
- ES6风格的Promises
- 没有Deferreds这样的概念;同时,不是注册2个callback分别对应resolve/reject,而是一个callback:function(value,error) {...}
- 如何用Q来模仿ES6 Promise API:略
- 无$digest集成:$$q
- var d = $$q.defer(); //后面一样,这应该是用setTimeout异步调度的了
$http
- vs $httpBackend
- SinonJS:模仿一个虚假的XMLHttpRequest请求 //注意本书代码的风格是TDD的
- xhr = sinon.useFakeXMLHttpRequest(); ==> requests[0].respond(200,{},'Hello');
- 302(服务器端重定向)被浏览器内部直接处理,不会到达JS代码
- 默认请求参数配置
- 请求头部
- 响应头部
- 允许CORS:withCredentials=true
- 请求变换:transformRequest:[f1,f2,...] //data
- 设置进默认配置:$http.defaults.transformRequest = ...
- 响应变换
- transformResponse: function(data,headers) { ... } //根据Content-Type?有必要这么做吗?
- JSON序列化与解析
- 二进制数据:var bb = new BlobBuilder(); bb.append(...); var data = bb.getBlob('text/plain');
- 跳过FormData
- if ((contentType && contentType.indexOf('application/json') === 0) || isJsonLike(data)) { return JSON.parse(data); }
- isJsonLike: 字符串以{或[开始?
- URL参数
- url: "...",params: { a:1,... } //车机webapp代码目前是硬拼字符串
- name有多个value的情况:a=1&a=2&a=...
- 定制的参数序列化:paramSerializer: serializeParams
- $httpParamSerializerJQLike:a: {b: {c: 42}} 被序列化为 a[b][c]=42(方括号被进一步url编码)
- Shorthand方法
- Interceptors
- var interceptors = _.map(interceptorFactories,function(fn) { return fn(); }); //又是工厂模式 -_-
- refine: return $injector.invoke(fn);
- Interceptors make heavy use of Promises. (靠!)
- serverRequest(config):return sendReq(config,reqData).then(transformResponse,transformResponse);
- 现在需要$http()之后调用$rootScope.$apply()
- Interceptors are objects that have one or more of 4 keys: request,requestError,response,and responseError.
- request返回一个修改过的request,也可以返回一个Promise,这意味着它比transform机制强大的多
- request请求拦截器核心代码如下://主要是修改config?
- var promise = $q.when(config); _.forEach(interceptors,function(interceptor) {
-
promise = promise.then(interceptor.request,interceptor.requestError);
- });
- return promise.then(serverRequest);
- _.forEachRight(interceptors,function(interceptor) {
-
promise = promise.then(interceptor.response,interceptor.responseError);
- Promise扩展
- $http.get('http://teropa.info').success/error(...)
- 请求超时
- 挂起的请求
- $http.pendingRequests: 请求已发送,但响应还未接受到
- 集成$http与1.3 $applyAsync
- 优化:使得原先的多个$apply发生在一个digest中执行
- 启用:$httpProvider.useApplyAsync(true);
- 实现:if (useApplyAsync) { $rootScope.$applyAsync(resolvePromise); }
指令[
DOM编译与基本指令
- "DOM编译"?为什么不叫“DOM转换”
- directive: invokeLater('$compileProvider','directive'),//=> $provide.factory(name + 'Directive',directiveFactory);
- $compile
- 指令应用:<my-directive> ... </my-directive>
- 注意,Angularjs不依赖jQuery实现低级DOM操纵,它有一个自己的实现:jqLite //DOM esoterica is not the focus of this book
- 对DOM元素的属性应用指令
- 应用指令到class
- 应用指令到注释(囧)
- 显示指出匹配限制:ECMA
- 优先级
- 中止编译
- terminal:true ==> 如 < div ng-if="condition"> ... </div>,用于对child节点是否继续compile的控制
- 跨越多个节点应用指令
指令属性
- ng-attr- (类似于data-)
- 设置属性:attrs.$set(k,v)
- var attrs = new Attributes($(node)); ...
- 去规范化属性名称
- attrs.$set('someAttribute',43); ==> element.attr('some-attribute')
- 持续观察属性
- Attributes.prototype.$observe = function(key,fn) {
-
this.$$observers = this.$$observers || Object.create(null); //经常看到这种 a = a || b; 的写法(避免a被重复初始化)
- 备注:在Angular DI的编码风格下,JS对象的底层内存管理有什么差别吗?
- while ((match = /([\d\w\-_]+)(?:\:([^;]+))?;?/.exec(className))) { className = className.substr(match.index + match[0].length); }
- Adding Comment Directives As Attributes *
- attrs.$addClass/$removeClass/$updateClass
指令链接与作用域
- Linkingis the process of combining the compiled DOM tree with scope objects.
- var linkFn = $compile(el); //返回一个函数对象的写法其实算很高级了 -_-
- linkFn将一个scope对象关联到DOM节点子树:$compileNodes.data('$scope',scope); //调试信息,产品build并不需要?
- Directive Link Functions(DLF,每个指令有它自己的link函数?)
- 普通DLF
- compile什么也不做,把工作推迟到link内执行?(真的很绝妙,虽然是源代码级别的转换,但是体现了JIT编译器的思想...)
- linking子节点
- 最底层的DOM节点上的指令首选链接:childLinkFn = compileNodes(node.childNodes); //就是个递归
- Pre- And Post-Linking
- 保持linking节点的稳定性
- 即child nodes先用一个var stableNodeList = [];存起来...
- 跨越多个nodes的指令linking
- groupElementsLinkFnWrapper: 这种情况实际上使得函数的参数不是稳定的一种类型,而可能变成是t或者[t]两种类型
- linking与作用域继承
- 更common的情况是指令创建它们自己的scope...(a inherited scope?这似乎指的是直接引用$parent的scope?)
- => scope = scope.$new(); //创建一个新的scope,但从$parent继承
- element将得到一个ng-scope class
- 隔离的scopes(不继承的)
- 隔离的属性绑定
- ‘@’
- 可以指定不同的名字(映射):scope: { aScopeAttr: '@anAttr' },...
- 双向数据绑定(React好像是单向传递?)
- ‘=’
- 子scope的属性改变自动更新到parent scope,在link后起作用
- case '=': var parentGet = $parse(attrs[attrName]); isolateScope[scopeName] = parentGet(scope);
-
scope.$watch(parentGet,function(newValue) { isolateScope[scopeName] = newValue; })
- var el = $('< div my-directive my-attr="parentAttr"></div>'); //对myAttr的赋值将更新到parent scope的parentAttr属性上去
- 一个digest中父子属性同时改变时,父修改优先
- =* 处理my-attr="parentFn()"收到一个集合对象的情况?$watchCollection
- =?如果DOM属性不存在,则不创建watcher
- 表达式绑定
- &
- 绑定行为,而不是数据(?)如ngClick
- expression在parent scope的context中被调用,而不是当前的isolate scope,原因很简单:表达式中的函数是由当前的指令使用者定义的
- case '&': var parentExpr = $parse(attrs[attrName]);
-
isolateScope[scopeName] = function() { return parentExpr(scope); };
- 允许传参
- 例:< div my-expr="parentFunction(a,b)"></div>
- 使用命名参数:scope.myExpr({a: 1,b: 2}); //Angular中的参数使用$前缀,如$event
- 实现:引入locals参数(<-- recall back!)
控制器
- $controller服务及其provider
- var $controller = injector.get('$controller'); //加载ng的内建控制器模块
- var controller = $controller(MyController,{aDep: 42}); //初始化特定控制器的实例(function MyController(aDep) {...})
- 注册
- $controllerProvider上的register方法并不那么常见,通常使用module上的:
-
module.controller('MyController',function MyController() { });
- 全局的控制器查找(不推荐,略)
- 指令控制器
- 关联到scope *
- 在指令上指定一个controllerAs属性(app开发者用不到,compiler内部使用)
- Controllers on Isolate Scope Directives
- 当指令has isolate scope时,$scope指的是当前isolate scope,而不是surrounding scope(parent)
- bindToController
- 嗯... 同名但使用时类型会改变的变量,可以理解为2个类型变量的一个union组合变量
- Requiring Controllers
- 当directive.require指定对另一个指令的依赖时,将对应到link函数的第4个参数,并可从中得到依赖指令的controller(其实,对JS来说,不存在私有变量)
- Requiring Multiple Controllers:略
- Self-Requiring Directives:当一个指令定义了自己的controller,而不是依赖于其他指令的
- Requiring Controllers in Multi-Element Directives
- Requiring Controllers from Parent Elements
- require: '^myDirective'
- ^^ 直接从parent寻找,而不是先从siblings开始
- 可选的Requiring:找不到controller不报错
- require: '?noSuchDirective'
- ngController指令
- < div ng-controller="TodoController as todoCtrl"> //注意,这里使用正则表达式来提取嵌入的变量名
- 从scope中查找控制器的constructor:locals => global(window)
指令模板
- replace: ture //此做法已废弃
- template: '< div my-other-directive></div>' //使得可以组件化的风格编写webapp
- 模板函数 //实际上,之前的模板字符串就被编译为一个函数
- 模板内的指令将接受到一个isolate scope **
- 异步模板:templateUrl
- 如何暂停-重启编译??
- 需要记住当前还没有应用的指令(保存状态):applyDirectivesToNode
- templateUrl也可以是一个函数:Template URL Functions(函数的返回值就是一个url?)
- 一个元素不允许超过1个templateUrl(技术实现的限制??)
- var templateDirective = prevIoUsCompileContext.templateDirective; //这是什么意思呢??-_-
- 链接异步指令(这将构造异步回调链)
- trick 1: 返回一个特殊的“delayed node link function”(?这个时候模板内容可能还没有成功下载下来吧)
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives,$compileNode,attrs,prevIoUsCompileContext);
- afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes);
- trick 2:让link在template接受到之前发生
- 引入var linkQueue = []; //所以,对于一般的异步回调链而言,需要检查2种情况?
- Linking Directives that Were Compiled Earlier
- 保留isolate scope指令 *
- 保留控制器指令 *
Directive Transclusion(k,感觉这部分内容是本书最复杂的...)
- 依我的理解,Transclusion指的是指令模板在被嵌套包含的情况下,其binding上下文可以自动对接的意思???
- ‘Transclusion’也可以被cloned:transclude: ’element’
- 基本Transclusion(即没有scope binding的情况)
- transclude: true
- var $transcludedNodes = $compileNode.clone().contents(); //注意这里的clone调用
- link: function(scope,ctrl,transclude) { element.find('[in-template]').append(transclude()); } //
- ... At its core,the transclusion function is really a link function. //Yes,of course
- Transclusion And Scopes
- 绑定的scope是定义时的(文法作用域),而不是使用时的(动态作用域)
- boundTranscludeFn = function() { return linkFn.nodeLinkFn.transclude(scope); } //到处是这种引用定义时上下文的闭包函数...
- 修改scope.$new(),指定parentE
-
We should create a special transclusion scope that prototypally inherits from the surrounding scope,but whose $parent is set to the Scope of the transclusion directive.
-
!the
scope-bound transclusion function(靠~ 这个地方真的有点复杂,它涉及到闭包下的对象生命周期控制问题...)
- :boundTranscludeFn = function(containingScope) { var transcludedScope = scope.$new(false,containingScope); ...
- Transclusion from Descendant Nodes
- ???
- Transclusion in Controllers
- The Clone Attach Function
- link时先clone,并可修改;与Transclusion不同(?)
- Transclusion with Template URLs
- Transclusion with Multi-Element Directives
- ngTransclude指令
- 提供一个内容替换掉stub位置(实现非常简单:...)
- Full Element Transclusion
- Requiring Controllers from Transcluded Directives
- $linkNodes.data('$' + name + 'Controller',controller.instance); //部分构造
插值: { { expr }}
- $interpolate服务(app开发人员很少使用,直接集成在$compile里面)
- 字符串插值
- var interpolateFn = $interpolate('{ {a}} and { {b}}'); interpolateFn({a: 1,b: 2}); // => '1 and 2'
- parts.push(text / expFn ); //expFn = $parse(exp);
- Value Stringification
- 转义:'\\{\\{expr\\}\\}' //注意这里\还有JS字符串转义
- 文本节点插值
- 属性插值
- addAttrInterpolateDirective:基本情况同Text Node的
- 与元素上其他指令交互:
- 其他指令可能通过Attributes.$observe在观察元素上的属性变化,当属性由于插值变化时,需要触发这些observers:
-
不适用jQuery的attr(),改为attrs.$set(name,newValue);
- 指令希望收到的是插值完成后的属性值:
- 当前插值发生在first digest after linking,但实际上应该是before any digests
-
优先级设为100(高)+ 挂载到pre-link时机
- isolate scope指令的问题
-
case '@': ... destination[scopeName] = $interpolate(attrs[attrName])(scope); ...
- 指令应用时可能删除元素属性(或修改其值),此种情况下需要移除插值调用
- 需要防止用户在事件处理器里进行插值,如onclick:<button onclick="{ {myFunction()}}">
- Optimizing Interpolation Watches With A Watch Delegate
- Making Interpolation Symbols Configurable(默认是2个大括号对)
- $interpolateProvider.startSymbol('FOO').endSymbol('OOF');
- 需要动态构造RegExp:new RegExp(startSymbol.replace(/./g,escapeChar),'g');
自启
- 实现ngClick
- Angularjs框架有点像是一个编译器+解释器框架,而编写定制指令则有点像一个编译器后端优化Pass...
- var button = $('<button ng-click="doSomething()"></button>');
- link: function(scope,element) { element.on('click',function(evt) { scope.$apply(attrs.ngClick); }); }
- 注入$event:scope.$eval(attrs.ngClick,{$event: evt});
- 注册:ngModule.directive('ngClick',require('./directives/ng_click'));
- Bootstrapping Angular Applications Manually
- 手工自启
- injector.invoke(['$compile','$rootScope',function($compile,$rootScope) { $compile($element)($rootScope); }]); //DOM不仅仅被compile,而且被link到root scope
- 运行初始digest:=> $rootScope.$apply(function() { $compile($element)($rootScope); });
- 严格的DI?window.angular.bootstrap(element,['myModule'],{strictDi: true});
- 自动(via ng-app属性)
- $(document).ready(function() { ... });
- window.angular.bootstrap(foundAppElement,foundModule? [foundModule]: []);
- Building The Production Bundle
- ./node_modules/browserify/bin/cmd.js src/bootstrap.js > myangular.js //递归扫描所有的require,打包到一个js文件里面去
- npm install --save-dev uglifyjs
- package.json: "build:minified": "browserify src/bootstrap.js | uglifyjs -mc > myangular.min.js"
- Running An Example App
- app.js: angular.module('myExampleApp',[]); //?
原文链接:https://www.f2er.com/angularjs/147183.html
- modules是局部变量,所以确保函数只定义一次?f=f||function(){...}
- 不允许注册'hasOwnProperty'名称的模块
- injector加载模块,通过“invoke queue”
- 备注:当整个webapp固化之后,防御性的代码是不是可以去掉了?因为angular库的使用者就是webapp的js代码
- module.requires
- DI: 期望injector自动找到模块依赖的参数并在构造时传递进来
- 显式指出:fn.$inject
- 绑定this:invoke:... return fn.apply(self,args);
- 给invoke提供额外的locals局部变量绑定
- 数组风格的依赖标注
- annotate:fn.slice(0,fn.length - 1) 或 fn.$inspect
- 依赖标注从形参(这是Angular 1.x最有意思的特性了,我当初看到简直就是大吃一惊啊!)
- 原理:(function(a,b) { }).toString() // => "function (a,b) { }" 靠!不过,为了取得形参的名字执行一个toString序列化有点太夸张了 -_-
- 去除注释:var STRIP_COMMENTS = /\/\*.*?\*\//g;
- 进一步改进:var STRIP_COMMENTS = /(\/\/.*$)|(\/\*.*?\*\/)/mg;
- 当JS代码被混淆压缩后,此机制会失效! => 严格模式
- 用DI实例化对象
- Type.$inject = ['a','b']; var instance = injector.instantiate(Type); //关键是取得a,b的实际取值!这里;
- var instance =Object.create(UnwrappedType.prototype); //ES 5.1
- invoke(Type,instance); //将当前scope绑定到this进行调用
- 任何提供了$get函数的对象,其返回值作为依赖:
- 给$get提供DI参数:cache[key] = invoke(provider.$get,provider); //直接调用=>invoke()
- 依赖的懒惰实例化(a的依赖b可以在a后面注册)
- cache拆成2个:var providerCache = {}; var instanceCache = {};
- providerCache[key + 'Provider'] = provider; //dirty trick
- “everything in Angular is a singleton”
- var instance = instanceCache[name] = invoke(provider.$get);
- 检测循环依赖
- 初始化DI之前,放置一个marker对象:if (instanceCache[name] === INSTANTIATING) { throw ... }
- 显示循环依赖的路径:var path = []; //使用一个name栈...
- Provider Constructors
- Angular doesn't care.
- 真正需要做的是多做一个类型检查:if (_.isFunction(provider)) { provider = instantiate(provider); }
- Two Injectors: The Provider Injector and The Instance Injector
- ?不能inject an instance to another provider’s constructor
- ?类似的,反之也不行:this.$get = function(aProvider) { return aProvider.$get(); }; X
- 不能通过injector.get获得provider的引用(recall:provider的存在只是为了调用$get获得其返回value)
- 常量:总是push(unshift)到invoke队列到前面
- invokeLater:invokeQueue[arrayMethod || 'push']([method,arguments]);
- constant: invokeLater('constant','unshift'),//常量:push改成unshift
- Injecting The $injectors(以$injector提供)
- 最有用的是动态get方法:var aProvider = $injector.get('aProvider');
- 注入$provide *
- 配置块(在模块加载时执行任意‘配置函数’)
- $routeProvider.when('/someUrl',{
- templateUrl: '/my/view.html',
- controller: 'MyController'
- });
- $routeProvider.when('/someUrl',{
- 运行块:injector构造时调用(module loader/injector --> module instance --> run blocks)
- module.provider('a',{$get: _.constant(42)});
- module.run(function(a) { ... })
- => moduleInstance._runBlocks.push(fn);
- 调用时机:需要等所有模块都加载完毕后执行(一次),trick:先收集到一个数组里,最后执行
- 函数模块
- hashKey:从JS对象返回一个string key
- expect(hashKey(null)).toEqual('object:null'); //格式:type:valueStr,不基于value语义,2个引用value相同,但是hash key不同?
- value.$$hashKey = _.uniqueId();
- HashMap:key可以是任意JS对象(普通js object key只能是string)
- hashKey:从JS对象返回一个string key
- Function Modules Redux
- var loadedModules = new HashMap(); //modules直接作为key?感觉更像是一个HashSet(用于判断指定module有没有被加载)
- 工厂
- 可以注入实例依赖:
- module.factory('a',function() { return 1; });
- module.factory('b',function(a) { return a + 2; });
- factory: function(key,factoryFn) { this.provider(key,{$get: factoryFn}); } //工厂就是一个provider
- enforceReturnValue(factoryFn):可以return null,但不能是undefined
- 可以注入实例依赖:
- Values
- values are not available to providers or config blocks(只用于instances)
- 实现:value: function(key,value) { this.factory(key,_.constant(value)); }
- Services(构造器函数)
- module.service('aService',function MyService(theValue) {
- this.getValue = function() { return theValue; }; });
- module.service('aService',function MyService(theValue) {
- Decorators
- module.decorator('aValue',function($delegate) { $delegate.decoratedKey = 43; }); //这里aValue是一个工厂,$delegate指代它返回的对象
- 多个装饰器:顺序(从内往外依次封装)应用:
- var instance = instanceInjector.invoke(original$get,provider);
- //对instance进行修改...
- instanceInjector.invoke(decoratorFn,null,{$delegate: instance});
- module.decorator('aValue',function($delegate) { $delegate.decoratedKey = 43; }); //这里aValue是一个工厂,$delegate指代它返回的对象
- 集成Scopes、表达式、过滤器、&注入控制器
- ?this.register('filter',require('./filter_filter'));
- ?当注册一个my过滤器时,它还以myFilter的名字作为一个正常工厂提供
- setupModuleLoader(window); //DI injector在这里配置好?
- var ngModule = angular.module('ng',[]);
- ngModule.provider('$filter',require('./filter')); //ng基础服务
- ngModule.provider('$parse',require('./parse'));
- ngModule.provider('$rootScope',require('./scope'));
- $rootScopeProvider: 配置$rootScope TTL
- $q服务,Q库的裁剪版?Promises/A+兼容(jQuery 3.0+支持)
Promises/A+规范是不是先多个then注册successCallback,最后来一个catch注册errorCallback?而ES6 Promise则在一个then里面同时注册2个callback??
- 与digest loop深度集成,可以使用$evalAsync,而不是setTimeout,来触发异步回调
- ngModule.provider('$q',require('./q'));
- 创建Deferreds
- Deferred是数据的生产者,而Promise就是消费者
- $q:function defer() { return new Deferred(); }
- d:this.promise = new Promise();
- 解析:
- promise.then(promiseSpy);
- deferred.resolve('ok'); //当调用resolve时,Promise并不立即触发(只是push到一个异步回调队列里,对于ES6 Promise来说这套机制是原生的)
- 注意,Promise也允许先resolve再then注册异步回调
- 触发digest:$rootScope.$apply();
- 防止多次解析
- 实现:维护内部的状态机!
- 确保回调被触发
- 在then注册异步回调的时候检查一下状态机特殊处理即可
- 注册多个异步回调(then链,这里实际上针对的是同一个Promise对象!)
- 否决Deferreds及捕获‘否决’
- resolve / reject
- Promise.prototype.catch = function(onRejected) { return this.then(null,onRejected); }
- finally: both then & catch
- Promise链
- 每个then调用返回一个新的Promise(或句话说,创建了另外一个新的状态机)
- value在Promise链中是传递的:one.then(two).catch(three) 这里one、two的异常都会被three捕获
- catch的返回值将会当作下一个resolve(!)
- 异常处理
- 异常不是reject到当前Promise的catch,而是传递到下一个Promise
- Promise回调返回Promise的情况
- => 将其与下一个Promise连接!
- resolve中需要对value特殊检查:if (value && _.isFunction(value.then)) {
- value.then( _.bind(this.resolve,this),_.bind(this.reject,this));
- finally的返回值需要被忽略(不在链中进一步传播)
- finally中返回一个Promise的情况:需要同步等待此Promise被resolve!**(这里有点不大好懂,需要找时间看第2遍)
- 进度通知
- defer.promise.then(null,progressNotifyCb);
- defer.notify('working...'); //哦,每个then都会返回新的Deferred对象及其内部Promise
- 但这里调用notify的线索条件来自哪里呢?
- 注意,defer.resolve之后,notify不起作用
- 立即否决:$q.reject,创建一个状态已经被否决的Promise
- 立即解析:$q.when,创建一个状态已经被解析的Promise
- Promise集合:$q.all,创建一个新的汇聚Promise,当输入的所有Promise都已经被完成后,解析自己
- var promise = $q.all([$q.when(1),$q.when(2),$q.when(3)]);
- 注意,$q.all等待的是所有Promise都成功resolve,若有一个被reject,则立即返回
- 备注:既然有$q.all,是不是也可以有$q.any/race?
- ES6风格的Promises
- 没有Deferreds这样的概念;同时,不是注册2个callback分别对应resolve/reject,而是一个callback:function(value,error) {...}
- 如何用Q来模仿ES6 Promise API:略
- 无$digest集成:$$q
- var d = $$q.defer(); //后面一样,这应该是用setTimeout异步调度的了
- vs $httpBackend
- SinonJS:模仿一个虚假的XMLHttpRequest请求 //注意本书代码的风格是TDD的
- xhr = sinon.useFakeXMLHttpRequest(); ==> requests[0].respond(200,{},'Hello');
- 302(服务器端重定向)被浏览器内部直接处理,不会到达JS代码
- 默认请求参数配置
- 请求头部
- 响应头部
- 允许CORS:withCredentials=true
- 请求变换:transformRequest:[f1,f2,...] //data
- 设置进默认配置:$http.defaults.transformRequest = ...
- 响应变换
- transformResponse: function(data,headers) { ... } //根据Content-Type?有必要这么做吗?
- JSON序列化与解析
- 二进制数据:var bb = new BlobBuilder(); bb.append(...); var data = bb.getBlob('text/plain');
- 跳过FormData
- if ((contentType && contentType.indexOf('application/json') === 0) || isJsonLike(data)) { return JSON.parse(data); }
- isJsonLike: 字符串以{或[开始?
- URL参数
- url: "...",params: { a:1,... } //车机webapp代码目前是硬拼字符串
- name有多个value的情况:a=1&a=2&a=...
- 定制的参数序列化:paramSerializer: serializeParams
- $httpParamSerializerJQLike:a: {b: {c: 42}} 被序列化为 a[b][c]=42(方括号被进一步url编码)
- Shorthand方法
- Interceptors
- var interceptors = _.map(interceptorFactories,function(fn) { return fn(); }); //又是工厂模式 -_-
- refine: return $injector.invoke(fn);
- Interceptors make heavy use of Promises. (靠!)
- serverRequest(config):return sendReq(config,reqData).then(transformResponse,transformResponse);
- 现在需要$http()之后调用$rootScope.$apply()
- Interceptors are objects that have one or more of 4 keys: request,requestError,response,and responseError.
- request返回一个修改过的request,也可以返回一个Promise,这意味着它比transform机制强大的多
- request请求拦截器核心代码如下://主要是修改config?
- var promise = $q.when(config); _.forEach(interceptors,function(interceptor) {
- promise = promise.then(interceptor.request,interceptor.requestError);
- });
- return promise.then(serverRequest);
- _.forEachRight(interceptors,function(interceptor) {
- promise = promise.then(interceptor.response,interceptor.responseError);
- var promise = $q.when(config); _.forEach(interceptors,function(interceptor) {
- var interceptors = _.map(interceptorFactories,function(fn) { return fn(); }); //又是工厂模式 -_-
- Promise扩展
- $http.get('http://teropa.info').success/error(...)
- 请求超时
- 挂起的请求
- $http.pendingRequests: 请求已发送,但响应还未接受到
- 集成$http与1.3 $applyAsync
- 优化:使得原先的多个$apply发生在一个digest中执行
- 启用:$httpProvider.useApplyAsync(true);
- 实现:if (useApplyAsync) { $rootScope.$applyAsync(resolvePromise); }
指令[
DOM编译与基本指令
- "DOM编译"?为什么不叫“DOM转换”
- directive: invokeLater('$compileProvider','directive'),//=> $provide.factory(name + 'Directive',directiveFactory);
- $compile
- 指令应用:<my-directive> ... </my-directive>
- 注意,Angularjs不依赖jQuery实现低级DOM操纵,它有一个自己的实现:jqLite //DOM esoterica is not the focus of this book
- 对DOM元素的属性应用指令
- 应用指令到class
- 应用指令到注释(囧)
- 显示指出匹配限制:ECMA
- 优先级
- 中止编译
- terminal:true ==> 如 < div ng-if="condition"> ... </div>,用于对child节点是否继续compile的控制
- 跨越多个节点应用指令
指令属性
- ng-attr- (类似于data-)
- 设置属性:attrs.$set(k,v)
- var attrs = new Attributes($(node)); ...
- 去规范化属性名称
- attrs.$set('someAttribute',43); ==> element.attr('some-attribute')
- 持续观察属性
- Attributes.prototype.$observe = function(key,fn) {
-
this.$$observers = this.$$observers || Object.create(null); //经常看到这种 a = a || b; 的写法(避免a被重复初始化)
- 备注:在Angular DI的编码风格下,JS对象的底层内存管理有什么差别吗?
- while ((match = /([\d\w\-_]+)(?:\:([^;]+))?;?/.exec(className))) { className = className.substr(match.index + match[0].length); }
- Adding Comment Directives As Attributes *
- attrs.$addClass/$removeClass/$updateClass
指令链接与作用域
- Linkingis the process of combining the compiled DOM tree with scope objects.
- var linkFn = $compile(el); //返回一个函数对象的写法其实算很高级了 -_-
- linkFn将一个scope对象关联到DOM节点子树:$compileNodes.data('$scope',scope); //调试信息,产品build并不需要?
- Directive Link Functions(DLF,每个指令有它自己的link函数?)
- 普通DLF
- compile什么也不做,把工作推迟到link内执行?(真的很绝妙,虽然是源代码级别的转换,但是体现了JIT编译器的思想...)
- linking子节点
- 最底层的DOM节点上的指令首选链接:childLinkFn = compileNodes(node.childNodes); //就是个递归
- Pre- And Post-Linking
- 保持linking节点的稳定性
- 即child nodes先用一个var stableNodeList = [];存起来...
- 跨越多个nodes的指令linking
- groupElementsLinkFnWrapper: 这种情况实际上使得函数的参数不是稳定的一种类型,而可能变成是t或者[t]两种类型
- linking与作用域继承
- 更common的情况是指令创建它们自己的scope...(a inherited scope?这似乎指的是直接引用$parent的scope?)
- => scope = scope.$new(); //创建一个新的scope,但从$parent继承
- element将得到一个ng-scope class
- 隔离的scopes(不继承的)
- 隔离的属性绑定
- ‘@’
- 可以指定不同的名字(映射):scope: { aScopeAttr: '@anAttr' },...
- 双向数据绑定(React好像是单向传递?)
- ‘=’
- 子scope的属性改变自动更新到parent scope,在link后起作用
- case '=': var parentGet = $parse(attrs[attrName]); isolateScope[scopeName] = parentGet(scope);
-
scope.$watch(parentGet,function(newValue) { isolateScope[scopeName] = newValue; })
- var el = $('< div my-directive my-attr="parentAttr"></div>'); //对myAttr的赋值将更新到parent scope的parentAttr属性上去
- 一个digest中父子属性同时改变时,父修改优先
- =* 处理my-attr="parentFn()"收到一个集合对象的情况?$watchCollection
- =?如果DOM属性不存在,则不创建watcher
- 表达式绑定
- &
- 绑定行为,而不是数据(?)如ngClick
- expression在parent scope的context中被调用,而不是当前的isolate scope,原因很简单:表达式中的函数是由当前的指令使用者定义的
- case '&': var parentExpr = $parse(attrs[attrName]);
-
isolateScope[scopeName] = function() { return parentExpr(scope); };
- 允许传参
- 例:< div my-expr="parentFunction(a,b)"></div>
- 使用命名参数:scope.myExpr({a: 1,b: 2}); //Angular中的参数使用$前缀,如$event
- 实现:引入locals参数(<-- recall back!)
控制器
- $controller服务及其provider
- var $controller = injector.get('$controller'); //加载ng的内建控制器模块
- var controller = $controller(MyController,{aDep: 42}); //初始化特定控制器的实例(function MyController(aDep) {...})
- 注册
- $controllerProvider上的register方法并不那么常见,通常使用module上的:
-
module.controller('MyController',function MyController() { });
- 全局的控制器查找(不推荐,略)
- 指令控制器
- 关联到scope *
- 在指令上指定一个controllerAs属性(app开发者用不到,compiler内部使用)
- Controllers on Isolate Scope Directives
- 当指令has isolate scope时,$scope指的是当前isolate scope,而不是surrounding scope(parent)
- bindToController
- 嗯... 同名但使用时类型会改变的变量,可以理解为2个类型变量的一个union组合变量
- Requiring Controllers
- 当directive.require指定对另一个指令的依赖时,将对应到link函数的第4个参数,并可从中得到依赖指令的controller(其实,对JS来说,不存在私有变量)
- Requiring Multiple Controllers:略
- Self-Requiring Directives:当一个指令定义了自己的controller,而不是依赖于其他指令的
- Requiring Controllers in Multi-Element Directives
- Requiring Controllers from Parent Elements
- require: '^myDirective'
- ^^ 直接从parent寻找,而不是先从siblings开始
- 可选的Requiring:找不到controller不报错
- require: '?noSuchDirective'
- ngController指令
- < div ng-controller="TodoController as todoCtrl"> //注意,这里使用正则表达式来提取嵌入的变量名
- 从scope中查找控制器的constructor:locals => global(window)
指令模板
- replace: ture //此做法已废弃
- template: '< div my-other-directive></div>' //使得可以组件化的风格编写webapp
- 模板函数 //实际上,之前的模板字符串就被编译为一个函数
- 模板内的指令将接受到一个isolate scope **
- 异步模板:templateUrl
- 如何暂停-重启编译??
- 需要记住当前还没有应用的指令(保存状态):applyDirectivesToNode
- templateUrl也可以是一个函数:Template URL Functions(函数的返回值就是一个url?)
- 一个元素不允许超过1个templateUrl(技术实现的限制??)
- var templateDirective = prevIoUsCompileContext.templateDirective; //这是什么意思呢??-_-
- 链接异步指令(这将构造异步回调链)
- trick 1: 返回一个特殊的“delayed node link function”(?这个时候模板内容可能还没有成功下载下来吧)
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives,$compileNode,attrs,prevIoUsCompileContext);
- afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes);
- trick 2:让link在template接受到之前发生
- 引入var linkQueue = []; //所以,对于一般的异步回调链而言,需要检查2种情况?
- Linking Directives that Were Compiled Earlier
- 保留isolate scope指令 *
- 保留控制器指令 *
Directive Transclusion(k,感觉这部分内容是本书最复杂的...)
- 依我的理解,Transclusion指的是指令模板在被嵌套包含的情况下,其binding上下文可以自动对接的意思???
- ‘Transclusion’也可以被cloned:transclude: ’element’
- 基本Transclusion(即没有scope binding的情况)
- transclude: true
- var $transcludedNodes = $compileNode.clone().contents(); //注意这里的clone调用
- link: function(scope,ctrl,transclude) { element.find('[in-template]').append(transclude()); } //
- ... At its core,the transclusion function is really a link function. //Yes,of course
- Transclusion And Scopes
- 绑定的scope是定义时的(文法作用域),而不是使用时的(动态作用域)
- boundTranscludeFn = function() { return linkFn.nodeLinkFn.transclude(scope); } //到处是这种引用定义时上下文的闭包函数...
- 修改scope.$new(),指定parentE
-
We should create a special transclusion scope that prototypally inherits from the surrounding scope,but whose $parent is set to the Scope of the transclusion directive.
-
!the
scope-bound transclusion function(靠~ 这个地方真的有点复杂,它涉及到闭包下的对象生命周期控制问题...)
- :boundTranscludeFn = function(containingScope) { var transcludedScope = scope.$new(false,containingScope); ...
- Transclusion from Descendant Nodes
- ???
- Transclusion in Controllers
- The Clone Attach Function
- link时先clone,并可修改;与Transclusion不同(?)
- Transclusion with Template URLs
- Transclusion with Multi-Element Directives
- ngTransclude指令
- 提供一个内容替换掉stub位置(实现非常简单:...)
- Full Element Transclusion
- Requiring Controllers from Transcluded Directives
- $linkNodes.data('$' + name + 'Controller',controller.instance); //部分构造
插值: { { expr }}
- $interpolate服务(app开发人员很少使用,直接集成在$compile里面)
- 字符串插值
- var interpolateFn = $interpolate('{ {a}} and { {b}}'); interpolateFn({a: 1,b: 2}); // => '1 and 2'
- parts.push(text / expFn ); //expFn = $parse(exp);
- Value Stringification
- 转义:'\\{\\{expr\\}\\}' //注意这里\还有JS字符串转义
- 文本节点插值
- 属性插值
- addAttrInterpolateDirective:基本情况同Text Node的
- 与元素上其他指令交互:
- 其他指令可能通过Attributes.$observe在观察元素上的属性变化,当属性由于插值变化时,需要触发这些observers:
-
不适用jQuery的attr(),改为attrs.$set(name,newValue);
- 指令希望收到的是插值完成后的属性值:
- 当前插值发生在first digest after linking,但实际上应该是before any digests
-
优先级设为100(高)+ 挂载到pre-link时机
- isolate scope指令的问题
-
case '@': ... destination[scopeName] = $interpolate(attrs[attrName])(scope); ...
- 指令应用时可能删除元素属性(或修改其值),此种情况下需要移除插值调用
- 需要防止用户在事件处理器里进行插值,如onclick:<button onclick="{ {myFunction()}}">
- Optimizing Interpolation Watches With A Watch Delegate
- Making Interpolation Symbols Configurable(默认是2个大括号对)
- $interpolateProvider.startSymbol('FOO').endSymbol('OOF');
- 需要动态构造RegExp:new RegExp(startSymbol.replace(/./g,escapeChar),'g');
自启
- 实现ngClick
- Angularjs框架有点像是一个编译器+解释器框架,而编写定制指令则有点像一个编译器后端优化Pass...
- var button = $('<button ng-click="doSomething()"></button>');
- link: function(scope,element) { element.on('click',function(evt) { scope.$apply(attrs.ngClick); }); }
- 注入$event:scope.$eval(attrs.ngClick,{$event: evt});
- 注册:ngModule.directive('ngClick',require('./directives/ng_click'));
- Bootstrapping Angular Applications Manually
- 手工自启
- injector.invoke(['$compile','$rootScope',function($compile,$rootScope) { $compile($element)($rootScope); }]); //DOM不仅仅被compile,而且被link到root scope
- 运行初始digest:=> $rootScope.$apply(function() { $compile($element)($rootScope); });
- 严格的DI?window.angular.bootstrap(element,['myModule'],{strictDi: true});
- 自动(via ng-app属性)
- $(document).ready(function() { ... });
- window.angular.bootstrap(foundAppElement,foundModule? [foundModule]: []);
- Building The Production Bundle
- ./node_modules/browserify/bin/cmd.js src/bootstrap.js > myangular.js //递归扫描所有的require,打包到一个js文件里面去
- npm install --save-dev uglifyjs
- package.json: "build:minified": "browserify src/bootstrap.js | uglifyjs -mc > myangular.min.js"
- Running An Example App
- app.js: angular.module('myExampleApp',[]); //?
原文链接:https://www.f2er.com/angularjs/147183.html
- "DOM编译"?为什么不叫“DOM转换”
- directive: invokeLater('$compileProvider','directive'),//=> $provide.factory(name + 'Directive',directiveFactory);
- $compile
- 指令应用:<my-directive> ... </my-directive>
- 注意,Angularjs不依赖jQuery实现低级DOM操纵,它有一个自己的实现:jqLite //DOM esoterica is not the focus of this book
- 对DOM元素的属性应用指令
- 应用指令到class
- 应用指令到注释(囧)
- 显示指出匹配限制:ECMA
- 优先级
- 中止编译
- terminal:true ==> 如 < div ng-if="condition"> ... </div>,用于对child节点是否继续compile的控制
- 跨越多个节点应用指令
- ng-attr- (类似于data-)
- 设置属性:attrs.$set(k,v)
- var attrs = new Attributes($(node)); ...
- 去规范化属性名称
- attrs.$set('someAttribute',43); ==> element.attr('some-attribute')
- 持续观察属性
- Attributes.prototype.$observe = function(key,fn) {
- this.$$observers = this.$$observers || Object.create(null); //经常看到这种 a = a || b; 的写法(避免a被重复初始化)
- 备注:在Angular DI的编码风格下,JS对象的底层内存管理有什么差别吗?
- Attributes.prototype.$observe = function(key,fn) {
- while ((match = /([\d\w\-_]+)(?:\:([^;]+))?;?/.exec(className))) { className = className.substr(match.index + match[0].length); }
- Adding Comment Directives As Attributes *
- attrs.$addClass/$removeClass/$updateClass
- Linkingis the process of combining the compiled DOM tree with scope objects.
- var linkFn = $compile(el); //返回一个函数对象的写法其实算很高级了 -_-
- linkFn将一个scope对象关联到DOM节点子树:$compileNodes.data('$scope',scope); //调试信息,产品build并不需要?
- Directive Link Functions(DLF,每个指令有它自己的link函数?)
- 普通DLF
- compile什么也不做,把工作推迟到link内执行?(真的很绝妙,虽然是源代码级别的转换,但是体现了JIT编译器的思想...)
- linking子节点
- 最底层的DOM节点上的指令首选链接:childLinkFn = compileNodes(node.childNodes); //就是个递归
- Pre- And Post-Linking
- 保持linking节点的稳定性
- 即child nodes先用一个var stableNodeList = [];存起来...
- 跨越多个nodes的指令linking
- groupElementsLinkFnWrapper: 这种情况实际上使得函数的参数不是稳定的一种类型,而可能变成是t或者[t]两种类型
- linking与作用域继承
- 更common的情况是指令创建它们自己的scope...(a inherited scope?这似乎指的是直接引用$parent的scope?)
- => scope = scope.$new(); //创建一个新的scope,但从$parent继承
- element将得到一个ng-scope class
- 更common的情况是指令创建它们自己的scope...(a inherited scope?这似乎指的是直接引用$parent的scope?)
- 隔离的scopes(不继承的)
- 隔离的属性绑定
- ‘@’
- 可以指定不同的名字(映射):scope: { aScopeAttr: '@anAttr' },...
- ‘@’
- 双向数据绑定(React好像是单向传递?)
- ‘=’
- 子scope的属性改变自动更新到parent scope,在link后起作用
- case '=': var parentGet = $parse(attrs[attrName]); isolateScope[scopeName] = parentGet(scope);
- scope.$watch(parentGet,function(newValue) { isolateScope[scopeName] = newValue; })
- case '=': var parentGet = $parse(attrs[attrName]); isolateScope[scopeName] = parentGet(scope);
- var el = $('< div my-directive my-attr="parentAttr"></div>'); //对myAttr的赋值将更新到parent scope的parentAttr属性上去
- 一个digest中父子属性同时改变时,父修改优先
- =* 处理my-attr="parentFn()"收到一个集合对象的情况?$watchCollection
- =?如果DOM属性不存在,则不创建watcher
- 表达式绑定
- &
- 绑定行为,而不是数据(?)如ngClick
- expression在parent scope的context中被调用,而不是当前的isolate scope,原因很简单:表达式中的函数是由当前的指令使用者定义的
- case '&': var parentExpr = $parse(attrs[attrName]);
- isolateScope[scopeName] = function() { return parentExpr(scope); };
- 允许传参
- 例:< div my-expr="parentFunction(a,b)"></div>
- 使用命名参数:scope.myExpr({a: 1,b: 2}); //Angular中的参数使用$前缀,如$event
- 实现:引入locals参数(<-- recall back!)
- $controller服务及其provider
- var $controller = injector.get('$controller'); //加载ng的内建控制器模块
- var controller = $controller(MyController,{aDep: 42}); //初始化特定控制器的实例(function MyController(aDep) {...})
- 注册
- $controllerProvider上的register方法并不那么常见,通常使用module上的:
- module.controller('MyController',function MyController() { });
- $controllerProvider上的register方法并不那么常见,通常使用module上的:
- 全局的控制器查找(不推荐,略)
- 指令控制器
- 关联到scope *
- 在指令上指定一个controllerAs属性(app开发者用不到,compiler内部使用)
- Controllers on Isolate Scope Directives
- 当指令has isolate scope时,$scope指的是当前isolate scope,而不是surrounding scope(parent)
- bindToController
- 嗯... 同名但使用时类型会改变的变量,可以理解为2个类型变量的一个union组合变量
- Requiring Controllers
- 当directive.require指定对另一个指令的依赖时,将对应到link函数的第4个参数,并可从中得到依赖指令的controller(其实,对JS来说,不存在私有变量)
- Requiring Multiple Controllers:略
- Self-Requiring Directives:当一个指令定义了自己的controller,而不是依赖于其他指令的
- Requiring Controllers in Multi-Element Directives
- Requiring Controllers from Parent Elements
- require: '^myDirective'
- ^^ 直接从parent寻找,而不是先从siblings开始
- 可选的Requiring:找不到controller不报错
- require: '?noSuchDirective'
- ngController指令
- < div ng-controller="TodoController as todoCtrl"> //注意,这里使用正则表达式来提取嵌入的变量名
- 从scope中查找控制器的constructor:locals => global(window)
- replace: ture //此做法已废弃
- template: '< div my-other-directive></div>' //使得可以组件化的风格编写webapp
- 模板函数 //实际上,之前的模板字符串就被编译为一个函数
- 模板内的指令将接受到一个isolate scope **
- 异步模板:templateUrl
- 如何暂停-重启编译??
- 需要记住当前还没有应用的指令(保存状态):applyDirectivesToNode
- templateUrl也可以是一个函数:Template URL Functions(函数的返回值就是一个url?)
- 一个元素不允许超过1个templateUrl(技术实现的限制??)
- var templateDirective = prevIoUsCompileContext.templateDirective; //这是什么意思呢??-_-
- 链接异步指令(这将构造异步回调链)
- trick 1: 返回一个特殊的“delayed node link function”(?这个时候模板内容可能还没有成功下载下来吧)
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives,$compileNode,attrs,prevIoUsCompileContext);
- afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes);
- trick 2:让link在template接受到之前发生
- 引入var linkQueue = []; //所以,对于一般的异步回调链而言,需要检查2种情况?
- trick 1: 返回一个特殊的“delayed node link function”(?这个时候模板内容可能还没有成功下载下来吧)
- Linking Directives that Were Compiled Earlier
- 保留isolate scope指令 *
- 保留控制器指令 *
- 依我的理解,Transclusion指的是指令模板在被嵌套包含的情况下,其binding上下文可以自动对接的意思???
- ‘Transclusion’也可以被cloned:transclude: ’element’
- 基本Transclusion(即没有scope binding的情况)
- transclude: true
- var $transcludedNodes = $compileNode.clone().contents(); //注意这里的clone调用
- link: function(scope,ctrl,transclude) { element.find('[in-template]').append(transclude()); } //
- ... At its core,the transclusion function is really a link function. //Yes,of course
- Transclusion And Scopes
- 绑定的scope是定义时的(文法作用域),而不是使用时的(动态作用域)
- boundTranscludeFn = function() { return linkFn.nodeLinkFn.transclude(scope); } //到处是这种引用定义时上下文的闭包函数...
- 修改scope.$new(),指定parentE
- We should create a special transclusion scope that prototypally inherits from the surrounding scope,but whose $parent is set to the Scope of the transclusion directive.
- !the scope-bound transclusion function(靠~ 这个地方真的有点复杂,它涉及到闭包下的对象生命周期控制问题...)
- :boundTranscludeFn = function(containingScope) { var transcludedScope = scope.$new(false,containingScope); ...
- Transclusion from Descendant Nodes
- ???
- Transclusion in Controllers
- The Clone Attach Function
- link时先clone,并可修改;与Transclusion不同(?)
- Transclusion with Template URLs
- Transclusion with Multi-Element Directives
- ngTransclude指令
- 提供一个内容替换掉stub位置(实现非常简单:...)
- Full Element Transclusion
- Requiring Controllers from Transcluded Directives
- $linkNodes.data('$' + name + 'Controller',controller.instance); //部分构造
- $interpolate服务(app开发人员很少使用,直接集成在$compile里面)
- 字符串插值
- var interpolateFn = $interpolate('{ {a}} and { {b}}'); interpolateFn({a: 1,b: 2}); // => '1 and 2'
- parts.push(text / expFn ); //expFn = $parse(exp);
- Value Stringification
- 转义:'\\{\\{expr\\}\\}' //注意这里\还有JS字符串转义
- 文本节点插值
- 属性插值
- addAttrInterpolateDirective:基本情况同Text Node的
- 与元素上其他指令交互:
- 其他指令可能通过Attributes.$observe在观察元素上的属性变化,当属性由于插值变化时,需要触发这些observers:
- 不适用jQuery的attr(),改为attrs.$set(name,newValue);
- 指令希望收到的是插值完成后的属性值:
- 当前插值发生在first digest after linking,但实际上应该是before any digests
- 优先级设为100(高)+ 挂载到pre-link时机
- isolate scope指令的问题
- case '@': ... destination[scopeName] = $interpolate(attrs[attrName])(scope); ...
- 指令应用时可能删除元素属性(或修改其值),此种情况下需要移除插值调用
- 其他指令可能通过Attributes.$observe在观察元素上的属性变化,当属性由于插值变化时,需要触发这些observers:
- 需要防止用户在事件处理器里进行插值,如onclick:<button onclick="{ {myFunction()}}">
- Optimizing Interpolation Watches With A Watch Delegate
- Making Interpolation Symbols Configurable(默认是2个大括号对)
- $interpolateProvider.startSymbol('FOO').endSymbol('OOF');
- 需要动态构造RegExp:new RegExp(startSymbol.replace(/./g,escapeChar),'g');
- 实现ngClick
- Angularjs框架有点像是一个编译器+解释器框架,而编写定制指令则有点像一个编译器后端优化Pass...
- var button = $('<button ng-click="doSomething()"></button>');
- link: function(scope,element) { element.on('click',function(evt) { scope.$apply(attrs.ngClick); }); }
- 注入$event:scope.$eval(attrs.ngClick,{$event: evt});
- 注册:ngModule.directive('ngClick',require('./directives/ng_click'));
- Bootstrapping Angular Applications Manually
- 手工自启
- injector.invoke(['$compile','$rootScope',function($compile,$rootScope) { $compile($element)($rootScope); }]); //DOM不仅仅被compile,而且被link到root scope
- 运行初始digest:=> $rootScope.$apply(function() { $compile($element)($rootScope); });
- 严格的DI?window.angular.bootstrap(element,['myModule'],{strictDi: true});
- 自动(via ng-app属性)
- $(document).ready(function() { ... });
- window.angular.bootstrap(foundAppElement,foundModule? [foundModule]: []);
- Building The Production Bundle
- ./node_modules/browserify/bin/cmd.js src/bootstrap.js > myangular.js //递归扫描所有的require,打包到一个js文件里面去
- npm install --save-dev uglifyjs
- package.json: "build:minified": "browserify src/bootstrap.js | uglifyjs -mc > myangular.min.js"
- Running An Example App
- app.js: angular.module('myExampleApp',[]); //?
- 手工自启