在上篇文章我们简单实现了一个 jQuery 的基础结构,不过为了顺应潮流,这次咱把它改为模块化的写法,此举得以有效提升项目的可维护性,因此在后续也将以模块化形式进行持续开发。

模块化开发和编译需要用上 ES6 和 rollup,具体原因和使用方法请参照我之前的《冗余代码都走开——前端模块打包利器 Rollup.js 入门》一文。

本期代码均挂在我的github上,有需要的童鞋自行下载。

1. 基本配置

为了让 rollup 得以静态解析模块,从而减少可能存在的冗余代码,我们得用上 ES6 的解构赋值语法,因此得配合 babel 辅助开发。

在目录下我们新建一个 babel 配置“.babelrc”:

{"presets": ["es2015-rollup"]
}

以及 rollup 配置“rollup.comfig.js”:

var rollup = require( 'rollup' );
var babel = require('rollup-plugin-babel');rollup.rollup({entry: 'src/jquery.js',plugins: [ babel() ]
}).then( function ( bundle ) {bundle.write({format: 'umd',moduleName: 'jQuery',dest: 'rel/jquery.js'});
});

其中入口文件为“src/jquery.js”,并将以 umd 模式输出到 rel 文件夹下。

别忘了确保已安装了三大套:

npm i babel-preset-es2015-rollup rollup rollup-plugin-babel

后续咱们直接执行:

node rollup.config.js

即可实现打包。

2. 模块拆分

从模块功能性入手,我们暂时先简单地把上次的整个 IIFE 代码段拆分为:

src/jquery.js  //出口模块
src/core.js  //jQuery核心模块
src/global.js  //全局变量处理模块
src/init.js  //初始化模块

它们的内容分别如下:

jquery.js:

import jQuery from './core';
import global from './global';
import init from './init';global(jQuery);
init(jQuery);export default jQuery;

core.js:

var version = "0.0.1",jQuery = function (selector, context) {return new jQuery.fn.init(selector, context);};jQuery.fn = jQuery.prototype = {jquery: version,constructor: jQuery,setBackground: function(){this[0].style.background = 'yellow';return this},setColor: function(){this[0].style.color = 'blue';return this}
};export default jQuery;

init.js:

var init = function(jQuery){jQuery.fn.init = function (selector, context, root) {if (!selector) {return this;} else {var elem = document.querySelector(selector);if (elem) {this[0] = elem;this.length = 1;}return this;}};jQuery.fn.init.prototype = jQuery.fn;
};export default init;

global.js:

var global = function(jQuery){//走模块化形式的直接绕过if(typeof module === 'object' && typeof module.exports !== 'undefined') return;var _jQuery = window.jQuery,_$ = window.$;jQuery.noConflict = function( deep ) {//确保window.$没有再次被改写if ( window.$ === jQuery ) {window.$ = _$;}//确保window.jQuery没有再次被改写if ( deep && window.jQuery === jQuery ) {window.jQuery = _jQuery;}return jQuery;  //返回 jQuery 接口引用
    };window.jQuery = window.$ = jQuery;
};export default global;

留意在 global.js 中我们先加了一层判断,如果使用者走的模块化形式,那是无须考虑全局变量冲突处理的,直接绕过该模块即可。

执行打包后效果如下(rel/jquery.js)

(function (global, factory) {typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :typeof define === 'function' && define.amd ? define(factory) :(global.jQuery = factory());
}(this, function () { 'use strict';/*** Created by vajoy on 2016/8/1.*/var version = "0.0.1";var jQuery = function jQuery(selector, context) {return new jQuery.fn.init(selector, context);};jQuery.fn = jQuery.prototype = {jquery: version,constructor: jQuery,setBackground: function setBackground() {this[0].style.background = 'yellow';return this;},setColor: function setColor() {this[0].style.color = 'blue';return this;}};var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {return typeof obj;} : function (obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;};/*** Created by vajoy on 2016/8/2.*/var global$1 = function global(jQuery) {//走模块化形式的直接绕过if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object' && typeof module !== 'undefined') return;var _jQuery = window.jQuery,_$ = window.$;jQuery.noConflict = function (deep) {//确保window.$没有再次被改写if (window.$ === jQuery) {window.$ = _$;}//确保window.jQuery没有再次被改写if (deep && window.jQuery === jQuery) {window.jQuery = _jQuery;}return jQuery; //返回 jQuery 接口引用
      };window.jQuery = window.$ = jQuery;};/*** Created by vajoy on 2016/8/1.*/var init = function init(jQuery) {jQuery.fn.init = function (selector, context, root) {if (!selector) {return this;} else {var elem = document.querySelector(selector);if (elem) {this[0] = elem;this.length = 1;}return this;}};jQuery.fn.init.prototype = jQuery.fn;};global$1(jQuery);init(jQuery);return jQuery;}));

View Code

3. extend 完善

如上章所说,我们可以通过 $.extend / $.fn.extend 接口来扩展 JQ 的静态方法/实例方法,也可以简单地实现对象的合并和深/浅拷贝。这是非常重要且实用的功能,在这里我们得完善它。

core.js 中我们新增如下代码段:

jQuery.extend = jQuery.fn.extend = function() {var options, target = arguments[ 0 ] || {},  //target为要被合并的目标对象i = 1,length = arguments.length,deep = false; //默认为浅拷贝// 若第一个参数为Boolean,表示其为决定是否要深拷贝的参数if ( typeof target === "boolean" ) {deep = target;// 那么 target 参数就得往后挪一位了target = arguments[ i ] || {};i++;}// 若 target 类型不是对象的处理if ( typeof target !== "object" && typeof target !== "function" ) {target = {};}// 若 target 后没有其它参数(要被拷贝的对象)了,则直接扩展jQuery自身(把target合并入jQuery)if ( i === length ) {target = this;i--;  //减1是为了方便取原target(它反过来变成被拷贝的源对象了)
    }for ( ; i < length; i++ ) {// 只处理源对象值不为 null/undefined 的情况if ( ( options = arguments[ i ] ) != null ) {// TODO - 完善Extend
        }}// 返回修改后的目标对象return target;
};

该段代码可以判断如下写法并做对应处理:

$.extend( targetObj, copyObj1[, copyObj2...] )
$.extend( true, targetObj, copyObj1[, copyObj2...]  )
$.extend( copyObj )
$.extend( true, copyObj )

其它情况会被绕过(返回空对象)

我们继续完善内部的遍历:

    var isObject = function(obj){return Object.prototype.toString.call(obj) === "[object Object]"};var isArray = function(obj){return Object.prototype.toString.call(obj) === "[object Array]"};for ( ; i < length; i++ ) { //遍历被拷贝的源对象// 只处理源对象值不为 null/undefined 的情况if ( ( options = arguments[ i ] ) != null ) {var name, clone, copy;// 遍历源对象属性for ( name in options ) {src = target[ name ];copy = options[ name ];// 避免自己合自己,导致无限循环if ( target === copy ) {continue;}// 深拷贝,且确保被拷贝属性值为对象/数组if ( deep && copy && ( isObject( copy ) ||( copyIsArray = isArray( copy ) ) ) ) {//被拷贝属性值为数组if ( copyIsArray ) {copyIsArray = false;//若被合并属性不是数组,则设为[]clone = src && isArray( src ) ? src : [];} else {  //被拷贝属性值为对象//若被合并属性不是数组,则设为{}clone = src && isObject( src ) ? src : {};}// 右侧递归直到最内层属性值非对象,再把返回值赋给 target 对应属性target[ name ] = jQuery.extend( deep, clone, copy );// 非对象/数组,或者浅拷贝情况(注意排除 undefined 类型)} else if ( copy !== undefined ) {target[ name ] = copy;}}}}// 返回被修改后的目标对象return target;

这里需要留意的有,我们会通过

jQuery.extend( deep, clone, copy )

来递归生成被合并的 target 属性值,这是为了避免扩展后的 target 属性和被扩展的 copyObj 属性引用了同一个对象,导致互相影响。

通过 extend 递归解剖 copyObj 源对象的属性直到最内层,最内层属性的值(上方代码里的 copy)大致有这么两种情况:

1. copy 为空对象/空数组:

    for ( ; i < length; i++ ) { //遍历被拷贝对象// 只处理源对象值不为 null/undefined 的情况if ( ( options = arguments[ i ] ) != null ) {//空数组/空对象没有可枚举的元素/属性,这里会忽略
        }}// 返回被修改后的目标对象return target;    //直接返回空数组/空对象

2. copy 为非对象(如“vajoy”):

                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||( copyIsArray = jQuery.isArray( copy ) ) ) ) {//不会执行这里
} else if ( copy !== undefined ) {// 执行这里target[ name ] = copy;}}}}// 返回如 ['vajoy'] 或者 {'name' : 'vajoy'}return target;

从而确保 target 所扩展的每一层属性都跟 copyObj 的是互不关联的。

P.S. jQuery 里的深拷贝实现其实比较简单,如果希望能做到更全面的兼容,可以参考 lodash 中的实现。

4. 建立基础工具模块

在上方的 extend 代码块中其实存在两个不合理的地方:

1. 仅通过 Object.toString.call(obj) === "[object Object]" 作为对象判断条件在我们扩展对象的逻辑中有些片面,适合扩展的对象应当是“纯粹/简单”(plain)的 js Object 对象,但在某些浏览器中,像 document 在 Object.toSting 调用时也会返回和 Object 相同结果;
2. 像 Object.hasOwnProperty 和 Object.prototype.toString.call 等方法在我们后续开发中会经常使用上,如果能把它们写到一个模块中封装起来复用就更好了。

关于 plainObject 的概念可以点这里了解。

基于上述两点,我们新增一个 var.js 来封装这些常用的输出:

export var class2type = {};  //在core.js中会被赋予各类型属性值

export const toString = class2type.toString; //等同于 Object.prototype.toString

export const getProto = Object.getPrototypeOf;export const hasOwn = class2type.hasOwnProperty;export const fnToString = hasOwn.toString; //等同于 Object.toString/Function.toString

export const ObjectFunctionString = fnToString.call( Object ); //顶层Object构造函数字符串"function Object() { [native code] }",用于判断 plainObj

然后在 core.js 导入所需接口即可:

import { class2type, toString, getProto, hasOwn, fnToString, ObjectFunctionString } from './var.js';

我们进一步修改 extend 接口代码为:

jQuery.extend = jQuery.fn.extend = function() {var options, name, src, copy, copyIsArray, clone,target = arguments[ 0 ] || {},i = 1,length = arguments.length,deep = false;if ( typeof target === "boolean" ) {deep = target;target = arguments[ i ] || {};i++;}if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {  //修改点1target = {};}if ( i === length ) {target = this;i--;}for ( ; i < length; i++ ) {if ( ( options = arguments[ i ] ) != null ) {for ( name in options ) {src = target[ name ];copy = options[ name ];if ( target === copy ) {continue;}// Recurse if we're merging plain objects or arraysif ( deep && copy && ( jQuery.isPlainObject( copy ) ||  //修改点2( copyIsArray = jQuery.isArray( copy ) ) ) ) {if ( copyIsArray ) {copyIsArray = false;clone = src && jQuery.isArray( src ) ? src : [];  //修改点3
} else {clone = src && jQuery.isPlainObject( src ) ? src : {};}target[ name ] = jQuery.extend( deep, clone, copy );} else if ( copy !== undefined ) {target[ name ] = copy;}}}}return target;
};//新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用,后面会用上
"Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){class2type[ "[object " + name + "]" ] = name.toLowerCase();
});//新增修改点2
jQuery.extend( {isArray: Array.isArray,isPlainObject: function( obj ) {var proto, Ctor;// 明显的非对象判断,直接返回falseif ( !obj || toString.call( obj ) !== "[object Object]" ) {return false;}proto = getProto( obj );  //获取 prototype// 通过 Object.create( null ) 形式创建的 {} 是没有prototype的if ( !proto ) {return true;}// 简单对象的构造函数等于最顶层 Object 构造函数Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;},isFunction: function( obj ) {return jQuery.type( obj ) === "function";},//获取类型(如'function')type: function( obj ) {  if ( obj == null ) {return obj + ""; //'undefined' 或 'null'
        }return typeof obj === "object" || typeof obj === "function" ?class2type[ toString.call( obj ) ] || "object" :typeof obj;}});

这里我们新增了isArray、isPlainObject、isFunction、type 四个 jQuery 静态方法,其中 isPlainObject 比较有趣,为了过滤某些浏览器中的 document 等特殊类型,会对 obj.prototype 及其构造函数进行判断:

1. 通过Object.create( null ) 形式创建的 {} ,或者实例对象都是没有 prototype 的,直接返回 true;
2. 判断其构造函数合法性(存在且等于原生的对象构造器 function Object(){ [native code] })

关于第二点,实际是直接判断两个构造器字符串化后是否相同:

Function.toString.call(constructor) === Function.toString.call(Object)

另外,需要留意的是,通过这段代码:

//新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用,后面会用上
"Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){class2type[ "[object " + name + "]" ] = name.toLowerCase();
});

class2type 对象是变成了这样的:

{
"[object Boolean]":"boolean",
"[object Number]":"number",
"[object String]":"string",
"[object Function]":"function",
"[object Array]":"array",
"[object Date]":"date",
"[object RegExp]":"regexp",
"[object Object]":"object",
"[object Error]":"error",
"[object Symbol]":"symbol"
}

所以后续只需要通过

class2type[ Object.prototype.toString(obj) ]

就能获取 obj 的类型名称。isFunction 接口便是利用这种钩子模式判断传入参数是否函数类型的:

    isFunction: function( obj ) {return jQuery.type( obj ) === "function";}

最后。我们执行打包处理:

node rollup.config.js

在 HTML 页面运行下述代码:

    var $div = $('div');$div.setBackground().setColor();var arr = [1, 2, 3];console.log($.type(arr))

效果如下:

留意 $.type 静态方法是我们上方通过 jQuery.extend 扩展进去的:

//新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用
"Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){class2type[ "[object " + name + "]" ] = name.toLowerCase();
});jQuery.extend( {type: function( obj ) {  if ( obj == null ) {return obj + ""; //'undefined' 或 'null'
        }return typeof obj === "object" || typeof obj === "function" ?//兼容安卓2.3- 函数表达式类型不正确情况class2type[ toString.call( obj ) ] || "object" :typeof obj;}});

它返回传入参数的类型(小写)。该方法在我们下一章也会直接在模块中使用到。

本章先这样吧,得感谢这台风天赏赐了一天的假期,才有了时间写文章,共勉~

从零开始,DIY一个jQuery(2)相关推荐

  1. jQuery:从零开始,DIY一个jQuery(2)

    在上篇文章我们简单实现了一个 jQuery 的基础结构,不过为了顺应潮流,这次咱把它改为模块化的写法,此举得以有效提升项目的可维护性,因此在后续也将以模块化形式进行持续开发. 模块化开发和编译需要用上 ...

  2. jQuery:从零开始,DIY一个jQuery(1)

    从本篇开始会陪大家一起从零开始走一遍 jQuery 的奇妙旅途,在整个系列的实践中,我们会把 jQuery 的主要功能模块都了解和实现一遍. 这会是一段很长的历程,但也会很有意思 -- 作为前端领域的 ...

  3. 从零开始构建一个的asp.net Core 项目(一)

    最近突发奇想,想从零开始构建一个Core的MVC项目,于是开始了构建过程. 首先我们添加一个空的CORE下的MVC项目,创建完成之后我们运行一下(Ctrl +F5).我们会在页面上看到"He ...

  4. 从零开始创建一个Android主屏幕Widget

    登录 / 注册 IT168首页 手机 整机 DIY硬件 摄影 消费数码 数字家电 企业IT 企业商用 办公 互动 社区 全部频道 IT168技术开发频道 IT168首页 > 技术开发 >  ...

  5. 动手DIY一个underscorejs库及underscorejs源码分析1

    Underscore 是一个 JavaScript 工具库,它提供一整套函数编程的实用功能.他弥补了 jQuery 没有实现的功能,同时又是Backbone 必不可少的部分. underscore.j ...

  6. 从零开始搭建一个vue项目 -- vue-cli/cooking-cli(一)

    从零开始搭建一个vue项目 -- vue-cli/cooking-cli(一) 1.vue-cli搭建一个可靠成熟的项目 1.介绍 vue-cli 我是去年六月份接触的vue1.0,当时还是个菜逼,当 ...

  7. 从零开始写一个武侠冒险游戏-3-地图生成

    2019独角兽企业重金招聘Python工程师标准>>> 从零开始写一个武侠冒险游戏-3-地图生成 概述 前面两章我们设计了角色的状态, 绘制出了角色, 并且赋予角色动作, 现在是时候 ...

  8. DIY一个粒子检测器 2020-10

    ➤ 01粒子检测器 在 TLE207x低噪声高速JFET-输入运算放大器的噪声特性测量 中实现了TLE207x的低噪声放大器.放置在金属盒屏蔽盒内进行信号放大. 基于上面的实验电路, DIY一个粒子检 ...

  9. 怎么DIY一个粒子检测器

    01在家带娃能干什么?   最初是在今日头天-大数据文摘(2020-09-28)看到一篇文章 在家做核子研究:怎么DIY一个粒子检测器 ,介绍了Steve Foster(一个刚刚退休英国中央银行 TI ...

最新文章

  1. JSON 列转行的一小段无用代码
  2. linux非lvm分区在线扩容,怎么给不是LVM的根分区扩容
  3. Spring Security 实战:自定义异常处理
  4. Yahoo!团队实践分享:网站性能优化的34条黄金守则
  5. 科罗拉多州立大学计算机科学专业,美国科罗拉多州立大学有哪些好专业?
  6. oracle 查看用户、权限、角色
  7. asp.net 开发知识小结【转】
  8. 英伟达收购交易取消后 ARM将裁员1000人
  9. 自留-Python:线性拟合(直线+曲线)
  10. 金蝶k3服务器系统吗,金蝶k3server2008服务器配置
  11. 采用Java+SSH+JSP技术架构开发实现在线会议租赁管理系统
  12. luogu P1653 猴子
  13. dma循环刷新oled屏幕
  14. 图说当下——人生感悟
  15. 操作系统正则符号知识点总结
  16. java之学习记录 5 - 1 - 模拟拉勾项目介绍与后台系统搭建
  17. 三层架构,四大天王——删
  18. Linux下基本指令
  19. Cannot invoke an object which is possibly ‘undefined‘.Vetur(2722)
  20. configuration 配置文件解析

热门文章

  1. SQL Server2000导出数据时包含主键、字段默认值、描述等信息
  2. C++_程序内存模型_new运算符---C++语言工作笔记030
  3. MyCat分布式数据库集群架构工作笔记0013---高可用_Mycat双主双从复制配置上
  4. mybatis工作笔记002_mybatis中如果返回的结果没有的话默认返回null的list_但可启用returnInstanceForEmptyRow_返回为list不为null但为0条
  5. Linux学习笔记017---文件解压命令的使用_压缩解压
  6. How can I force Python's file.write() to use the same newline format in Windows as in Linux (“\r\n”
  7. Windows 下 C/C++ 多线程编程入门参考范例
  8. java基础案例教程试题,Java基础案例教程-中国大学mooc-试题题目及答案
  9. 学fpga(流水灯)
  10. 机器学习与计算机视觉(深度学习)