AngularJS深入(6)——指令

2015-07-28 Alex Sun 更多博文 » 博客 » GitHub »

原文链接 https://syaning.github.io/2015/07/28/dive-into-angular-6/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


参考资料:

注:本部分源码比较多且逻辑复杂,我也没有完全通读并理解,因此分析过程中难免有不当或错误之处,还请指出。

1. 注册指令

指令的注册在$CompileProvider中,源码结构如下:

this.directive = function registerDirective(name, directiveFactory) {
    assertNotHasOwnProperty(name, 'directive');
    if (isString(name)) {
        assertValidDirectiveName(name);
        assertArg(directiveFactory, 'directiveFactory');
        if (!hasDirectives.hasOwnProperty(name)) {
            hasDirectives[name] = [];
            $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
                function($injector, $exceptionHandler) {
                    // ... ...
                }
            ]);
        }
        hasDirectives[name].push(directiveFactory);
    } else {
        forEach(name, reverseParams(registerDirective));
    }
    console.log(hasDirectives);
    return this;
};

其中,hasDirectives的结构为如下形式,即每个指令对应一个指令函数集合:

{
    directive_1: [directive_1_factory],
    directive_2: [directive_2_factory_1, directive_2_factory_2],
    // ... ...
}

整体的逻辑比较清晰,如果hasDirectives中已有相关指令的函数集合,则直接将新的指令函数加进去即可;否则的话,新建指令函数集合(hasDirectives[name] = []),并调用$provider.factory创建相关的指令Provider,然后将参数中的指令函数加到新创建的指令函数集合中。

name对类型判断,是为了支持如下两种调用方式:

app.directive('myDirective', function() { /* ... */ });

app.directive({
    myDirective1: function() { /* ... */ },
    myDirective2: function() { /* ... */ },
    // ... ...
})

2. compile

首先在函数bootstrap中,有如下代码:

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

其中最核心的一句是compile(element)(scope)compile便是$CompileProvider的一个实例,在$CompileProvider源码中,this.$get最终执行返回的便是compile函数。其源码结构如下:

function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,
    previousCompileContext) {

    // ... ...
    var compositeLinkFn = compileNodes($compileNodes, transcludeFn,
        $compileNodes, maxPriority, ignoreDirective, previousCompileContext);
    // ... ...

    return function publicLinkFn(scope, cloneConnectFn, options) {
        // ... ...
        var $linkNode;
        // ... ...

        compile.$$addScopeInfo($linkNode, scope);

        if (cloneConnectFn) cloneConnectFn($linkNode, scope);
        if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
        return $linkNode;
    };
}

其主要逻辑是:

  • compile阶段:调用compileNodes来对节点进行编译,从而得到compositeLinkFn
  • link阶段:返回函数publicLinkFn,在该函数中主要进行了scope的绑定等操作

接下来分析compileNodes函数。

3. 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();

        // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
        directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,
            ignoreDirective);

        nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement,
            null, [], [], previousCompileContext) : null;

        // ... ...

        childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
            !(childNodes = nodeList[i].childNodes) ||
            !childNodes.length) ? null : compileNodes(childNodes,
            nodeLinkFn ? (
                (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) && nodeLinkFn.transclude) : transcludeFn);

        if (nodeLinkFn || childLinkFn) {
            linkFns.push(i, nodeLinkFn, childLinkFn);
            linkFnFound = true;
            nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
        }

        //use the previous context only for the first element in the virtual group
        previousCompileContext = null;
    }

    return linkFnFound ? compositeLinkFn : null;

    function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
        // ... ...
    }
}

其主要逻辑是:

  • 对参数nodeList进行遍历,对其中的每一项执行如下操作:
    • 调用collectDirectives搜集该节点上所应用的所有指令
    • 如果没有指令,则为nodeLinkFn赋值null;否则调用applyDirectivesToNode来对节点应用指令,并将返回值赋给nodeLinkFn
    • 如果需要,对子节点调用compileNodes,并将返回值赋给childLinkFn。这是一个递归的过程
    • 如果nodeLinkFn或者childLinkFn有效,则将(i, nodeLinkFn, childLinkFn)这样的一组值加入到linkFns数组中;并设置标志linkFnFoundtrue,表示找到有link函数
  • 如果linkFnFoundtrue,则返回函数compositeLinkFn,否则返回null

接下来看compositeLinkFn的逻辑。返回的compositeLinkFn使用了闭包,主要涉及到nodeLinkFnFoundlinkFns这两个变量。源码简化如下:

function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
    var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn;
    var stableNodeList;

    // ... ...

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

        if (nodeLinkFn) {
            // ... ..

            nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn,
                nodeLinkFn);

        } else if (childLinkFn) {
            childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
        }
    }
}

在对compileNodes的分析中,可以知道数组linkFns中,每三个元素为一组值。因此在该函数的for循环中,每次取出三个值。如果nodeLinkFn不为null,则执行nodeLinkFn;如果nodeLinkFnnullchildLinkFn不为null,则执行childLinkFn

需要注意的是,nodeLinkFnapplyDirectivesToNode的返回值;而childLinkFn则为compileNodes的返回值,也就是函数compositeLinkFn。因此调用childLinkFn,其实也就是compositeLinkFn的递归调用,只不过每次传入的参数以及通过闭包所引用到的nodeLinkFnFoundlinkFns这两个值不同而已。

关于此过程,在第四篇参考资料中,作者给了一个很好的例子,并画了一幅非常详细的图,对于理解整个过程非常有帮助。

4. applyDirectivesToNode

该函数源码比较长,而且其中细节逻辑较为复杂,因此并没有完全搞清楚,仅做一个大概的分析。源码结构简化如下:

function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn,
    jqCollection, originalReplaceDirective, preLinkFns, postLinkFns,
    previousCompileContext) {

    // ... ...

    // executes all directives on the current element
    for (var i = 0, ii = directives.length; i < ii; i++) {
        // ... ...
    }

    // ... ...

    // might be normal or delayed nodeLinkFn depending on if templateUrl is present
    return nodeLinkFn;

    // ... ...

    function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn, thisLinkFn) {
        // ... ...
    }
}

可以看到,该函数主要就是某个节点的所有指令,依次应用到该节点上,最后返回函数nodeLinkFn。应用指令的过程比较繁琐,相关代码主要都在for循环中。这里主要看下指令的compile和link相关逻辑。

通过API文档,可以知道:

  • 如果定义了compile,则link无效
  • 如果compile返回的是一个函数,则作为postLink函数;如果返回的是一个对象,则其pre属性作为preLinkpost属性作为postLink

首先,在注册指令的时候,即registerDirective函数中,有如下代码段:

if (isFunction(directive)) {
    directive = {
        compile: valueFn(directive)
    };
} else if (!directive.compile && directive.link) {
    directive.compile = valueFn(directive.link);
}

即:

  • 如果定义的指令是一个函数,则将其作为compile的返回值
  • 如果没有定义compile函数但是定义了link,则将link作为compile函数的返回值

然后,在applyDirectivesToNode的循环体中,有如下代码段:

if (directive.templateUrl) {
    // ... ...
} else if (directive.compile) {
    try {
        linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
        if (isFunction(linkFn)) {
            addLinkFns(null, linkFn, attrStart, attrEnd);
        } else if (linkFn) {
            addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
        }
    } catch (e) {
        $exceptionHandler(e, startingTag($compileNode));
    }
}

其中函数addLinkFns为:

function addLinkFns(pre, post, attrStart, attrEnd) {
    if (pre) {
        // ... ...
        preLinkFns.push(pre);
    }
    if (post) {
        // ... ...
        postLinkFns.push(post);
    }
}

因此:

  • 首先执行directive.compile,并将值赋给linkFn
  • 如果linkFn是一个函数,则将其添加到postLinkFns数组中;否则将其pre属性添加到preLinkFns数组中,将其post属性添加到postLinkFns数组中

5. nodeLinkFn

nodeLinkFn是函数applyDirectivesTiNode的返回值,是一个闭包函数,其源码简化如下:

function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn, thisLinkFn) {
    // ... ...

    // PRELINKING
    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
        );
    }

    // RECURSION
    // We only pass the isolate scope, if the isolate directive has a template,
    // otherwise the child elements do not belong to the isolate directive.
    var scopeToChild = scope;
    if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
        scopeToChild = isolateScope;
    }
    childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);

    // POSTLINKING
    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

6. 案例分析

第四篇参考资料中的例子为例,假设DOM结构如下:

<A><!--has directives-->
    <B></B><!--has directives-->
    <C><!--no directives-->
        <E></E><!--has directives-->
        <F><!--no directives-->
            <G></G><!--no directives-->
        </F>
    </C>
    <D></D><!--has directives-->
</A>

其中节点A,B,E,D有指令,节点C,F,G无指令,则compile(A)的整体调用过程如下:

compile(A)
    compileNodes([A])
        collectiveDirectives(A) // A.directives = [...]
        applyDirectivesToNode(A.directives, A) // A.nodeLinkFn; A.childLinkFn = BCD.compostiteLinkFn
        compileNodes([B, C, D])
            collectiveDirectives(B) // B.directives = [...]
            applyDirectivesToNode(B.directives, B) // B.nodeLinkFn; B.childLinkFn = null
            collectiveDirectives(C) // C.directives = []
            applyDirectivesToNode(C.directives, C) // C.nodeLinkFn = null; C.childLinkFn = EF.compositeLinkFn
            compileNodes([E, F])
                collectiveDirectives(E) // E.directives = [...]
                applyDirectivesToNode(E.directives, E) // E.nodeLinkFn; E.childLinkFn = null
                collectiveDirectives(F) // F.directives = []
                applyDirectivesToNode(F.directives, F) // F.nodeLinkFn = null; F.childLinkFn = null
                compileNodes([G])
                    collectiveDirectives(G) // G.directives = []
                    applyDirectivesToNode(G.directives, G) // G.nodeLinkFn = null; G.childLinkFn = null
                    return null // G.linkFns = []
                return EF.compositeLinkFn //EF.linkFns = [0, E.nodeLinkFn, null]
            collectiveDirectives(D) // D.directives = [...]
            applyDirectivesToNode(D.directives, D) // D.nodeLinkFn; D.childLinkFn = null
            return BCD.compositeLinkFn // BCD.linkFns = [0, B.nodeLinkFn, null, 1, null, EF.compositeLinkFn, 2, D.nodeLinkFn, null]
        return A.compositeLinkFn // A.linkFns = [0, A.nodeLinkFn, BCD.compositeLinkFn]
    return publicLinkFn

compile(A)结束后,最终返回的是函数publicLinkFn,该函数有一句非常重要的代码,即:

if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);

而在这里,compositeLinkFn也就是compileNodes([A])的结果,即A.compositeLinkFn。在上面已经对函数compositeLinkFn的执行逻辑进行了分析,因此此时调用过程为:

A.compositeLinkFn()
    A.nodeLinkFn(BCD.compositeLinkFn)
        A.preLinkFns()
        BCD.compositeLinkFn()
            B.nodeLinkFn(null)
                B.preLinkFns()
                B.postLinkFns()
            EF.compositeLinkFn()
                E.nodeLinkFn(null)
                    E.preLinkFns()
                    E.postLinkFns()
            D.nodeLinkFn(null)
                D.preLinkFns()
                D.postLinkFns()
        A.postLinkFns()

这里可以看出link过程中preLinkFnspostLinkFns的执行顺序。