最近又开始维护好几年前的老代码了,为了应对接下来的需求,对AngularJS做了简单的复习。
基础知识
脏检查机制
用一句话来概括“脏检查机制”:Angular 将双向绑定转换为一堆 watch 表达式,然后递归检查这些 watch 表达式的结果是否变了,如果变了,则执行相应的 watcher 函数。等到 Model 的值不再变化,也就不会再有 watcher 函数被触发,一个完整的 digest 循环就结束了。这时,浏览器就会重新渲染 DOM 来体现 model 的改变。这里所说的 watcher 函数,是由 View 上的指令(如 ngBind、ngShow、ngHide 等)或 {{}}
表达式(严格来说是$compile
服务)所注册的。指令在 Angular 的 compile 阶段会被逐一解析、注册。
$scope
$rootScope
对象是Angular中所有$scope
对象的祖先。所有的$scope
都是直接或者间接利用$rootScope
提供的$new
方法创建的。都从$rootScope
中继承了$new
,$watch
,$watchGroup
、$watchCollection
、$digest
、$destroy
、$eval
、$evalAsync
、$apply
、$on
、$emit
、$broadcast
等方法,并且有$id
、$parent
这两个属性。
$watch
watch 表达式很灵活:可以是一个函数,可以是$scope
上的一个属性名,也可以是一个字符串形式的表达式。$scope
上的属性名或表达式,最终仍会被$parse
服务解析为响应的获取属性值的函数。$watch
函数会返回一个反注册函数,一旦我们调用它,就可以移除刚才注册的 watcher。
需要注意的是,Angular 默认是不会使用 angular.equals()函数进行深度比较的,因为使用===比较会更快,所以,它对数组或者 Object 进行比较时检查的是引用。这就导致内容完全相同的两个表达式被判定为不同。如果需要进行深度比较,第三个可选参数 objectEquality,需要显式设置为 true,如$watch('someExp',function(),{...},true)
。
Angular 还提供了$watchGroup
、$watchCollection
方法来监听数组或者是一组属性。
$digest
$digest
循环实际上包括两个while循环。它们分别是:处理$evalAsync
的异步运算队列,处理$watch
的 watchers 队列。
遍历一遍所有 watcher 函数称为一轮脏检查。执行完一轮脏检查,如果任何一个 watcher 所监听的值改变过,那么就会重新再进行一轮脏检查,直到所有的 watcher 函数都报告其所监听的值不再变了。
从第一轮脏检查到结果变得稳定,这个过程就是一个完整的$digest
循环。当$digest
循环结束时,才把模型的变化结果更新到 DOM 中去。这样可以合并多个更新,防止频繁的 DOM 操作。
$apply
$digest
是一个内部函数,正常的应用代码中是不应该直接调用它的。要想主动触发它,就要调用$scope.$apply
函数,它是触发 Angular “脏检查机制”的常用公开接口。
$apply
函数也很少需要主动调用,因为在各种Angular事件指令以及$timeout
等服务中,都会自动调用它一次来确保界面刷新。如果你要自己挂接第三方组件的事件,那么就要记得调用一次$apply
了,否则在这个事件处理函数中对 scope 变量的更新就不会更新到界面上。
内置$
服务替代原生服务
应注意以下几点:
$timeout
替代 setTimeout$Interval
替代 setInterval$window
替代 window$document
替代 document$resource
或$http
替代$.ajax
- angular.element 替代
$
这样可以避免手动调用$scope.$apply
启动“脏检查机制”的问题、获得更多的 API,并在测试中更好地进行 Mock。
在 AngularJS 中 Provider、Factory 和 Service 有什么区别?
Factory 相当于拿到:factoryResult;
var factoryResult = MyFactory();
Service 相当于拿到:serviceObj;
var serviceObj = new MyService();
Provider 相当于拿到:providerObj;
var instance = new MyProvider();
var providerObj = instance.$get();
Factory/service 是第一个注入时才实例化,而 provider 不是,它是在 config 之前就已实例化好。
$injector
// 在控制器或其他地方获取 $injector 服务
app.controller("MyController", [
"$injector",
function ($injector) {
// 使用 $injector 来获取服务实例
var myServiceInstance = $injector.get("myService");
// 手动创建新的服务实例
var newServiceInstance = $injector.instantiate(myService);
},
]);
- get(name):用于获取指定名称的服务实例。通过名称获取已注册的服务,返回该服务的实例。
- invoke(fn, self, locals):用于调用函数并注入依赖项。可以调用指定的函数,并自动注入函数所需的依赖项。
- annotate(fn):用于获取函数的参数列表,包括依赖项。返回一个函数的参数列表,包括需要注入的依赖项。
- has(name):用于检查是否存在指定名称的服务。检查指定名称的服务是否已经注册。
- instantiate(Type, locals):用于实例化一个构造函数,并注入依赖项。实例化一个构造函数,并自动注入构造函数所需的依赖项。
- loadNewModules(modules):用于加载新的模块并使其可用于依赖注入。这个方法的作用是动态加载新的模块,以便在应用程序中使用新模块中定义的服务,以实现动态模块加载和依赖注入。
独立 scope 声明中的“@”、“&”、“=”三种形式
@:用于单向绑定,将父作用域的值传递给指令的作用域。这种绑定只支持字符串,并且支持使用{{}}
表达式进行插值。
app.directive("myDirective", function () {
return {
restrict: "E",
scope: {
isolatedAttribute: "@",
},
template: "<div>{{isolatedAttribute}}</div>",
};
});
=:用于双向绑定,将父作用域的模型与指令的独立作用域的模型进行关联。这意味着对一个模型的更改会影响另一个模型,反之亦然。
app.directive("myDirective", function () {
return {
restrict: "E",
scope: {
isolatedBinding: "=",
},
template: '<input ng-model="isolatedBinding">',
};
});
&:用于在独立作用域中调用父作用域的函数,并传递参数。这种绑定允许指令调用父作用域中的函数。
app.directive("myDirective", function () {
return {
restrict: "E",
scope: {
isolatedExpression: "&",
},
template: '<button ng-click="isolatedExpression()">Click</button>',
};
});
restrict
A
:表示指令可以作为 HTML 元素的属性使用。例如:<div my-directive></div>
E
:表示指令可以作为 HTML 元素使用。例如:<my-directive></my-directive>
C
:表示指令可以作为类名使用。例如:<div class="my-directive"></div>
M
:表示指令可以作为 HTML 注释使用。例如:<!-- directive: my-directive -->
这些值可以组合使用,例如'A'和'E'可以一起使用,表示指令可以作为 HTML 元素和属性使用。但是最佳实践建议只使用'A'和'E'两种类型。
$observe
$observe
方法是在 link 函数中使用的,用于监视指令的属性变化。$observe
方法接受两个参数:属性名和回调函数。当指令的属性发生变化时,回调函数会被调用。$observe
方法只能用于监视指令的属性,而不能用于监视作用域中的变量。$observe
方法通常用于监视 HTML 属性的变化,例如 class、style 等。- 当指令的属性值是插值表达式(如{{
variable
}})时,$observe
方法会在表达式的值发生变化时被调用。
app.directive("myDirective", function () {
return {
restrict: "A",
link: function (scope, element, attrs) {
attrs.$observe("myAttribute", function (value) {
console.log("myAttribute值发生变化:" + value);
});
},
};
});
angular.element
angular.element 是 AngularJS 中用于操作 DOM 元素的函数。它返回一个包装了原生 DOM 元素的 jQuery lite 对象,因此可以使用类似 jQuery 的方法来操作 DOM。
在 AngularJS 中,angular.element 可以用于以下情况:
在指令的链接函数中使用,以便在指令的 DOM 元素上执行操作。 在控制器中使用,但通常不建议在控制器中直接操作 DOM 元素。 以下是一个简单的示例,演示了如何在 AngularJS 中使用 angular.element:
// 在指令中使用 angular.element
app.directive("myDirective", function () {
return {
link: function (scope, element, attrs) {
// 使用 angular.element 操作指令的 DOM 元素
angular.element(element).addClass("highlight");
},
};
});
// 在控制器中使用 angular.element
app.controller("myController", function ($scope, $element) {
// 不建议在控制器中直接操作 DOM 元素
// 但如果需要,可以使用 angular.element
angular.element(document.querySelector("#myElement")).addClass("highlight");
});
- addClass(className): 添加一个或多个类名到元素。
- removeClass(className): 移除一个或多个类名。
- toggleClass(className, condition): 如果存在(不存在)就移除(添加)一个类。
- attr(name, value): 获取或设置元素的属性。
- removeAttr(name): 移除元素的属性。
- css(property, value): 获取或设置元素的 CSS 样式。
- on(eventType, handler): 绑定事件处理程序。
- off(eventType, handler): 移除事件处理程序。
- find(selector): 在元素内部查找匹配选择器的元素。
- parent(): 获取父元素。
- children(): 获取子元素。
- html(): 获取或设置元素的 HTML 内容。
- text(): 获取或设置元素的文本内容。
- clone(): 克隆元素。
一个简单的 DI
const DI = {
/**
* 保存能够被注入的服务
*/
providerCache: {},
/**
* 注册一个新的服务时,以key: value形式保存在providerCache map中
* @param key
* @param value
*/
register(key, value) {
this.providerCache[key] = value;
},
/**
* 实现依赖注入
* @param fn
* @param self
* @returns {*}
*/
inject(fn, self) {
const $inject = this.annotate(fn), //获得函数的参数(被注入的对象key值)
args = [];
//遍历providerCache获得所有注入的对象,用一个数组记录
for (var i = 0, len = $inject.length; i < len; i++) {
args.push(this.providerCache[$inject[i]]);
}
if (Array.isArray(fn)) {
fn = fn[len];
}
//注入
return fn.apply(self, args);
},
/**
* 提取函数的参数
* @param fn
* @returns {Array}
*/
annotate(fn) {
const fnString = fn.toString(),
args = [],
FUNC_ARGS = /^function\s*[^(]*\(\s*([^)]*)\s*\)/m,
FUNC_ARG_SPLIT = /,\s*/;
if (isFunction(fn)) {
args = fnString.match(FUNC_ARGS)[1].split(FUNC_ARG_SPLIT);
} else if (isArray(fn)) {
args = fn.slice(0, fn.length - 1);
}
return args;
},
};
function isFunction(fn) {
return typeof fn === 'function';
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
/**
* provider定义方法
* @param name
* @param fn
*/
function registerProvider(name, fn) {
const obj = DI.inject(fn);
DI.register(name, obj);
}
/**
* controller定义方法
* @param name
* @param fn
*/
function registerController(name, fn) {
DI.inject(fn);
}
registerProvider('provider1', function () {
return {
provider1: 'foo',
};
});
registerProvider('provider2', function (provider1) {
return {
provider2: provider1.provider1 + ' bar',
};
});
registerController('controller', [
'provider2',
function (provider2) {
console.log(provider2.provider2);
},
]);
在 Angular 中,这个注册表就叫作 module。
根据 DI 的原理,一个自然的推论就是:被注入的对象都是单例对象,因为创建了一个,就可以始终使用它了,不需要多次创建。因此,如果你需要在 Angular 中跨 Controller 共享数据或者通讯,那么就可以创建一个 Service/Value/Constant 等,然后把它们分别注入到两个 Controller 中,这两个 Controller 就自然会共享同一个对象了。
另外,由于实现 DI 需要容器进行处理,因此,只有少数几种函数可以使用依赖注入,它们是:controller、service/factory/provider、directive、filter、animation、config、run、decorator。简单地说,通过 module 注册进来的函数都可以,因为 module 会负责管理它们。
有一种场合不能使用依赖注入,那就是循环依赖。我们就使用手动注入的方式解决循环依赖问题,$injector.get('$http')。