Javascript依賴注入學習筆記

來源:互聯網
上載者:User

注意:不要將依賴注入和設計模式裡的依賴倒置原則(Dependency Inversion Principle)混淆。

目標

設想我們有兩個模組。第一個是負責Ajax請求服務(service),第二個是路由(router)。

var service = function() { 
    return { name: 'Service' };
}
var router = function() { 
    return { name: 'Router' };
}
//我們有另一個函數需要用到這兩個模組。
var doSomething = function(other) { 
    var s = service();
    var r = router();
};

為使看起來更有趣,這函數接受一個參數。當然,我們完全可以使用上面的代碼,但這顯然不夠靈活。如果我們想使用ServiceXML或ServiceJSON呢,或者如果我們需要一些測試模組呢。我們不能僅靠編輯函數體來解決問題。首先,我們可以通過函數的參數來解決依賴性。即:

var doSomething = function(service, router, other) { 
    var s = service();
    var r = router();
};

我們通過傳遞額外的參數來實現我們想要的功能,然而,這會帶來新的問題。想象如果我們的doSomething方法散落在我們的代碼中。如果我們需要更改依賴條件,我們不可能更改所有調用函數的檔案。

我們需要一個能幫我們搞定這些的工具。這就是依賴注入嘗試解決的問題。讓我們寫下一些我們的依賴注入解決辦法應該達到的目標:

我們應該能夠註冊依賴關係
注入應該接受一個函數,並返回一個我們需要的函數
我們不能寫太多東西——我們需要精簡漂亮的文法
注入應該保持被傳遞函數的範圍
被傳遞的函數應該能夠接受自訂參數,而不僅僅是依賴描述
堪稱完美的清單,下面 讓我們實現它。
RequireJS / AMD的方法
你可能對RequireJS早有耳聞,它是解決依賴注入不錯的選擇。

define(['service', 'router'], function(service, router) { 
    // ...
});

這種想法是先描述需要的依賴,然後再寫你的函數。這裡參數的順序很重要。如上所說,讓我們寫一個叫做injector的模組,能接受相同的文法。

var doSomething = injector.resolve(['service', 'router'], function(service, router, other) { 
    console.log(service().name); //Service
    console.log(router().name);  //Router
    console.log(other().name);   //Other
});
doSomething(other); 

下面開始我們的injector模組,這是非常棒的一個單例模式,所以它能在我們程式的不同部分工作的很好。

var injector = { 
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {

    }
}

這是一個非常簡單的對象,有兩個方法,一個用來儲存的屬性。我們要做的是檢查deps數組並在dependencies變數中搜尋答案。剩下的只是調用apply方法並傳遞之前的func方法的參數。

resolve: function(deps, func, scope) { 
    var args = [];
    for(var i=0; i<deps.length, d=deps[i]; i++) {
        if(this.dependencies[d]) {
            args.push(this.dependencies[d]);
        } else {
            throw new Error('Can\'t resolve ' + d);
        }
    }
    return function() {
        func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
    }       
}

scope是可選的,Array.prototype.slice.call(arguments, 0)是必須的,用來將arguments變數轉換為真正的數組。到目前為止還不錯。我們的測試通過了。這種實現的問題是,我們需要寫所需組件兩次,並且我們不能混淆他們的順序。附加的自訂參數總是位於依賴之後。

反射方法
根據維基百科的定義反射是指一個程式在運行時檢查和修改一個對象的結構和行為的能力。簡單的說,在JavaScript的上下文裡,這具體指讀取和分析的對象或函數的原始碼。讓我們完成文章開頭提到的doSomething函數。如果你在控制台輸出doSomething.tostring()的日誌。你將得到如下的字串:

"function (service, router, other) {
    var s = service();
    var r = router();
}"

通過此方法返回的字串給我們遍曆參數的能力,更重要的是,能夠擷取他們的名字。這其實是Angular 實現它的依賴注入的方法。我偷了一點懶,直接截取Angular代碼中擷取參數的Regex。

/^function\s*[^\(]*\(\s*([^\)]*)\)/m

我們可以像下面這樣修改resolve 的代碼:

resolve: function() { 
    var func, deps, scope, args = [], self = this;
    func = arguments[0];
    deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
    scope = arguments[1] || {};
    return function() {
        var a = Array.prototype.slice.call(arguments, 0);
        for(var i=0; i<deps.length; i++) {
            var d = deps[i];
            args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
        }
        func.apply(scope || {}, args);
    }       
}

我們執行Regex的結果如下:

["function (service, router, other)", "service, router, other"]

看起來,我們只需要第二項。一旦我們清楚空格並分割字串就得到deps數組。只有一個大的改變:

var a = Array.prototype.slice.call(arguments, 0); 
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift()); 

我們迴圈遍曆dependencies數組,如果發現缺失項則嘗試從arguments對象中擷取。謝天謝地,當數組為空白時,shift方法只是返回undefined,而不是拋出一個錯誤(這得益於web的思想)。新版的injector能像下面這樣使用:

var doSomething = injector.resolve(function(service, other, router) { 
    console.log(service().name); //Service
    console.log(router().name);  //Router
    console.log(other().name);   //Other
});
doSomething(other); 

不必重寫依賴並且他們的順序可以打亂。它仍然有效,我們成功複製了Angular的魔法。

然而,這種做法並不完美,這就是反射類型注射一個非常大的問題。壓縮會破壞我們的邏輯,因為它改變參數的名字,我們將無法保持正確的映射關係。例如,doSometing()壓縮後可能看起來像這樣:

var doSomething=function(e,t,n){var r=e();var i=t()} 
Angular團隊提出的解決方案看起來像:

var doSomething = injector.resolve(['service', 'router', function(service, router) {

}]);

這看起來很像我們開始時的解決方案。我沒能找到一個更好的解決方案,所以決定結合這兩種方法。下面是injector的最終版本。

var injector = { 
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function() {
        var func, deps, scope, args = [], self = this;
        if(typeof arguments[0] === 'string') {
            func = arguments[1];
            deps = arguments[0].replace(/ /g, '').split(',');
            scope = arguments[2] || {};
        } else {
            func = arguments[0];
            deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
            scope = arguments[1] || {};
        }
        return function() {
            var a = Array.prototype.slice.call(arguments, 0);
            for(var i=0; i<deps.length; i++) {
                var d = deps[i];
                args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
            }
            func.apply(scope || {}, args);
        }       
    }
}

resolve訪客接受兩或三個參數,如果有兩個參數它實際上和文章前面寫的一樣。然而,如果有三個參數,它會將第一個參數轉換並填充deps數組,下面是一個測試例子:

var doSomething = injector.resolve('router,,service', function(a, b, c) { 
    console.log(a().name); //Service
    console.log(c().name); //Router
    console.log(b().name); //Other
});
doSomething(other); 

你可能注意到在第一個參數後面有兩個逗號——注意這不是筆誤。空值實際上代表other參數(預留位置)。這顯示了我們是如何控制參數順序的。

直接注入Scope
有時我會用到第三個注入變數,它涉及到操作函數的範圍(換句話說,就是this對象)。所以,很多時候不需要使用這個變數。

var injector = { 
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {
        var args = [];
        scope = scope || {};
        for(var i=0; i<deps.length, d=deps[i]; i++) {
            if(this.dependencies[d]) {
                scope[d] = this.dependencies[d];
            } else {
                throw new Error('Can\'t resolve ' + d);
            }
        }
        return function() {
            func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));
        }       
    }
}

我們所做的一切其實就是將依賴添加到範圍。這樣做的好處是,開發人員不用再寫依賴性參數;它們已經是函數範圍的一部分。

var doSomething = injector.resolve(['service', 'router'], function(other) { 
    console.log(this.service().name); //Service
    console.log(this.router().name);  //Router
    console.log(other().name);        //Other
});
doSomething(other); 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.