MVVM大比拼之AngularJS源码精析

简介

AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档。这里推荐几个深度学习的资料:

  • AngularJS学习笔记 作者:邹业盛 。这个笔记非常细致,记录了作者对于AngularJS各个方面的思考,其中也不乏源码级的分析。
  • 构建自己的AngularJS 。虽然放出第一章后作者就写书去了。但这第一部分已经足以带领读者深入窥探angularJS在核心概念上的实现,特别是dirty check。有愿意继续深入的读者可以去买书。
  • Design Decisions in AngularJS。 google io 上AngularJS作者的演讲视频,非常值得一看。

其实随便google一下就会看到非常的多的AngularJS的深度文章,AngularJS的开发团队本身对外也非常活跃。特别是现在AngularJS 2.0也在火热设计和开发中,大家完全可以把握这个机会跟进一下。设计文档在这里。在这些资料面前,我的源码分析只能算是班门弄斧了。不过人总要自己思考,否则和咸鱼没有区别。 以下源码以1.3.0为准。

入口

除了使用 ng-app,angular还有手工的入口:

  1. angular.bootstrap(document,['module1','module2'])

  

angularJS build的相关信息和文件结构翻阅一下gruntFile就清楚了。我们直击/src/Angular.js 的1381行 bootstrap 定义:

  1. function bootstrap(element, modules, config) {
  2. if (!isObject(config)) config = {};
  3. var defaultConfig = {
  4. strictDi: false
  5. };
  6. config = extend(defaultConfig, config);
  7. var doBootstrap = function() {
  8. element = jqLite(element);
  9.  
  10. if (element.injector()) {
  11. var tag = (element[0] === document) ? 'document' : startingTag(element);
  12. throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag);
  13. }
  14.  
  15. modules = modules || [];
  16. modules.unshift(['$provide', function($provide) {
  17. $provide.value('$rootElement', element);
  18. }]);
  19. modules.unshift('ng');
  20. var injector = createInjector(modules, config.strictDi);
  21. injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
  22. function(scope, element, compile, injector, animate) {
  23. scope.$apply(function() {
  24. element.data('$injector', injector);
  25. compile(element)(scope);
  26. });
  27. }]
  28. );
  29. return injector;
  30. };
  31.  
  32. var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/;
  33.  
  34. if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) {
  35. return doBootstrap();
  36. }
  37.  
  38. window.name = window.name.replace(NG_DEFER_BOOTSTRAP, '');
  39. angular.resumeBootstrap = function(extraModules) {
  40. forEach(extraModules, function(module) {
  41. modules.push(module);
  42. });
  43. doBootstrap();
  44. };
  45. }

  

已经熟练使用AngularJS的读者应该马上就注意到,代码中部的createInjector和后面的几行代码就已经暴露了两个核心概念的入口:“依赖注入”和“视图编译”。

依赖注入

先不要急着去看 createInjector 的定义, 先看看后面这一句 injector.invoke()。在angular中有显式注入和隐式注入,这里是显式。往 invoke 中传如的参数是个数组,数组前n-1个参数对应着对最后一个函数的每一个参数,也就是最后一个函数中要传入的依赖。不难猜想,injector应该是个对象,其中保存了所有已经实例化过的service等可以作为依赖的函数或对象,调用invoke时就会按名字去取依赖。现在让我们去验证吧。翻到 /src/auto/injector.js 609:

  1. function createInjector(modulesToLoad, strictDi) {
  2. strictDi = (strictDi === true);
  3. var INSTANTIATING = {},
  4. providerSuffix = 'Provider',
  5. path = [],
  6. loadedModules = new HashMap(),
  7. providerCache = {
  8. $provide: {
  9. provider: supportObject(provider),
  10. factory: supportObject(factory),
  11. service: supportObject(service),
  12. value: supportObject(value),
  13. constant: supportObject(constant),
  14. decorator: decorator
  15. }
  16. },
  17. providerInjector = (providerCache.$injector =
  18. createInternalInjector(providerCache, function() {
  19. throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
  20. }, strictDi)),
  21. instanceCache = {},
  22. instanceInjector = (instanceCache.$injector =
  23. createInternalInjector(instanceCache, function(servicename) {
  24. var provider = providerInjector.get(servicename + providerSuffix);
  25. return instanceInjector.invoke(provider.$get, provider, undefined, servicename);
  26. }, strictDi));
  27.  
  28. forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });
  29.  
  30. return instanceInjector;
  31.  
  32. /*下面省略若干函数定义*/
  33. }

  

我们从最后的返回值看到,真实的injector对象又是由 createInternalInjector 创造的。只不过最后对于所有需要加载的模块(也就是参数modulesToLoad),主动使用instanceInjector.invoke执行了一次。明显这个invoke和前面讲到的invoke是同一个函数,但是前面传的参是数组,用来显示传入依赖,这里传的参看起来是函数,那很有可能是隐式注入的调用。 另外值得注意的是这里有个 providerInjector 也是用 createInternalInjector 创造的。它在instancInjector 的 createInternalInjector 中被用到了。

下面让我们看看 createInternalInjector :

  1. function createInternalInjector(cache, factory) {
  2.  
  3. function getService(serviceName) {
  4. /*省略*/
  5. }
  6.  
  7. function invoke(fn, self, locals, serviceName){
  8. if (typeof locals === 'string') {
  9. serviceName = locals;
  10. locals = null;
  11. }
  12.  
  13. var args = [],
  14. $inject = annotate(fn, strictDi, serviceName),
  15. length, i,
  16. key;
  17.  
  18. for(i = 0, length = $inject.length; i < length; i++) {
  19. key = $inject[i];
  20. if (typeof key !== 'string') {
  21. throw $injectorMinErr('itkn',
  22. 'Incorrect injection token! Expected service name as string, got {0}', key);
  23. }
  24. args.push(
  25. locals && locals.hasOwnProperty(key)
  26. ? locals[key]
  27. : getService(key)
  28. );
  29. }
  30. if (!fn.$inject) {
  31. // this means that we must be an array.
  32. fn = fn[length];
  33. }
  34.  
  35. // http://jsperf.com/angularjs-invoke-apply-vs-switch
  36. // #5388
  37. return fn.apply(self, args);
  38. }
  39.  
  40. function instantiate(Type, locals, serviceName) {
  41. /*省略*/
  42. }
  43.  
  44. return {
  45. invoke: invoke,
  46. instantiate: instantiate,
  47. get: getService,
  48. annotate: annotate,
  49. has: function(name) {
  50. return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
  51. }
  52. };
  53. }

  

我们快先看看之前对 invoke 函数的猜测是否正确,我们前面看到了调用它时第一个参数为数组或者函数,如果你记性不错的话,应该也注意到前面还有一句:

  1. instanceInjector.invoke(provider.$get, provider, undefined, servicename)

  

好,我们来看 invoke。注意 $inject = annotate(fn, strictDi, serviceName) 。这里的第一个参数 fn 就是之前提到的可以是数组也可以是函数。大家自己去看 annotate 的定义吧,就是这一句,提取出了所有依赖的名字,对于隐式注入试用 toString 加上 正则匹配来提取的,所以如果 angular 应用代码压缩时进行了变量名混淆的话,隐式注入就失效了。继续看,提取出名字之后,通过 getService 获取到了每一个依赖的实例,最后在用 fn.apply 传入依赖即可。 还记得之前的 providerInjector 吗,它其实是用来提供一些快速注册 service 等可依赖实例的。它提供的一些方法其实都直接暴露到了 angular 对象上,大家如果仔细看过文档其实就很明了了:

总体来说依赖注入在实现上并没有什么特别巧妙的地方,但有价值的是angular从很早就有了完整的模块化体系,依赖是模块化体系中很重要的一部分。而模块化的意义也不只是拆分、解耦而已,从工程实践的角度来说,模块化是实现那些超越单个工程师所能掌握的大工程的基石之一。

视图编译

关于 $compile 的使用和相应地内部机制其实文档已经很详细了。看这里。我们这里看源码的目的有两个:一是看数据改动时触发的 $digest 具体是如何更新视图的;二是看源码是否有些精妙之处可以学习。 打开 /src/ng/compile.js 511行,注意到这里定义的 $compileProvider 是 provider 的写法,不熟悉的请去看下文档。provider在用的时候会实例化,而我们在用的 $compile 函数实际上就是 this.$get 这个数组的最后一个元素(一个函数)的返回值。跳到638行看定义,源码太长,我就不贴了。后面只贴关键的地方。这个函数的返回了一个叫compile的函数:

  1. function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,
  2. previousCompileContext) {
  3. /*省略若干行预处理节点的代码*/
  4. var compositeLinkFn =
  5. compileNodes($compileNodes, transcludeFn, $compileNodes,
  6. maxPriority, ignoreDirective, previousCompileContext);
  7. safeAddClass($compileNodes, 'ng-scope');
  8.  
  9. return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){
  10. /*省略若干行和cloneConnectFn等有关的代码*/
  11. if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
  12. return $linkNode;
  13. };
  14. }

  

没有什么神奇的,返回的这个publicLinkFn就是我们用来link scope的函数。而这个函数实际上又是调用了 compileNodes 生成的 compositeLinkFn。如果你熟悉 directive 的使用,那我们不妨轻松地猜测一下这个 compileNodes 应该就是收集了节点中的各种指令然后调用相应地compile函数,并将link函数组合起来成为一个新函数,也就是这个compositeLinkFn以供调用。而 directive 里的link函数扮演了将scope的变化映射到节点上(使用 scope.$watch),将节点变化映射到scope(通常要用scope.$apply来触发scope.$digest)的角色。 我可以直接说“恭喜你,猜对了”吗?这里没什么复杂的,大家自己看下吧。值得再看看的是scope.$watch 和 scope.$digest。通常我们用 watch 来将视图更新函数注册相应地scope下,用digest来对比当前scope的属性是否有变动,如果有变化就调用注册的这些函数。我前面文章中说的angular性能不如ko等框架并且可能遇到瓶颈就是出于这个机制。我们来翻一下$digest的底:

  1. $digest: function() {
  2. /*省略若干变量定义代码*/
  3.  
  4. beginPhase('$digest');
  5.  
  6. lastDirtyWatch = null;
  7.  
  8. do { // "while dirty" loop
  9. dirty = false;
  10. current = target;
  11.  
  12. /*省略若干行异步任务代码*/
  13.  
  14. traverseScopesLoop:
  15. do { // "traverse the scopes" loop
  16. if ((watchers = current.$$watchers)) {
  17. // process our watches
  18. length = watchers.length;
  19. while (length--) {
  20. try {
  21. watch = watchers[length];
  22. // Most common watches are on primitives, in which case we can short
  23. // circuit it with === operator, only when === fails do we use .equals
  24. if (watch) {
  25. if ((value = watch.get(current)) !== (last = watch.last) &&
  26. !(watch.eq
  27. ? equals(value, last)
  28. : (typeof value == 'number' && typeof last == 'number'
  29. && isNaN(value) && isNaN(last)))) {
  30. dirty = true;
  31. lastDirtyWatch = watch;
  32. watch.last = watch.eq ? copy(value) : value;
  33. watch.fn(value, ((last === initWatchVal) ? value : last), current);
  34. /*省略若干行log代码*/
  35. } else if (watch === lastDirtyWatch) {
  36. // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
  37. // have already been tested.
  38. dirty = false;
  39. break traverseScopesLoop;
  40. }
  41. }
  42. } catch (e) {
  43. clearPhase();
  44. $exceptionHandler(e);
  45. }
  46. }
  47. }
  48.  
  49. // Insanity Warning: scope depth-first traversal
  50. // yes, this code is a bit crazy, but it works and we have tests to prove it!
  51. // this piece should be kept in sync with the traversal in $broadcast
  52. if (!(next = (current.$$childHead ||
  53. (current !== target && current.$$nextSibling)))) {
  54. while(current !== target && !(next = current.$$nextSibling)) {
  55. current = current.$parent;
  56. }
  57. }
  58. } while ((current = next));
  59.  
  60. // `break traverseScopesLoop;` takes us to here
  61.  
  62. if((dirty || asyncQueue.length) && !(ttl--)) {
  63. clearPhase();
  64. /*省略若干 throw error*/
  65. }
  66.  
  67. } while (dirty || asyncQueue.length);
  68.  
  69. clearPhase();
  70.  
  71. while(postDigestQueue.length) {
  72. try {
  73. postDigestQueue.shift()();
  74. } catch (e) {
  75. $exceptionHandler(e);
  76. }
  77. }
  78. }

  

这段代码有两个关键的loop,对应两个关键概念。大loop就是所谓的dirty check。什么是dirty?只要进入了这个循环,就是dirty的,直到值已经稳定下来。我们看到源码中用了lastDirtyWatch来作为标记,要使watch === lastDirtyWatch,至少第二次循环才能实现。这是因为在调用监听函数的时候,监听函数本身可能去修改属性,所以我们必须等到值已经完全不变了(或者超过了最大循环值)才能结束digest。另外看那个insanity warning,digest是进行深度优先遍历检测的。所以在设计复杂的directive时,要非常注意在scope哪个层级调用digest。在写简单应用的时候,dirty check和遍历子元素都没有什么问题,但是相比于基于observer的模式,最主要的缺点是它的所有监听函数都是注册在scope上的,每次digest都要检测所有的watcher是否有变化。

最后总结一下视图,angular在视图层的设计上较为完备,但同时概念也更多更复杂,在首屏渲染时速度不够快。并且内存开销是vue ko等轻框架倍数级的。但它的本身的规范和各个方面考虑的周全性确是非常值得学习,实际上也对后来者产生了极大的指导性意义。

其他

这里再记录一个实践中的问题,就是如何对数据实现getter 和setter?比如说这样一个场景:有个三个输入框,第一个让用户填姓,第二个填名,第三个自动显示“姓+空格+名”。用户也可以直接在第三个框中填,第一框和第二框会自动变化。这个时候如果有类似于ko的computed property就简单了,不然只能用$watch加中间变量去实现,代码会有点难看。有代码洁癖的话相信各位迟早会碰到这个问题,以下提供几个参考资料:

总结

总体来说,AngularJS无论在设计还是实践上都具有指导性意义。对新手来说学习曲线较陡,但如果能深入,收获是很大的。AngularJS本身在工程上也有很多其他产出,比如karma,从它中间独立出来发展成了通用测试框架。还是建议各位读者可以跟一跟AngularJS2.0的开发,必能受益。

MVVM大比拼之AngularJS源码精析的更多相关文章

  1. MVVM大比拼之knockout.js源码精析

    简介 本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档. knockout.js (以下简称 ko )是最早将 MVVM 引入到前端的重要功臣之一.目前版本已更新到 3 .相比同类主要有 ...

  2. vue.js源码精析

    MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...

  3. MVVM大比拼之avalon.js源码精析

    简介 avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是: 使用 observe 模式,性能高. 将原始对象用object.defineProperty重写,不需要用户像用knoc ...

  4. MVVM大比拼之vue.js源码精析

    VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多来自knockout.angularj ...

  5. AngularJS 源码分析1

    AngularJS简介 angularjs 是google出品的一款MVVM前端框架,包含一个精简的类jquery库,创新的开发了以指令的方式来组件化前端开发,可以去它的官网看看,请戳这里 再贴上一个 ...

  6. angularjs源码分析之:angularjs执行流程

    angularjs用了快一个月了,最难的不是代码本身,而是学会怎么用angular的思路思考问题.其中涉及到很多概念,比如:directive,controller,service,compile,l ...

  7. SpringMVC学习(一)——概念、流程图、源码简析

    学习资料:开涛的<跟我学SpringMVC.pdf> 众所周知,springMVC是比较常用的web框架,通常整合spring使用.这里抛开spring,单纯的对springMVC做一下总 ...

  8. Flink源码阅读(一)——Flink on Yarn的Per-job模式源码简析

    一.前言 个人感觉学习Flink其实最不应该错过的博文是Flink社区的博文系列,里面的文章是不会让人失望的.强烈安利:https://ververica.cn/developers-resource ...

  9. 30s源码刨析系列之函数篇

    前言 由浅入深.逐个击破 30SecondsOfCode 中函数系列所有源码片段,带你领略源码之美. 本系列是对名库 30SecondsOfCode 的深入刨析. 本篇是其中的函数篇,可以在极短的时间 ...

随机推荐

  1. MySQL数据库和InnoDB存储引擎文件

    参数文件 当MySQL示例启动时,数据库会先去读一个配置参数文件,用来寻找数据库的各种文件所在位置以及指定某些初始化参数,这些参数通常定义了某种内存结构有多大等.在默认情况下,MySQL实例会按照一定 ...

  2. iOS逆向工程之App脱壳

    本篇博客以微信为例,给微信脱壳."砸壳"在iOS逆向工程中是经常做的一件事情,,因为从AppStore直接下载安装的App是加壳的,其实就是经过加密的,这个“砸壳”的过程就是一个解 ...

  3. Mac上MySQL忘记root密码且没有权限的处理办法&workbench的一些tips (转)

    忘记Root密码肿么办 Mac上安装MySQL就不多说了,去mysql的官网上下载最新的mysql包以及workbench,先安装哪个影响都不大.如果你是第一次安装,在mysql安装完成之后,会弹出来 ...

  4. servlet 简介,待完善

    什么是Servlet?① Servlet就是JAVA 类② Servlet是一个继承HttpServlet类的类③ 这个在服务器端运行,用以处理客户端的请求 Servlet相关包的介绍--javax. ...

  5. PHP设计模式(六)原型模式(Prototype For PHP)

    原型设计模式: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象. 原型设计模式简单的来说,顾名思义, 不去创建新的对象进而保留原型的一种设计模式. 缺点:原型设计模式是的最主要的缺点就 ...

  6. Android listview和gridview以及view的区别

    GridView 可以指定显示的条目的列数. listview一般显示的条目的列数都是一列 如果是列表(单列多行形式)的使用ListView,如果是多行多列网状形式的优先使用GridView andr ...

  7. Xamarin.Android活动的生命周期

    一.前言 用过Android手机的人一定会发现一种现象,当你把一个应用置于后台后,一段时间之后在打开就会发现应用重新打开了,但是之前的相关的数据却没有丢失.可以看出app的“生命”是掌握在系统手上的, ...

  8. Xamarin.Android通知详解

    一.发送通知的机制 在日常的app应用中经常需要使用通知,因为服务.广播后台活动如果有事件需要通知用户,则需要通过通知栏显示,而在Xamarin.Android下的通知需要获取Notification ...

  9. keepalived 知识备注

    keepalived可用于配置nginx/lvs等负载均衡设备的双机热备. keepalived基于VRRP协议,简单的说就是两个物理路由节点(一主一备),虚拟成一个逻辑上的路由节点. 实际消息的路由 ...

  10. Thinking in Unity3D

    关于<Thinking in Unity3D> 笔者在研究和使用Unity3D的过程中,获得了一些Unity3D方面的信息,同时也感叹Unity3D设计之精妙.不得不说,笔者最近几年的引擎 ...