cordova研習筆記(二) —— cordova 6.X 源碼解讀(上)

前言

cordova(PhoneGap) 是一個優秀的經典的中間件框架,網上對其源代碼解讀的文章確實很少,本系列文章試着解讀一下,以便對cordova 框架的原理理解得更深刻。本文源碼爲cordova android版本6.1.2javascript

源碼結構

咱們使用IDE的代碼摺疊功能先從總體上把握代碼結構。html

/*
* 版權申明及註釋部分
*/
;(function() {
  ...
})();

;是保證導入的其它js腳本,使用工具壓縮js文件時不出錯。一個自執行匿名函數包裹,防止內部變量污染到外部命名空間。閱讀過jQuery源碼的人都知道,jQuery的也是相同的結構,只是jQuery定義的匿名函數多了兩個參數window和undefined,而後調用的時候只傳入window,這樣,window能夠在jQuery內部安全使用,而undefined也的確表示未定義(有些瀏覽器實現容許重定義undefined)。java

繼續展開代碼,能夠看到以下的結構:node

;(function() {
var PLATFORM_VERSION_BUILD_LABEL = '6.1.2';

// 模塊化系統
/* ------------------------------------------------------------- */
var require, // 加載使用module
    define;  // 定義註冊module

// require|define 的邏輯
(function () {
  ...
})();

// Export for use in node
if (typeof module === "object" && typeof require === "function") {
    module.exports.require = require;
    module.exports.define = define;
}
/* ------------------------------------------------------------- */

// 事件的處理和回調,外部訪問cordova.js的入口
define("cordova", function(require, exports, module) { ... }

// JS->Native的具體交互形式
define("cordova/android/nativeapiprovider", function(require, exports, module) { ... }

// 經過prompt()和Native交互
define("cordova/android/promptbasednativeapi", function(require, exports, module)  { ... }

// 用於plugin中校驗參數,好比argscheck.checkArgs('fFO', 'Camera.getPicture', arguments); 參數應該是2個函數1個對象
define("cordova/argscheck", function(require, exports, module) { ... }

// JS->Native交互時對ArrayBuffer進行uint8ToBase64(WebSockets二進制流)
define("cordova/base64", function(require, exports, module) { ... }

// 對象屬性操做,好比把一個對象的屬性Merge到另一個對象
define("cordova/builder", function(require, exports, module) { ... }

// 事件通道
define("cordova/channel", function(require, exports, module) { ... }

// 執行JS->Native交互
define("cordova/exec", function(require, exports, module) { ... }

// 用於Plugin中往已經有的模塊上添加方法
define("cordova/exec/proxy", function(require, exports, module) { ... }

// 初始化處理
define("cordova/init", function(require, exports, module) { ... }
define("cordova/init_b", function(require, exports, module) { ... }

// 把定義的模塊clobber到一個對象,在初始化的時候會賦給window
define("cordova/modulemapper", function(require, exports, module) { ... }
define("cordova/modulemapper_b", function(require, exports, module) { ... }

// 平臺啓動處理
define("cordova/platform", function(require, exports, module) { ... }

// 清緩存、loadUrl、退出程序等
define("cordova/plugin/android/app", function(require, exports, module) { ... }

// 載全部cordova_plugins.js中定義的模塊,執行完成後會觸發
define("cordova/pluginloader", function(require, exports, module) { ... }
define("cordova/pluginloader_b", function(require, exports, module) { ... }

// 獲取絕對URL,InAppBrowser中會用到
define("cordova/urlutil", function(require, exports, module) { ... }

// 工具類
define("cordova/utils", function(require, exports, module) { ... }

// 全部模塊註冊完以後,導入cordova至全局環境中
window.cordova = require('cordova');

// 初始化啓動
require('cordova/init');

})();

從上能夠清晰的看出,在cordova內部,首先是定義了兩個公共的require和define函數,而後是使用define註冊全部模塊,再經過window.cordova=require('cordova')導入庫文件至全局執行環境中。android

模塊機制

相似於Java的package/import,在JavaScript中也有相似的define/require,它用來異步加載module化的js,從而提升運行效率。模塊化加載的必要性,起源於nodejs的出現。可是JavaScript並無內置模塊系統,因此就出現了不少規範。 主要有2種:CommonJSAMD(Asynchronous Module Definition)。還有國內興起的CMD(Common Module Definition) 。CommonJS主要面對的是服務器,表明是Node.js;AMD針對瀏覽器進行了優化,主要實現require.js;CMD是seajs。 git

cordova-js最開始採用的是require.js做者寫的almond.js(兼容AMD和CommonJS),但以後因爲特殊需求(好比模塊不存在的時候要throw異常),最終從almond.js fork過來實現了一個簡易CommonJS風格的模塊系統,同時提供了和nodejs之間很好的交互。在cordova.js中能夠直接使用define()和require(),在其餘文件能夠經過cordova.define()和cordova.require()來調用。因此src/scripts/require.js中定義的就是一個精簡的JavaScript模塊系統。 es6

// cordova.js內部使用的全局函數require/define
var require,
    define;

(function () {
    // 初始化一個空對象,緩存全部的模塊
    var modules = {},
    // 正在build中的模塊ID的棧
        requireStack = [],
    // 標示正在build中模塊ID的Map
        inProgressModules = {},
        SEPARATOR = ".";

    // 模塊build
    function build(module) {
        // 備份工廠方法
        var factory = module.factory,
        // 對require對象進行特殊處理 
            localRequire = function (id) {
                var resultantId = id;
                //Its a relative path, so lop off the last portion and add the id (minus "./")
                if (id.charAt(0) === ".") {
                    resultantId = module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + SEPARATOR + id.slice(2);
                }
                return require(resultantId);
            };
        // 給模塊定義一個空的exports對象,防止工廠類方法中的空引用
        module.exports = {};
        // 刪除工廠方法
        delete module.factory;
        // 調用備份的工廠方法(參數必須是require,exports,module)  
        factory(localRequire, module.exports, module);
        // 返回工廠方法中實現的module.exports對象
        return module.exports;
    }
    
    // 加載使用模塊
    require = function (id) {
        // 若是模塊不存在拋出異常 
        if (!modules[id]) {
            throw "module " + id + " not found";
        // 若是模塊正在build中拋出異常
        } else if (id in inProgressModules) {
            var cycle = requireStack.slice(inProgressModules[id]).join('->') + '->' + id;
            throw "Cycle in require graph: " + cycle;
        }
        // 若是模塊存在工廠方法說明還未進行build(require嵌套)
        if (modules[id].factory) {
            try {
                // 標示該模塊正在build
                inProgressModules[id] = requireStack.length;
                // 將該模塊壓入請求棧
                requireStack.push(id);
                // 模塊build,成功後返回module.exports
                return build(modules[id]);
            } finally {
                // build完成後刪除當前請求
                delete inProgressModules[id];
                requireStack.pop();
            }
        }
        // build完的模塊直接返回module.exports  
        return modules[id].exports;
    };
    
    // 定義註冊模塊
    define = function (id, factory) {
        // 若是已經存在拋出異常
        if (modules[id]) {
            throw "module " + id + " already defined";
        }
        // 模塊以ID爲索引包含ID和工廠方法
        modules[id] = {
            id: id,
            factory: factory
        };
    };
    
    // 移除模塊
    define.remove = function (id) {
        delete modules[id];
    };
    
    // 返回全部模塊
    define.moduleMap = modules;
})();

首先在外部cordova環境中定義require和define兩個變量,用來存儲實現導入功能的函數和實現註冊功能的函數。而後用一個當即調用的匿名函數來實例化這兩個變量,在這個匿名函數內部,緩存了全部的功能模塊。註冊模塊時,若是已經註冊了,就直接拋出異常,防止無心中重定義,如確實須要重定義,可先調用define.remove。github

從內部私有函數build中,能夠看出,調用工廠函數時, factory(localRequire, module.exports, module); 第一個參數localRequire實質仍是調用全局的require()函數,只是把ID稍微加工了一下支持相對路徑。cordova.js沒有用到相對路徑的require,但在一些Plugin的js中有,好比Contact.jsContactError = require('./ContactError'); api

這裏咱們寫個測試用例:跨域

<script src="module.js"></script>
<script type="text/javascript">
    define('plugin.first', function (require, exports, module) {
        module.exports = {
            name: 'first plugin',
            show: function () {
                console.log("call "+this.name);
            }
        }
    });

    define('plugin.second', function (require, exports, module) {
        var first = require("plugin.first");
        first.show();
    });

    require("plugin.second"); 
    // call first plugin
</script>

注:module.js爲上述cordova的模塊代碼。

上面例子中咱們定義了兩個模塊,這裏是寫在同一個頁面下,在實際中咱們天然但願寫在兩個不一樣的文件中,而後按需加載。咱們上一篇文章中說明了cordova的插件使用方法,咱們會發現cordova_plugins.js中定義了cordova插件的id、路徑等變量,而且該文件定義了一個id爲cordova/plugin_list的模塊,咱們在cordova.js中能夠看到有這個模塊的引用。

定義了require和define並賦值後,是將cordova全部模塊一一註冊,例如:

define("cordova",function(require,exports,module){
  // 工廠函數內部實現代碼
});

這裏須要注意的是,define只是註冊模塊,不會調用其factory。factory函數在這個時候並無實際執行,而只是定義,並做爲一個參數傳遞給define函數。全部模塊註冊完以後,經過:

window.cordova = require('cordova');

導入至全局環境。

由於是註冊後第一次導入,因此在執行require('cordova')時,modules['cordova'].factory的值是註冊時的工廠函數,轉變爲boolean值時爲true,從而在這裏會經過build調用這個工廠函數,並將這個工廠函數從註冊緩存裏面刪除,接下來的就是去執行cordova的這個factory函數了。

事件通道

做爲觀察者模式(Observer)的一種變形,不少MV*框架(好比:Vue.js、Backbone.js)中都提供發佈/訂閱模型來對代碼進行解耦。cordova.js中也提供了一個自定義的pub-sub模型,基於該模型提供了一些事件通道,用來控制通道中的事件何時以什麼樣的順序被調用,以及各個事件通道的調用。

src/common/channel.js的代碼結構也是一個很經典的定義結構(構造函數、實例、修改函數原型共享實例方法),它提供事件通道上事件的訂閱(subscribe)、撤消訂閱(unsubscribe)、調用(fire)。pub-sub模型用於定義和控制對cordova初始化的事件的觸發以及此後的自定義事件。

頁面加載和Cordova啓動期間的事件順序以下:

  • onDOMContentLoaded ——(內部事件通道)頁面加載後DOM解析完成
  • onNativeReady ——(內部事件通道)Cordova的native準備完成
  • onCordovaReady ——(內部事件通道)全部Cordova的JavaScript對象被建立完成能夠開始加載插件
  • onDeviceReady —— Cordova所有準備完成
  • onResume —— 應用從新返回前臺
  • onPause —— 應用暫停退到後臺

能夠經過下面的事件進行監聽:

document.addEventListener("deviceready", myDeviceReadyListener, false);
document.addEventListener("resume", myResumeListener, false);
document.addEventListener("pause", myPauseListener, false);

DOM生命週期事件應用於保存和恢復狀態:

  • window.onload
  • window.onunload
define("cordova/channel", function(require, exports, module) {

    var utils = require('cordova/utils'),
        nextGuid = 1;

    // 事件通道的構造函數
    var Channel = function(type, sticky) {
        // 通道名稱
        this.type = type;
        // 通道上的全部事件處理函數Map(索引爲guid)
        this.handlers = {};
        // 通道的狀態(0:非sticky, 1:sticky但未調用, 2:sticky已調用)  
        this.state = sticky ? 1 : 0;
        // 對於sticky事件通道備份傳給fire()的參數 
        this.fireArgs = null;
        // 當前通道上的事件處理函數的個數
        this.numHandlers = 0;
        // 訂閱第一個事件或者取消訂閱最後一個事件時調用自定義的處理
        this.onHasSubscribersChange = null;
    },
    // 事件通道外部接口
    channel = {
        // 把指定的函數h訂閱到c的各個通道上,保證h在每一個通道的最後被執行  
        join: function(h, c) {
            var len = c.length,
                i = len,
                f = function() {
                    if (!(--i)) h();
                };
            for (var j=0; j<len; j++) {
                if (c[j].state === 0) {
                    throw Error('Can only use join with sticky channels.');
                }
                c[j].subscribe(f);
            }
            if (!len) h();
        },
        // 建立事件通道
        create: function(type) {
            return channel[type] = new Channel(type, false);
        },
        // 建立sticky事件通道
        createSticky: function(type) {
            return channel[type] = new Channel(type, true);
        },
    
        // 保存deviceready事件以前要調用的事件
        deviceReadyChannelsArray: [],
        deviceReadyChannelsMap: {},
    
        // 設置deviceready事件以前必需要完成的事件
        waitForInitialization: function(feature) {
            if (feature) {
                var c = channel[feature] || this.createSticky(feature);
                this.deviceReadyChannelsMap[feature] = c;
                this.deviceReadyChannelsArray.push(c);
            }
        },
    
        // 初始化代碼已經完成
        initializationComplete: function(feature) {
            var c = this.deviceReadyChannelsMap[feature];
            if (c) {
                c.fire();
            }
        }
    };

    // 校驗事件處理函數
    function checkSubscriptionArgument(argument) {
        if (typeof argument !== "function" && typeof argument.handleEvent !== "function") {
            throw new Error(
                "Must provide a function or an EventListener object " +
                "implementing the handleEvent interface."
            );
        }
    }

    /**
     * 向事件通道訂閱事件處理函數(subscribe部分)  
     * f:事件處理函數 c:事件的上下文
     */
    Channel.prototype.subscribe = function(eventListenerOrFunction, eventListener) {
        // 校驗事件處理函數
        checkSubscriptionArgument(eventListenerOrFunction);
        
        var handleEvent, guid;
    
        if (eventListenerOrFunction && typeof eventListenerOrFunction === "object") {
            // 接收到一個實現handleEvent接口的EventListener對象
            handleEvent = eventListenerOrFunction.handleEvent;
            eventListener = eventListenerOrFunction;
        } else {
            // 接收處處理事件的回調函數
            handleEvent = eventListenerOrFunction;
        }
        
        // 若是是被訂閱過的sticky事件,就直接調用
        if (this.state == 2) {
            handleEvent.apply(eventListener || this, this.fireArgs);
            return;
        }
    
        guid = eventListenerOrFunction.observer_guid;
        // 若是事件有上下文,要先把事件函數包裝一下帶上上下文
        if (typeof eventListener === "object") {
            handleEvent = utils.close(eventListener, handleEvent);
        }
        
        // 自增加的ID
        if (!guid) {
            guid = '' + nextGuid++;
        }
        // 把自增加的ID反向設置給函數,之後撤消訂閱或內部查找用
        handleEvent.observer_guid = guid;
        eventListenerOrFunction.observer_guid = guid;
    
        // 判斷該guid索引的事件處理函數是否存在(保證訂閱一次)
        if (!this.handlers[guid]) {
            // 訂閱到該通道上(索引爲guid)
            this.handlers[guid] = handleEvent;
            // 通道上的事件處理函數的個數增1 
            this.numHandlers++;
            if (this.numHandlers == 1) {
                // 訂閱第一個事件時調用自定義的處理(好比:第一次按下返回按鈕提示「再按一次...」) 
                this.onHasSubscribersChange && this.onHasSubscribersChange();
            }
        }
    };

    /**
     * 撤消訂閱通道上的某個函數(guid)
     */
    Channel.prototype.unsubscribe = function(eventListenerOrFunction) {
         // 事件處理函數校驗
        checkSubscriptionArgument(eventListenerOrFunction);

        var handleEvent, guid, handler;
    
        if (eventListenerOrFunction && typeof eventListenerOrFunction === "object") {
            // 接收到一個實現handleEvent接口的EventListener對象
            handleEvent = eventListenerOrFunction.handleEvent;
        } else {
            // 接收處處理事件的回調函數
            handleEvent = eventListenerOrFunction;
        }

        // 事件處理函數的guid索引
        guid = handleEvent.observer_guid;
        // 事件處理函數
        handler = this.handlers[guid];
        if (handler) {
            // 從該通道上撤消訂閱(索引爲guid)
            delete this.handlers[guid];
            // 通道上的事件處理函數的個數減1
            this.numHandlers--;
            if (this.numHandlers === 0) {
                // 撤消訂閱最後一個事件時調用自定義的處理
                this.onHasSubscribersChange && this.onHasSubscribersChange();
            }
        }
    };

    /**
     * 調用全部被髮布到該通道上的函數
     */
    Channel.prototype.fire = function(e) {
        var fail = false,
            fireArgs = Array.prototype.slice.call(arguments);

        // sticky事件被調用時,標示爲已經調用過
        if (this.state == 1) {
            this.state = 2;
            this.fireArgs = fireArgs;
        }
        if (this.numHandlers) {
            // 把該通道上的全部事件處理函數拿出來放到一個數組中
            var toCall = [];
            for (var item in this.handlers) {
                toCall.push(this.handlers[item]);
            }
            // 依次調用通道上的全部事件處理函數
            for (var i = 0; i < toCall.length; ++i) {
                toCall[i].apply(this, fireArgs);
            }
            // sticky事件是一次性所有被調用的,調用完成後就清空
            if (this.state == 2 && this.numHandlers) {
                this.numHandlers = 0;
                this.handlers = {};
                this.onHasSubscribersChange && this.onHasSubscribersChange();
            }
        }
    };

    /**
     * 建立事件通道(publish部分)
     */
    //(內部事件通道)頁面加載後DOM解析完成
    channel.createSticky('onDOMContentLoaded');
    
    //(內部事件通道)Cordova的native準備完成
    channel.createSticky('onNativeReady');
    
    //(內部事件通道)全部Cordova的JavaScript對象被建立完成能夠開始加載插件  
    channel.createSticky('onCordovaReady');
    
    //(內部事件通道)全部自動load的插件js已經被加載完成(待刪除)
    channel.createSticky('onPluginsReady');
    
    // Cordova所有準備完成
    channel.createSticky('onDeviceReady');
    
    // 應用從新返回前臺
    channel.create('onResume');
    
    // 應用暫停退到後臺
    channel.create('onPause');
    
    // 設置deviceready事件以前必需要完成的事件
    channel.waitForInitialization('onCordovaReady');
    channel.waitForInitialization('onDOMContentLoaded');
    
    module.exports = channel;
});

咱們能夠寫一個測試用例:

<script src="channel.js"></script>
<script type="text/javascript">
    var test = channel.create('onTest');
    // 訂閱事件(此處test = channel.onTest)
    test.subscribe(function () {
        console.log('test fire');
    });
    // 觸發事件(此處test = channel.onTest)
    test.fire();
</script>

可是不少時候咱們但願可以傳遞參數,經過閱讀上面的源碼能夠得知:

if (eventListenerOrFunction && typeof eventListenerOrFunction === "object") {
    // 接收到一個實現handleEvent接口的EventListener對象
    handleEvent = eventListenerOrFunction.handleEvent;
   eventListener = eventListenerOrFunction;
} else {
   // 接收處處理事件的回調函數
   handleEvent = eventListenerOrFunction;
}

咱們上面的例子中咱們傳遞的是一個方法,這裏咱們也能夠傳遞一個EventListener對象。

// 建立事件通道
channel.create('onTest');

// 訂閱事件
channel.onTest.subscribe(function (event) {
  console.log(event);
  console.log(event.data.name+' fire');
});

// 建立 Event 對象
var event = document.createEvent('Events');
// 初始化事件
event.initEvent('onTest', false, false);
// 綁定數據
event.data = {name: 'test'};
// 觸發事件
channel.onTest.fire(event);

工具模塊

咱們在寫插件的時候若是熟悉cordova自帶的工具函數,能夠更加方便的拓展本身的插件。

define("cordova/utils", function(require, exports, module) {
    var utils = exports;
    // 定義對象屬性(或方法)的setter/getter
    utils.defineGetterSetter = function(obj, key, getFunc, opt_setFunc) {...}
    // 定義對象屬性(或方法)的getter
    utils.defineGetter = utils.defineGetterSetter;
    // Array IndexOf 方法
    utils.arrayIndexOf = function(a, item) {...}
    // Array remove 方法
    utils.arrayRemove = function(a, item) {...}
    // 類型判斷
    utils.typeName = function(val) {...}
    // 數組判斷
    utils.isArray = Array.isArray ||
        function(a) {return utils.typeName(a) == 'Array';};
    // Date判斷
    utils.isDate = function(d) {...}
    // 深度拷貝
    utils.clone = function(obj) {...}
    // 函數包裝調用
    utils.close = function(context, func, params) {...}
    // 內部私有函數,產生隨機數
    function UUIDcreatePart(length) {...}
    // 建立 UUID (通用惟一識別碼)
    utils.createUUID = function() {...}
    // 繼承
    utils.extend = function() {...}
    // 調試
    utils.alert = function(msg) {...}
});
  • UUIDcreatePart函數用來隨機產生一個16進制的號碼,接受一個表示號碼長度的參數(其實是最終號碼長度的一半),通常用途是作爲元素的惟一ID。
  • utils.isArray 在這裏不使用instanceof來判斷是否是Array類型,主要是考慮到跨域或者多個frame的狀況,多個frame時每一個frame都會有本身的Array構造函數,從而得出不正確的結論。使用'[object Array]'來判斷是根據ECMA標準中的返回值來進行的,事實上,這裏不須要類型轉換,而能夠用全等「===」來判斷。
  • utils.close函數,封裝函數的調用,將執行環境做爲一個參數,調用的函數爲第二個參數,調用函數自己的參數爲後續參數。

原型繼承實現詳解

utils.extend = (function() {
    // proxy used to establish prototype chain
    var F = function() {};
    // extend Child from Parent
    return function(Child, Parent) {
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.__super__ = Parent.prototype;
        Child.prototype.constructor = Child;
    };
}());

這裏的繼承是經過原型鏈的方式實現,咱們能夠經過下述方式調用:

var Parent = function () {
    this.name = 'Parent';
}
Parent.prototype.getName = function () {
    return this.name;
}
var Child = function () {
    this.name = 'Child';
}

utils.extend(Child, Parent);

var child = new Child();
console.log(child.getName())

ES5中有一個Object.create方法,咱們可使用這個函數實現繼承:

// 建立一個新的對象
Object.create = Object.create || function (proto) {
    var F = function () {};
    F.prototype = proto;
    return new F();
}

// 實現繼承
var extend = function(Child, Parent) {
    // 拷貝Parent原型對象
    Child.prototype = Object.create(Parent.prototype);
    // 將Child構造函數賦值給Child的原型對象
    Child.prototype.constructor = Child;
}

// 實例
var Parent = function () {
    this.name = 'Parent';
}
Parent.prototype.getName = function () {
    return this.name;
}
var Child = function () {
    this.name = 'Child';
}
extend(Child, Parent);
var child = new Child();
console.log(child.getName())

原型鏈的概念對於初學者而言可能有點繞,可是咱們把握構造函數實例化對象原型對象三者的關係就很簡單了。咱們以此爲例說明:

// 構造函數
var Child = function () {
    this.name = 'Child';
}
// 原型對象Child.prototype
Child.prototype.getName = function () {
    return this.name;
}
// 實例化對象
var child = new Child();
  • 原型對象是構造函數的prototype屬性,是全部實例化對象共享屬性和方法的原型對象。
  • 實例化對象經過new構造函數獲得,都繼承了原型對象的屬性和方法。
  • 如何訪(qiu)問(jie)原型對象?若已知構造函數Child,則能夠經過Child.prototype獲得;若已知實例化對象child,則能夠經過child.__proto__或者Object.getPrototypeOf(child)獲得,也經過Object.setPrototypeOf方法來重寫對象的原型。
Child.prototype === child.__proto__  // true
child.__proto__ === Object.getPrototypeOf(child) // true
  • 原型對象中有個隱式的constructor,指向了構造函數自己,也就是咱們能夠經過Child.prototype.constructor(雖然看似畫蛇添足,可是常常須要從新設置構造函數)或child.__proto__.constructor或者Object.getPrototypeOf(child).constructor獲得構造函數。
  • instanceof和Object.isPrototypeOf()能夠判斷兩個對象是不是繼承關係
child instanceof Child  // true
Child.prototype.isPrototypeOf(child) // true

至此構造函數實例化對象原型對象三者的關係咱們已經很清除了,再回過頭看看上面繼承的實現就很簡單了。

咱們能夠經過instanceof來檢驗是否知足繼承關係:

child instanceof Child && child instanceof Parent  // true

其實上述繼承的思路很簡單:
1.首先得到父類原型對象的方法,這裏的F對象做爲中間變量引用拷貝Parent.prototype對象(即和Parent.prototype共享同一內存空間);例如咱們修改上述的Object.create爲:

Object.create = function (proto) {
    var F = function () {};
    F.prototype = proto;
    F.prototype.setName = function(name){
        this.name = name;
    }
    return new F();
}

此時Parent.prototype、Child.prototype、child都擁有的setName方法,可是咱們應當避免這樣作,這也是爲何咱們不直接經過Child.prototype = Parent.prototype得到;經過實例化中間對象F間接獲得Parent.prototype的方法,此時經過Object.create方法得到的對象和Parent.prototype再也不是共享內存空間。Child經過extend(Child, Parent)從Parent.prototype對象得到一份新的拷貝。實質是由於咱們經過new一個構造函數得到的實例化對象是得到了一個新的內存空間,子對象互不影響;
2.對子類進行修正,咱們經過拷貝得到了父類的一個備份,此時子類原型對象下的constructor屬性依然是父類的構造函數,顯然不符合咱們的要求,咱們須要重置,同時有時候咱們但願保留對父類的引用,如cordova這裏用一個__super__屬性保存。

Child.__super__ = Parent.prototype;
Child.prototype.constructor = Child;

其實繼承的本質咱們是但願能實現如下功能:

  • 父類有的我都有,我也能重載,但不至於影響到父類的屬性和方法
  • 除了繼承以外,我也能添加本身的方法和屬性

咱們能夠利用es6新特性實現一樣的效果:

class Parent {
    constructor () {
        this.name = 'Parent';
    }
    getName () {
        return this.name;
    }
}

class Child extends Parent {
    constructor () {
        super();
        this.name = 'Child';
    }
}

var child = new Child();
console.log(child.getName())

super關鍵字在這裏表示父類的構造函數,用來新建父類的this對象。在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。

cordova 模塊

本文最後一部分咱們來看看cordova模塊,cordova模塊是事件的處理和回調,外部訪問cordova.js的入口。

define("cordova", function(require, exports, module) {
  if (window.cordova && !(window.cordova instanceof HTMLElement)) {
    throw new Error("cordova already defined");
  }
  // 導入事件通道模塊
  var channel = require('cordova/channel');
  // 導入平臺模塊
  var platform = require('cordova/platform');
  // 保存addEventListener、removeEventListener的原生實現
  var m_document_addEventListener = document.addEventListener;
  var m_document_removeEventListener = document.removeEventListener;
  var m_window_addEventListener = window.addEventListener;
  var m_window_removeEventListener = window.removeEventListener;

  // 緩存全部的事件處理函數
  var documentEventHandlers = {},
      windowEventHandlers = {};
  
  // 從新定義addEventListener、removeEventListener,方便後面註冊添加pause、resume、deviceReady等事件
  document.addEventListener = function(evt, handler, capture) {...}
  window.addEventListener = function(evt, handler, capture) {...}
  document.removeEventListener = function(evt, handler, capture) {...}
  window.removeEventListener = function(evt, handler, capture) {...}
  function createEvent(type, data) {...}

  var cordova = {
    define: define,
    require: require,
    version: PLATFORM_VERSION_BUILD_LABEL,
    platformVersion: PLATFORM_VERSION_BUILD_LABEL,
    platformId: platform.id,
    addWindowEventHandler: function(event) {...},
    addStickyDocumentEventHandler: function(event) {...},
    addDocumentEventHandler: function(event) {...},
    removeWindowEventHandler: function(event) {...},
    removeDocumentEventHandler: function(event) {...},
    getOriginalHandlers: function() {...},
    fireDocumentEvent: function(type, data, bNoDetach) {...},
    fireWindowEvent: function(type, data) {...},
    callbackId: Math.floor(Math.random() * 2000000000),
    callbacks: {},
    callbackStatus: {},
    callbackSuccess: function(callbackId, args) {...},
    callbackError: function(callbackId, args) {...},
    callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {...},
    addConstructor: function(func) {...}
  }
  // 暴露cordova對象給外部
  module.exports = cordova;
});

這裏咱們以document Event爲例說明一下cordova模塊中關於事件的處理:

// 保存addEventListener、removeEventListener的原生實現
var m_document_addEventListener = document.addEventListener;
var m_document_removeEventListener = document.removeEventListener;
// 緩存事件處理函數
var documentEventHandlers = {};
// 從新定義addEventListener
document.addEventListener = function(evt, handler, capture) {
    var e = evt.toLowerCase();
    if (typeof documentEventHandlers[e] != 'undefined') {
        documentEventHandlers[e].subscribe(handler);
    } else {
        m_document_addEventListener.call(document, evt, handler, capture);
    }
};
// 從新定義removeEventListener
document.removeEventListener = function(evt, handler, capture) {
    var e = evt.toLowerCase();
    // If unsubscribing from an event that is handled by a plugin
    if (typeof documentEventHandlers[e] != "undefined") {
        documentEventHandlers[e].unsubscribe(handler);
    } else {
        m_document_removeEventListener.call(document, evt, handler, capture);
    }
};
// 建立 Event 對象
function createEvent(type, data) {
    var event = document.createEvent('Events');
    event.initEvent(type, false, false);
    if (data) {
        for (var i in data) {
            if (data.hasOwnProperty(i)) {
                event[i] = data[i];
            }
        }
    }
    return event;
}
var codova = {
    ...
    // 建立事件通道
    addStickyDocumentEventHandler:function(event) {
        return (documentEventHandlers[event] = channel.createSticky(event));
    },
    addDocumentEventHandler:function(event) {
        return (documentEventHandlers[event] = channel.create(event));
    },
    // 取消事件通道
    removeDocumentEventHandler:function(event) {
        delete documentEventHandlers[event];
    },
    // 發佈事件消息
    fireDocumentEvent: function(type, data, bNoDetach) {
        var evt = createEvent(type, data);
        if (typeof documentEventHandlers[type] != 'undefined') {
            if( bNoDetach ) {
                documentEventHandlers[type].fire(evt);
            }
            else {
                setTimeout(function() {
                    // Fire deviceready on listeners that were registered before cordova.js was loaded.
                    if (type == 'deviceready') {
                        document.dispatchEvent(evt);
                    }
                    documentEventHandlers[type].fire(evt);
                }, 0);
            }
        } else {
            document.dispatchEvent(evt);
        }
    },
    ...
}
module.exports = cordova;

在初始化啓動模塊cordova/init中有這樣的代碼:

// 註冊pause、resume、deviceReady事件
channel.onPause = cordova.addDocumentEventHandler('pause');
channel.onResume = cordova.addDocumentEventHandler('resume');
channel.onActivated = cordova.addDocumentEventHandler('activated');
channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');

// 監聽DOMContentLoaded事件併發布事件消息
if (document.readyState == 'complete' || document.readyState == 'interactive') {
    channel.onDOMContentLoaded.fire();
} else {
    document.addEventListener('DOMContentLoaded', function() {
        channel.onDOMContentLoaded.fire();
    }, false);
}

// 原生層加載完成事件
if (window._nativeReady) {
    channel.onNativeReady.fire();
}

// 加載完成發佈時間事件消息
channel.join(function() {
    modulemapper.mapModules(window);
    platform.initialize && platform.initialize();
    channel.onCordovaReady.fire();
    channel.join(function() {
        require('cordova').fireDocumentEvent('deviceready');
    }, channel.deviceReadyChannelsArray);
}, platformInitChannelsArray);

這裏經過addDocumentEventHandler及addStickyDocumentEventHandler建立了事件通道,並經過fireDocumentEvent或者fire發佈事件消息,這樣咱們就能夠經過document.addEventListener訂閱監聽事件了。

若是咱們要建立一個自定義事件Test,咱們能夠這樣作:

// 建立事件通道
cordova.addWindowEventHandler('Test');

// 發佈事件消息
cordova.fireWindowEvent('Test', {
    name: 'test',
    data: {
        time: new Date()
    }
})

// 訂閱事件消息
window.addEventListener('Test', function (evt) {
     console.log(evt);
});

參考

Cordova 3.x 入門 - 目錄
PhoneGap源碼分析

寫在後面

本文至此已經說完了cordova的模塊機制和事件機制,已經cordova的工具模塊,瞭解這些後寫起插件來才能駕輕就熟,對於原理實現部分不屬於本文的範疇,下一篇會詳細講解cordova原理實現。敬請關注,不過近來在寫畢設,估計一時半會兒也不會寫完,本文前先後後已經是拖了半個月。若是以爲本文對您有幫助,不妨打賞支持以此鼓勵。

clipboard.png

轉載需標註本文原始地址:https://zhaomenghuan.github.io/

相關文章
相關標籤/搜索