AngularJS源码阅读2:编译链接过程

通过上一篇我们知道,Angular在 bootstrap 后就开始编译整个文档了,使用的就是 Angular 里十分重要的服务-$compile,指令的编译链接、双向数据绑定、各种监听等都是通过$compile来完成的。

回看上一篇你可以知道,$compile是在publishExternalAPI时挂载在ng模块下的服务,这就是$compile服务的起源。

启动compile

$compile真正派上用场就是在 bootstrap 创建完 injector之后,也就是如下代码:

injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',  
  function(scope, element, compile, injector, animate) {
    scope.$apply(function() {
      element.data('$injector', injector);
      compile(element)(scope);
    });
  }
]);

上面的代码实际作用就是初始化相关的依赖,然后开始全局编译链接。主要的代码就一行:

compile(element)(scope);  

其实这是两步:

  • compile(element):从根节点($rootElement,即包含ng-app的节点)开始,递归收集完整个页面的指令,然后返回链接函数(publicLinkFn
  • publicLinkFn(scope):传入$rootScope,执行publicLinkFn,完成整个页面的链接工作

compile(element)

先来看第一步的代码:

function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {  
  // 对于文本节点,使用<span>包装
  forEach($compileNodes, function(node, index) {
    if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/)) {
       $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
    }
  });

  // 调用compileNodes编译当前节点,返回链接函数
  var compositeLinkFn = 
        compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext);

  // 添加scope class
  compile.$$addScopeClass($compileNodes);
  var namespace = null;
  return function publicLinkFn(scope, cloneConnectFn, options) {
    // 后面再看
  };        
}

上面的代码的逻辑并不复杂,首先如果要查找的节点是文本元素,则包装一个span标签,然后执行compileNodes函数,这个方法主要是收集指令,获得链接函数:

function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) {  
  var linkFns = [],
      attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;

  // 遍历当前层的所有节点
  for (var i = 0; i < nodeList.length; i++) {
    attrs = new Attributes();

    // 收集每个节点上的所有指令
    directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective);

    // 应用指令,返回链接函数
    nodeLinkFn = (directives.length)
            ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext)
            : null;

    // 添加scope class
    if (nodeLinkFn && nodeLinkFn.scope) {
       compile.$$addScopeClass(attrs.$$element);
    }

    // 如果父亲节点没有链接函数或者已终止(terminal)
    // 或者没有孩子节点
    // 孩子节点的链接函数就为null
    // 否则递归编译孩子节点
    childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
                  !(childNodes = nodeList[i].childNodes) ||
                  !childNodes.length)
       ? null
       : compileNodes(childNodes,
           nodeLinkFn ? (
              (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
               && nodeLinkFn.transclude) : transcludeFn);

    // 将当前节点的链接函数和孩子节点的链接函数都插入到linkFns数组中
    if (nodeLinkFn || childLinkFn) {
       linkFns.push(i, nodeLinkFn, childLinkFn);
       linkFnFound = true;
       nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
    }

    previousCompileContext = null;
  }

  // 如果有链接函数返回闭包(compositeLinkFn能访问linkFns)
  return linkFnFound ? compositeLinkFn : null;

  function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
    // 代码略,后面再说
  }
}

编译节点的主要流程为两步,第一步通过collectDirectives搜集当前节点的指令,第二步调用applyDirectivesToNode来应用指令,最后调用compileNodes函数递归编译当前节点的子节点,最后把所有的函数添加到一个内部的linkFns数组中,这个数组将在最后链接的时候用到。

收集指令

Angular收集指令是根据节点类型来的,一共有三种类型:

1.element node:先根据tagName使用addDirective来添加指令,然后遍历节点的attrs使用addAttrInterpolateDirectiveaddDirective来添加指令,最后通过className使用addDirective来添加指令

2.text node:调用addTextInterpolateDirective方法来添加指令

3.comment node:匹配注释,使用addDirective添加指令

收集指令的过程主要用到了三个方法:addDirectiveaddAttrInterpolateDirectiveaddTextInterpolateDirective

首先来看addDirective方法:

function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) {  
  // 是被忽略的指令,返回null
  if (name === ignoreDirective) return null;
  var match = null;
  // hasDirectives系统在初始化的时候添加的一个内健指令对象集合,$injector
  if (hasDirectives.hasOwnProperty(name)) {
     for (var directive, directives = $injector.get(name + Suffix),
          i = 0, ii = directives.length; i < ii; i++) {
       try {
         directive = directives[i];
         if ((maxPriority === undefined || maxPriority > directive.priority) &&
              directive.restrict.indexOf(location) != -1) {
           if (startAttrName) {
             directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
           }
           // 合法指令添加到tDirectives数组中
           tDirectives.push(directive);
           match = directive;
         }
      } catch (e) { $exceptionHandler(e); }
    }
  }
  return match;
}

对于属性值要处理是否修改和监听变化,例如{{name}}就需要处理:

value="Hello, {{name}}"  

对于属性的处理,使用的就是addAttrInterpolateDirective函数了:

function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) {  
  var trustedContext = getTrustedContext(node, name);
  allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing;

  // 处理html,返回篡改函数
  var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing);

  // 没有需要监听处理的值 -> 忽略
  if (!interpolateFn) return;

  if (name === "multiple" && nodeName_(node) === "select") {
    // 绑定到多个属性的节点时,报错
  }

  directives.push({
    priority: 100,
    compile: function() {
        return {
          pre: function attrInterpolatePreLinkFn(scope, element, attr) {
            var $$observers = (attr.$$observers || (attr.$$observers = {}));

            if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
              // 不能篡改事件属性,报错
            }

            // 监听变化
            var newValue = attr[name];
            if (newValue !== value) {
              interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing);
              value = newValue;
            }

            if (!interpolateFn) return;

            // 属性值初始化。例如{{name}} -> lwl
            attr[name] = interpolateFn(scope);

            ($$observers[name] || ($$observers[name] = [])).$$inter = true;
            (attr.$$observers && attr.$$observers[name].$$scope || scope).
              $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
                // 监听变化,设置属性值
                if (name === 'class' && newValue != oldValue) {
                  attr.$updateClass(newValue, oldValue);
                } else {
                  attr.$set(name, newValue);
                }
              });
          }
        };
      }
  });
}

如果是文本节点就创建内部指令,监听scope变化然后设置节点的值

function addTextInterpolateDirective(directives, text) {  
  var interpolateFn = $interpolate(text, true);
  if (interpolateFn) {
    directives.push({
      priority: 0,
      compile: function textInterpolateCompileFn(templateNode) {
        var templateNodeParent = templateNode.parent(),
            hasCompileParent = !!templateNodeParent.length;

        if (hasCompileParent) compile.$$addBindingClass(templateNodeParent);

        return function textInterpolateLinkFn(scope, node) {
          var parent = node.parent();
          if (!hasCompileParent) compile.$$addBindingClass(parent);
          compile.$$addBindingInfo(parent, interpolateFn.expressions);
          scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
            node[0].nodeValue = value;
          });
        };
      }
    });
  }
}

像如下的文本节点就会自动添加上面的指令,自动添加一个监听,通过原生方法来修改节点的值

<body>  
    {{ feenan }}
</body>  

应用指令到当前节点

收集完所有指令之后,接下来就要调用applyDirectivesToNode方法来将指令应用到当前节点上了,这个方法将会生成用于链接阶段的link函数。

这个函数比较复杂,我们拆开来看,其主体如下:

// 遍历当前节点的每个指令
for (var i = 0, ii = directives.length; i < ii; i++) {  
  // 1.判断scope类型
  // 2.判断是否需要有controller
  // 3.transclude的处理
  // 4.template的处理
  // 5.templateurl的处理
  // 6.compile处理
  // 7.terminal处理
}
// return 收集到的信息-nodeLinkFn

下面逐步来看看编译的完整过程,首先说明一下:当前节点$compileNode-即要编译的节点

1.判断scope类型

if (directiveValue = directive.scope) {  
  // 跳过需要异步处理的指令,模板加载完成之后再处理
  if (!directive.templateUrl) {
    // scope属性为对象,例如{},则需要创建独立作用域
    if (isObject(directiveValue)) {
      newIsolateScopeDirective = directive;
    }
  }
  newScopeDirective = newScopeDirective || directive;
}

2.判断是否需要controller

// 同样,跳过要异步处理的指令,模板加载完成之后再处理
if (!directive.templateUrl && directive.controller) {  
  directiveValue = directive.controller;
  // 收集当前节点上所用要创建的controller
  controllerDirectives = controllerDirectives || createMap();
  controllerDirectives[directiveName] = directive;
}

3.transclude的处理

if (directiveValue = directive.transclude) {  
  hasTranscludeDirective = true;
  // element
  if (directiveValue == 'element') {
    hasElementTranscludeDirective = true;
    terminalPriority = directive.priority;
    $template = $compileNode;
    // 删除当前节点,替换为注释
    $compileNode = templateAttrs.$$element =
        jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
    compileNode = $compileNode[0];
    replaceWith(jqCollection, sliceArgs($template), compileNode);

    // 编译当前节点
    childTranscludeFn = compile($template, transcludeFn, terminalPriority,  replaceDirective && replaceDirective.name, { nonTlbTranscludeDirective: nonTlbTranscludeDirective});
  } else { // true
    // 复制当前节点内容
    $template = jqLite(jqLiteClone(compileNode)).contents();
    // 清空当前节点
    $compileNode.empty();
    // 编译复制的内容
    childTranscludeFn = compile($template, transcludeFn);
  }
}

从上面的代码可以看出,transclude的取值有两种:element和true,两者的区别是值为element时会将当前节点也处理了,而true时只处理当前节点的子节点。

4.template的处理

if (directive.template) {  
  hasTemplate = true;
  templateDirective = directive;

  // 如果template为函数,则函数返回值为模板内容
  // 否则就是字符串,那么字符串就是模板内容
  directiveValue = (isFunction(directive.template))
      ? directive.template($compileNode, templateAttrs)
      : directive.template;

  directiveValue = denormalizeTemplate(directiveValue);

  // 如果replace为true
  if (directive.replace) {
    replaceDirective = directive;
    if (jqLiteIsTextNode(directiveValue)) {
      $template = [];
    } else {
      $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
    }
    // 模板的第一个节点
    compileNode = $template[0];

    if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
      // 没有要编译的内容或不是有效节点,报错
    }

    // 1.模板的第一个节点(compileNode)替换当前节点($compileNode)
    replaceWith(jqCollection, $compileNode, compileNode);

    var newTemplateAttrs = {$attr: {}};

    // 2.收集模板的第一个节点(compileNode)的所有指令
    var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
    // 3.当前节点($compileNode)剩余未编译的指令
    var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));

    if (newIsolateScopeDirective) {
      markDirectivesAsIsolate(templateDirectives);
    }
    // 4.$compileNode与compileNode的指令合并
    directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
    // 5.将$compileNode与compileNode的属性合并
    mergeTemplateAttributes(templateAttrs, newTemplateAttrs);

    ii = directives.length;
  } else {
    // replace为false,直接将模板内容插入当前节点即可
    $compileNode.html(directiveValue);
  }
}

5.templateurl的处理

if (directive.templateUrl) {  
  hasTemplate = true;
  templateDirective = directive;

  if (directive.replace) {
    replaceDirective = directive;
  }

  // 和template的处理类似,只是在获取到模板之前,这些节点编译挂起
  // 等获取到模板内容之后再继续编译
  nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
      templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
        controllerDirectives: controllerDirectives,
        newIsolateScopeDirective: newIsolateScopeDirective,
        templateDirective: templateDirective,
        nonTlbTranscludeDirective: nonTlbTranscludeDirective
      });
  ii = directives.length;
}

6.compile处理

// 同样,跳过需要异步处理的指令
if (!directive.templateUrl && directive.compile) {  
  try {
    // 使用指令的compile函数编译指令
    linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);

    // 如果返回的是函数,则该函数为post-link函数
    // 否则返回为对象,pre和post属性分别对应指令的pre-link和post-link函数
    if (isFunction(linkFn)) {
      addLinkFns(null, linkFn, attrStart, attrEnd);
    } else if (linkFn) {
      addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
    }
  } catch (e) {
    $exceptionHandler(e, startingTag($compileNode));
  }
}

7.terminal处理

// 有终止属性的指令,优先级小于当前指令的不会被编译
if (directive.terminal) {  
  nodeLinkFn.terminal = true;
  terminalPriority = Math.max(terminalPriority, directive.priority);
}

编译阶段收集了足够的信息,最后返回nodeLinkFn函数及其属性,在链接过程中就可以使用了:

nodeLinkFn = {  
  scope = newScopeDirective && newScopeDirective.scope === true
  transcludeOnThisElement = hasTranscludeDirective
  elementTranscludeOnThisElement = hasElementTranscludeDirective
  templateOnThisElement = hasTemplate
  transclude = childTranscludeFn
}

publicLinkFn(scope)

这里传进来的scope$rootScope,这个方法主要是执行所有的链接函数,创建scope,添加监听函数:

function publicLinkFn(scope, cloneConnectFn, options) {  
  // 略去一大推
  if (cloneConnectFn) cloneConnectFn($linkNode, scope);
  if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
  return $linkNode;
};

这个函数实际调用的是compositeLinkFn函数,而compositeLinkFn函数是compileNodes函数的返回值,实际上它返回了一个闭包。compositeLinkFn函数操作的是linkFnslinkFns包含两部分:当前节点的链接函数nodeLinkFn和子节点的链接函数childLinkFn,而childLinkFn本身也是一个compositeLinkFn(在子节点上递归调用compileNodes的返回结果),所以实际的链接过程就是递归调用nodelinkFn函数。

先来简单地看看外层compositeLinkFn做了什么:

for (i = 0, ii = linkFns.length; i < ii;) {  
  node = stableNodeList[linkFns[i++]];
  nodeLinkFn = linkFns[i++];
  childLinkFn = linkFns[i++];

  if (nodeLinkFn) {
    if (nodeLinkFn.scope) {
       // 新建子作用域
       childScope = scope.$new();   
    } else {
      // 使用父级作用域
      childScope = scope;
    }

    // 包装一下TranscludeFn
    if (nodeLinkFn.transcludeOnThisElement) {
      // transclude: element
      childBoundTranscludeFn = createBoundTranscludeFn(
          scope, nodeLinkFn.transclude, parentBoundTranscludeFn,
          nodeLinkFn.elementTranscludeOnThisElement);
    } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
      childBoundTranscludeFn = parentBoundTranscludeFn;
    } else if (!parentBoundTranscludeFn && transcludeFn) {
      // transclude: true
      childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn);
    } else {
      // 没有transclude属性
      childBoundTranscludeFn = null;
    }

    // 实际链接处理就是nodeLinkFn了
    nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn, nodeLinkFn);
  } else if (childLinkFn) {
     childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn)
  }
}

nodeLinkFn登场,看看链接都做了什么,要注意上面关于scope的处理,非独立作用域是在上面处理的,而独立作用域是在nodeLinkFn中处理的。

如果设置了transclude属性,都会调用createBoundTranscludeFn方法,这个方法里会创建一个transcludedScope。

// 1.创建独立scope
if (newIsolateScopeDirective) {  
   isolateScope = scope.$new(true);
}
// 2.创建控制器
if (controllerDirectives) {  
   elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);

  for (i in elementControllers) {
    controller = elementControllers[i];
    var controllerResult = controller();
    // 略
  }
}

// 3.pre-link
for (i = 0, ii = preLinkFns.length; i < ii; i++) {  
  linkFn = preLinkFns[i];
  invokeLinkFn(linkFn,
    linkFn.isolateScope ? isolateScope : scope,
    $element,
    attrs,
    linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
    transcludeFn
  );
}

// 4.递归执行子节点的链接函数
var scopeToChild = scope;  
if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {  
   scopeToChild = isolateScope;
}
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);

// 5.post-link
for (i = postLinkFns.length - 1; i >= 0; i--) {  
  linkFn = postLinkFns[i];
  invokeLinkFn(linkFn,
      linkFn.isolateScope ? isolateScope : scope,
      $element,
      attrs,
      linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
      transcludeFn
  );
}

执行流程为preLinkFns -> childLinkFn -> postLinkFns,这些信息都已经在第一步编译的时候收集好了,链接的时候使用即可。所以pre-link的执行顺序和文档节点顺序相同,而post-link是在递归回溯的过程中执行的,因此正好和文档节点顺序相反,后面会对编译连接过程给出一个小例子。

这里补充说一下如何获取节点上的所有控制器,看代码:

function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {  
  var elementControllers = createMap();
  for (var controllerKey in controllerDirectives) {
    var directive = controllerDirectives[controllerKey];
    var locals = {
      $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
      $element: $element,
      $attrs: attrs,
      $transclude: transcludeFn
    };

    var controller = directive.controller;
    // 特殊处理ng-controller
    if (controller == '@') {
      controller = attrs[directive.name];
    }

    // 生成控制器实例,实际是返回已经注入依赖的工厂函数
    var controllerInstance = $controller(controller, locals, true, directive.controllerAs);

    // 含有 transclude 指令的元素是注释
    // jQuery不支持在注释节点设置data
    // 因此,暂时将controller设置在local hash中
    // 当 transclude 完成后,生成真正的节点之后,再将controller设置到data中
    elementControllers[directive.name] = controllerInstance;
    if (!hasElementTranscludeDirective) {
      $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
    }
  }
  return elementControllers;
}

例子

html结构如下

<body ng-app>  
      <A a1>
            <B b1 b2></B>
            <C>
                  <E e1></E>
                  <F>
                        <G></G>
                  </F>
            </C>
            <D d1></D>
      </A>
</body>  

js代码

var app = angular.module('myApp', []);  
var names = ['a1', 'b1', 'b2', 'e1', 'd1'];

names.forEach(function (name) {  
  app.directive(name, function () {
    return {
      compile: function () {
        console.log(name + ' compile');
        return {
          pre: function () {
            console.log(name + ' preLink');
          },
          post: function () {
            console.log(name + ' postLink');
          }
       };
     }
    };
  });
});

控制台的输出:

// console log
a1 compile  
b1 compile  
b2 compile  
e1 compile  
d1 compile  
a1 preLink  
b1 preLink  
b2 preLink  
b2 postLink  
b1 postLink  
e1 preLink  
e1 postLink  
d1 preLink  
d1 postLink  
a1 postLink  

可以看出:

  1. 所有的指令都是先compile,然后preLink,然后postLink。
  2. 节点指令的preLink是在所有子节点指令preLink,postLink之前,所以一般这里就可以通过scope给子节点传递一定的信息。
  3. 节点指令的postLink是在所有子节点指令preLink,postLink完毕之后,也就意味着,当父节点指令执行postLink时,子节点postLink已经都完成了,此时子dom树已经稳定,所以我们大部分dom操作,访问子节点都在这个阶段。
  4. 指令在link的过程,其实是一个深度优先遍历的过程,postLink的执行其实是一个回溯的过程。
  5. 节点上的可能有若干指令,在搜集的时候就会按一定顺序排列(通过Priority排序),执行的时候,preLinks是正序执行,而postLinks则是倒序执行。

来张图表示: 编译链接实例

总结

编译链接过程,可以用下面的图简单的表示:

编译链接过程

参考

  1. Directive Compilation in AngularJS – step-by-step
  2. AngularJS 源码分析4:$compile
  3. 彻底弄懂AngularJS中的transclusion
Author image
关于 superlin
Beijing, CN 主页
The reason why a great man is great is that he resolves to be a great man.
默认颜色 边栏居左 边栏居右