AngularJS 指令實現原理

wxlworkhard發表於2016-11-02

瀏覽器會解析渲染 HTML 元素的樣式和行為,這個能力是 Web 強大功能的基礎之一。指令本質上是 AngularJS 對 HTML 元素的擴充套件,給 HTML 元素增加自定義功能,語義化 HTML 標籤。AngularJS 編譯 DOM 時,會執行與指令關聯的 JS 程式碼,即找到指令物件,執行指令物件的相關方法。

本文的程式碼使用 AngularJS 的原始碼,為了把重心放在主要流程上而略去了很多程式碼,這裡說明一下,有需要可以直接看原始碼。

指令註冊

module 物件的 directive 方法 directive: invokeLaterAndSetModuleName('$compileProvider', 'directive')只是指令的儲存,儲存了一個['$compileProvider', 'directive', ['myDirective', function () {}]] 單元到一個陣列invokeQueue中。
指令的註冊是在應用啟動後,在執行var injector = createInjector(modules, config.strictDi);時通過呼叫loadModules這個方法,來載入所有的 module 物件(這裡就包括了 myApp 模組物件),模組載入的實質其中就包括遍歷當前模組物件上的 invokeQueue 佇列這一項,取出每一個資料元單位,然後執行對應服務的對應方法,所以這裡的指令註冊便會 link 到 $compileProvider 服務的 directive 方法(定義一個 hasDirectives 陣列儲存指令的 factory 函式)

function $CompileProvider($provide, $$sanitizeUriProvider) {
    var hasDirectives = {},
         Suffix = 'Directive';
    //......
    this.directive = function registerDirective(name, directiveFactory) {
        if (!hasDirectives.hasOwnProperty(name)) {
            hasDirectives[name] = [];
            $provide.factory(name + Suffix, ['$injector', function($injector) {
                var directives = [];
                forEach(hasDirectives[name], function(directiveFactory, index) {
                    var directive = $injector.invoke(directiveFactory);
                    if (isFunction(directive)) {
                      directive = { compile: valueFn(directive) };
                    } else if (!directive.compile && directive.link) {
                      directive.compile = valueFn(directive.link);
                    }
                    directive.priority = directive.priority || 0;
                    directive.index = index;
                    directive.name = directive.name || name;
                    directive.require = getDirectiveRequire(directive);
                    directive.restrict = getDirectiveRestrict(directive.restrict, name);
                    directive.$$moduleName = directiveFactory.$$moduleName;
                    directives.push(directive);
                });
                return directives;
            }]);
        }
        hasDirectives[name].push(directiveFactory);
        return this;
    };
    //......
}

$provider.factory會把 'myDirective' + 'Directive'註冊為服務,註冊的具體邏輯見上面程式碼,即指令的指令物件是通過註冊為服務的方式來被外界獲取的(依賴注入)。

指令編譯

先看一下 AngularJS 執行的主流程,在建立一個 injector 物件(實現依賴注入的主要物件)後例項化主要的 4 個服務後開始 DOM 編譯和啟動 $digest。

var injector = createInjector(modules, config.strictDi);
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
        function bootstrapApply(scope, element, compile, injector) {
    scope.$apply(function() {
        element.data('$injector', injector);
        compile(element)(scope);
    });
 }]);

$compile 服務是一個方法,內部首先呼叫 compileNodes 使用深度優先的方式遍歷 DOM 樹,找到 DOM 樹上的指令名,通過依賴注入獲取到指令物件例項,執行指令物件的 compile 方法,compile 會返回 link 函式,通過linkFns.push(i, nodeLinkFn, childLinkFn);把 link 函式 push 到一個 linkFns 陣列中,compileNodes函式最終返回另一個函式 compositeLinkFn,而 $compile 返回的 publicLinkFn 會呼叫 compositeLinkFn在呼叫時把 scope 已經傳遞進compositeLinkFn中,然後去迴圈執行所有的 link 函式。

function compileNodes(nodeList) {
    var linkFns = [],

    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]) : null;

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

       if (nodeLinkFn || childLinkFn) {
            linkFns.push(i, nodeLinkFn, childLinkFn);
            linkFnFound = true;
            nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
        } 
    }
    return linkFnFound ? compositeLinkFn : null;
    function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
        for (i = 0, ii = linkFns.length; i < ii;) {
            nodeLinkFn = linkFns[i++];
            if (nodeLinkFn) {
                if (nodeLinkFn.scope) {
                    childScope = scope.$new();
                } else {
                    childScope = scope;
                }
                nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
            } else if (childLinkFn) {
                childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
            }
        }
    }
}

nodeLinkFn =...賦值語句是執行指令的 compile 方法返回 link 方法,childLinkFn =... 這句是深度優先遍歷 DOM 樹,compositeLinkFn 在執行時 scope 已經定義好並傳遞進來,該方法迴圈呼叫nodeLinkFn(... 執行指令的 link 函式。

相關文章