cordova研习笔记(二) —— cordova 6.X 源码解读(上)

前言

cordova(PhoneGap) 是一个优秀的经典的中间件框架,网上对其源代码解读的文章确实不多,本系列文章试着解读一下,以便对cordova 框架的原理理解得更深入。本文源码为cordova android版本6.1.2

源码结构

我们使用IDE的代码折叠功能先从整体上把握代码结构。

/*
* 版权申明及注释部分
*/
;(function() {
  ...
})();

;是保证导入的其它js脚本,使用工具压缩js文件时不出错。一个自执行匿名函数包裹,防止内部变量污染到外部命名空间。阅读过jQuery源码的人都知道,jQuery的也是相同的结构,只是jQuery定义的匿名函数多了两个参数window和undefined,然后调用的时候只传入window,这样,window可以在jQuery内部安全使用,而undefined也的确表示未定义(有些浏览器实现允许重定义undefined)。

继续展开代码,可以看到如下的结构:

;(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')导入库文件至全局执行环境中。

模块机制

类似于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。 

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模块系统。 

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

从内部私有函数build中,可以看出,调用工厂函数时, factory(localRequire, module.exports, module); 第一个参数localRequire实质还是调用全局的require()函数,只是把ID稍微加工了一下支持相对路径。cordova.js没有用到相对路径的require,但在一些Plugin的js中有,比如Contact.jsContactError = require('./ContactError');

这里我们写个测试用例:

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


匠心博客
看似寻常最奇崛,成如容易却艰辛。始终保持一颗匠心去铸造去创造。

看似寻常最奇崛,成如容易却艰辛。

4.6k 声望
1.5k 粉丝
0 条评论
推荐阅读
基于沙盒技术的企业移动应用安全平台设计
移动互联网的飞速发展, 改变了企业传统的业务模式, 提高了工作效率. 但同时也给企业的数据安全带来了巨大的挑战, 我们面对各种攻击的可能性会大 大增加, 面临潜在的风险:

匠心8阅读 5.4k

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木140阅读 11.9k评论 10

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 5.9k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.1k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7k评论 6

【关于Javascript】--- 正则表达式篇
基础知识一、元字符 {代码...} 二、量词 {代码...} 三、集合 字符类 {代码...} 四、分支 {代码...} 五、边界 开始结束 {代码...} 六、修饰符 {代码...} 七、贪婪模式和非贪婪模式js默认贪婪模式即最大可能的匹配...

Jerry35阅读 2.9k

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木32阅读 6k评论 9

看似寻常最奇崛,成如容易却艰辛。

4.6k 声望
1.5k 粉丝
宣传栏