AngularJS 学习笔记

18 阅读6分钟

最近又开始维护好几年前的老代码了,为了应对接下来的需求,对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')。