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

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

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

1. 基本配置

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

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

  1. {
  2. "presets": ["es2015-rollup"]
  3. }

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

  1. var rollup = require( 'rollup' );
  2. var babel = require('rollup-plugin-babel');
  3.  
  4. rollup.rollup({
  5. entry: 'src/jquery.js',
  6. plugins: [ babel() ]
  7. }).then( function ( bundle ) {
  8. bundle.write({
  9. format: 'umd',
  10. moduleName: 'jQuery',
  11. dest: 'rel/jquery.js'
  12. });
  13. });

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

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

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

后续咱们直接执行:

  1. node rollup.config.js

即可实现打包。

2. 模块拆分

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

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

它们的内容分别如下:

jquery.js:

  1. import jQuery from './core';
  2. import global from './global';
  3. import init from './init';
  4.  
  5. global(jQuery);
  6. init(jQuery);
  7.  
  8. export default jQuery;

core.js:

  1. var version = "0.0.1",
  2. jQuery = function (selector, context) {
  3.  
  4. return new jQuery.fn.init(selector, context);
  5. };
  6.  
  7. jQuery.fn = jQuery.prototype = {
  8. jquery: version,
  9. constructor: jQuery,
  10. setBackground: function(){
  11. this[0].style.background = 'yellow';
  12. return this
  13. },
  14. setColor: function(){
  15. this[0].style.color = 'blue';
  16. return this
  17. }
  18. };
  19.  
  20. export default jQuery;

init.js:

  1. var init = function(jQuery){
  2. jQuery.fn.init = function (selector, context, root) {
  3. if (!selector) {
  4. return this;
  5. } else {
  6. var elem = document.querySelector(selector);
  7. if (elem) {
  8. this[0] = elem;
  9. this.length = 1;
  10. }
  11. return this;
  12. }
  13. };
  14.  
  15. jQuery.fn.init.prototype = jQuery.fn;
  16. };
  17.  
  18. export default init;

global.js:

  1. var global = function(jQuery){
  2. //走模块化形式的直接绕过
  3. if(typeof module === 'object' && typeof module.exports !== 'undefined') return;
  4.  
  5. var _jQuery = window.jQuery,
  6. _$ = window.$;
  7.  
  8. jQuery.noConflict = function( deep ) {
  9. //确保window.$没有再次被改写
  10. if ( window.$ === jQuery ) {
  11. window.$ = _$;
  12. }
  13.  
  14. //确保window.jQuery没有再次被改写
  15. if ( deep && window.jQuery === jQuery ) {
  16. window.jQuery = _jQuery;
  17. }
  18.  
  19. return jQuery; //返回 jQuery 接口引用
  20. };
  21.  
  22. window.jQuery = window.$ = jQuery;
  23. };
  24.  
  25. export default global;

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

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

  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. (global.jQuery = factory());
  5. }(this, function () { 'use strict';
  6.  
  7. /**
  8. * Created by vajoy on 2016/8/1.
  9. */
  10.  
  11. var version = "0.0.1";
  12. var jQuery = function jQuery(selector, context) {
  13.  
  14. return new jQuery.fn.init(selector, context);
  15. };
  16. jQuery.fn = jQuery.prototype = {
  17. jquery: version,
  18. constructor: jQuery,
  19. setBackground: function setBackground() {
  20. this[0].style.background = 'yellow';
  21. return this;
  22. },
  23. setColor: function setColor() {
  24. this[0].style.color = 'blue';
  25. return this;
  26. }
  27. };
  28.  
  29. var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
  30. return typeof obj;
  31. } : function (obj) {
  32. return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;
  33. };
  34.  
  35. /**
  36. * Created by vajoy on 2016/8/2.
  37. */
  38. var global$1 = function global(jQuery) {
  39. //走模块化形式的直接绕过
  40. if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object' && typeof module !== 'undefined') return;
  41.  
  42. var _jQuery = window.jQuery,
  43. _$ = window.$;
  44.  
  45. jQuery.noConflict = function (deep) {
  46. //确保window.$没有再次被改写
  47. if (window.$ === jQuery) {
  48. window.$ = _$;
  49. }
  50.  
  51. //确保window.jQuery没有再次被改写
  52. if (deep && window.jQuery === jQuery) {
  53. window.jQuery = _jQuery;
  54. }
  55.  
  56. return jQuery; //返回 jQuery 接口引用
  57. };
  58.  
  59. window.jQuery = window.$ = jQuery;
  60. };
  61.  
  62. /**
  63. * Created by vajoy on 2016/8/1.
  64. */
  65.  
  66. var init = function init(jQuery) {
  67. jQuery.fn.init = function (selector, context, root) {
  68. if (!selector) {
  69. return this;
  70. } else {
  71. var elem = document.querySelector(selector);
  72. if (elem) {
  73. this[0] = elem;
  74. this.length = 1;
  75. }
  76. return this;
  77. }
  78. };
  79.  
  80. jQuery.fn.init.prototype = jQuery.fn;
  81. };
  82.  
  83. global$1(jQuery);
  84. init(jQuery);
  85.  
  86. return jQuery;
  87.  
  88. }));

3. extend 完善

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

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

  1. jQuery.extend = jQuery.fn.extend = function() {
  2. var options,
  3. target = arguments[ 0 ] || {}, //target为要被合并的目标对象
  4. i = 1,
  5. length = arguments.length,
  6. deep = false; //默认为浅拷贝
  7.  
  8. // 若第一个参数为Boolean,表示其为决定是否要深拷贝的参数
  9. if ( typeof target === "boolean" ) {
  10. deep = target;
  11.  
  12. // 那么 target 参数就得往后挪一位了
  13. target = arguments[ i ] || {};
  14. i++;
  15. }
  16.  
  17. // 若 target 类型不是对象的处理
  18. if ( typeof target !== "object" && typeof target !== "function" ) {
  19. target = {};
  20. }
  21.  
  22. // 若 target 后没有其它参数(要被拷贝的对象)了,则直接扩展jQuery自身(把target合并入jQuery)
  23. if ( i === length ) {
  24. target = this;
  25. i--; //减1是为了方便取原target(它反过来变成被拷贝的源对象了)
  26. }
  27.  
  28. for ( ; i < length; i++ ) {
  29.  
  30. // 只处理源对象值不为 null/undefined 的情况
  31. if ( ( options = arguments[ i ] ) != null ) {
  32.  
  33. // TODO - 完善Extend
  34. }
  35. }
  36.  
  37. // 返回修改后的目标对象
  38. return target;
  39. };

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

  1. $.extend( targetObj, copyObj1[, copyObj2...] )
  2. $.extend( true, targetObj, copyObj1[, copyObj2...] )
  3. $.extend( copyObj )
  4. $.extend( true, copyObj )

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

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

  1. var isObject = function(obj){
  2. return Object.prototype.toString.call(obj) === "[object Object]"
  3. };
  4. var isArray = function(obj){
  5. return Object.prototype.toString.call(obj) === "[object Array]"
  6. };
  7.  
  8. for ( ; i < length; i++ ) { //遍历被拷贝的源对象
  9.  
  10. // 只处理源对象值不为 null/undefined 的情况
  11. if ( ( options = arguments[ i ] ) != null ) {
  12.  
  13. var name, clone, copy;
  14. // 遍历源对象属性
  15. for ( name in options ) {
  16. src = target[ name ];
  17. copy = options[ name ];
  18.  
  19. // 避免自己合自己,导致无限循环
  20. if ( target === copy ) {
  21. continue;
  22. }
  23.  
  24. // 深拷贝,且确保被拷贝属性值为对象/数组
  25. if ( deep && copy && ( isObject( copy ) ||
  26. ( copyIsArray = isArray( copy ) ) ) ) {
  27.  
  28. //被拷贝属性值为数组
  29. if ( copyIsArray ) {
  30. copyIsArray = false;
  31. //若被合并属性不是数组,则设为[]
  32. clone = src && isArray( src ) ? src : [];
  33.  
  34. } else { //被拷贝属性值为对象
  35. //若被合并属性不是数组,则设为{}
  36. clone = src && isObject( src ) ? src : {};
  37. }
  38.  
  39. // 右侧递归直到最内层属性值非对象,再把返回值赋给 target 对应属性
  40. target[ name ] = jQuery.extend( deep, clone, copy );
  41.  
  42. // 非对象/数组,或者浅拷贝情况(注意排除 undefined 类型)
  43. } else if ( copy !== undefined ) {
  44. target[ name ] = copy;
  45. }
  46. }
  47. }
  48. }
  49.  
  50. // 返回被修改后的目标对象
  51. return target;

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

  1. jQuery.extend( deep, clone, copy )

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

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

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

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

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

  1. if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
  2. ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
  3. //不会执行这里
  4.  
  5. } else if ( copy !== undefined ) {// 执行这里
  6. target[ name ] = copy;
  7. }
  8. }
  9. }
  10. }
  11.  
  12. // 返回如 ['vajoy'] 或者 {'name' : 'vajoy'}
  13. 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 来封装这些常用的输出:

  1. export var class2type = {}; //在core.js中会被赋予各类型属性值
  2.  
  3. export const toString = class2type.toString; //等同于 Object.prototype.toString
  4.  
  5. export const getProto = Object.getPrototypeOf;
  6.  
  7. export const hasOwn = class2type.hasOwnProperty;
  8.  
  9. export const fnToString = hasOwn.toString; //等同于 Object.toString/Function.toString
  10.  
  11. export const ObjectFunctionString = fnToString.call( Object ); //顶层Object构造函数字符串"function Object() { [native code] }",用于判断 plainObj

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

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

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

  1. jQuery.extend = jQuery.fn.extend = function() {
  2. var options, name, src, copy, copyIsArray, clone,
  3. target = arguments[ 0 ] || {},
  4. i = 1,
  5. length = arguments.length,
  6. deep = false;
  7.  
  8. if ( typeof target === "boolean" ) {
  9. deep = target;
  10.  
  11. target = arguments[ i ] || {};
  12. i++;
  13. }
  14.  
  15. if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { //修改点1
  16. target = {};
  17. }
  18.  
  19. if ( i === length ) {
  20. target = this;
  21. i--;
  22. }
  23.  
  24. for ( ; i < length; i++ ) {
  25.  
  26. if ( ( options = arguments[ i ] ) != null ) {
  27.  
  28. for ( name in options ) {
  29. src = target[ name ];
  30. copy = options[ name ];
  31.  
  32. if ( target === copy ) {
  33. continue;
  34. }
  35.  
  36. // Recurse if we're merging plain objects or arrays
  37. if ( deep && copy && ( jQuery.isPlainObject( copy ) || //修改点2
  38. ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
  39.  
  40. if ( copyIsArray ) {
  41. copyIsArray = false;
  42. clone = src && jQuery.isArray( src ) ? src : []; //修改点3
  43.  
  44. } else {
  45. clone = src && jQuery.isPlainObject( src ) ? src : {};
  46. }
  47.  
  48. target[ name ] = jQuery.extend( deep, clone, copy );
  49.  
  50. } else if ( copy !== undefined ) {
  51. target[ name ] = copy;
  52. }
  53. }
  54. }
  55. }
  56.  
  57. return target;
  58. };
  59.  
  60. //新增修改点1,class2type注入各JS类型键值对,配合 jQuery.type 使用,后面会用上
  61. "Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){
  62. class2type[ "[object " + name + "]" ] = name.toLowerCase();
  63. });
  64.  
  65. //新增修改点2
  66. jQuery.extend( {
  67. isArray: Array.isArray,
  68. isPlainObject: function( obj ) {
  69. var proto, Ctor;
  70.  
  71. // 明显的非对象判断,直接返回false
  72. if ( !obj || toString.call( obj ) !== "[object Object]" ) {
  73. return false;
  74. }
  75.  
  76. proto = getProto( obj ); //获取 prototype
  77.  
  78. // 通过 Object.create( null ) 形式创建的 {} 是没有prototype的
  79. if ( !proto ) {
  80. return true;
  81. }
  82.  
  83. // 简单对象的构造函数等于最顶层 Object 构造函数
  84. Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
  85. return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
  86. },
  87. isFunction: function( obj ) {
  88. return jQuery.type( obj ) === "function";
  89. },
  90. //获取类型(如'function')
  91. type: function( obj ) {
  92. if ( obj == null ) {
  93. return obj + ""; //'undefined' 或 'null'
  94. }
  95.  
  96. return typeof obj === "object" || typeof obj === "function" ?
  97. class2type[ toString.call( obj ) ] || "object" :
  98. typeof obj;
  99. }
  100.  
  101. });

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

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

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

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

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

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

class2type 对象是变成了这样的:

  1. {
  2. "[object Boolean]":"boolean",
  3. "[object Number]":"number",
  4. "[object String]":"string",
  5. "[object Function]":"function",
  6. "[object Array]":"array",
  7. "[object Date]":"date",
  8. "[object RegExp]":"regexp",
  9. "[object Object]":"object",
  10. "[object Error]":"error",
  11. "[object Symbol]":"symbol"
  12. }

所以后续只需要通过

  1. class2type[ Object.prototype.toString(obj) ]

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

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

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

  1. node rollup.config.js

在 HTML 页面运行下述代码:

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

效果如下:

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

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

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

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

从零开始,DIY一个jQuery(2)的更多相关文章

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

    在前两章,为了方便调试,我们写了一个非常简单的 jQuery.fn.init 方法: jQuery.fn.init = function (selector, context, root) { if ...

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

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

  3. 自己diy一个jquery分页插件

    js基础学习过程中,期间经历换工作的各种面试,很多面试官问过:有没有写过jquery插件?等类似问题. 就个人而言,关于jquery插件的文章确实看过不少,但是一直没有动手写一个,一是不想在目前学习j ...

  4. 从零开始构建一个的asp.net Core 项目

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

  5. 一起学习造轮子(二):从零开始写一个Redux

    本文是一起学习造轮子系列的第二篇,本篇我们将从零开始写一个小巧完整的Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Red ...

  6. 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise

    本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...

  7. 一起学习造轮子(三):从零开始写一个React-Redux

    本文是一起学习造轮子系列的第三篇,本篇我们将从零开始写一个React-Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Re ...

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

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

  9. Vue.js 入门:从零开始做一个极简 To-Do 应用

    Vue.js 入门:从零开始做一个极简 To-Do 应用 写作时间:2019-12-10版本信息:Vue.js 2.6.10官网文档:https://cn.vuejs.org/ 前言  学习 Vue ...

随机推荐

  1. Content Security Policy 入门教程

    阮一峰文章:Content Security Policy 入门教程

  2. 利用XAG在RAC环境下实现GoldenGate自动Failover

    概述 在RAC环境下配置OGG,要想实现RAC节点故障时,OGG能自动的failover到正常节点,要保证两点: 1. OGG的checkpoint,trail,BR文件放置在共享的集群文件系统上,R ...

  3. animate.css(第三方动画使用方法)

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 17.0px Monaco; color: #a5b2b9 } animation 语法: animatio ...

  4. Android MVP+Retrofit+RxJava实践小结

    关于MVP.Retrofit.RxJava,之前已经分别做了分享,如果您还没有阅读过,可以猛戳: 1.Android MVP 实例 2.Android Retrofit 2.0使用 3.RxJava ...

  5. 如何使用dos命令打开当前用户、当前日期、当前时间以及当前用户加当前时间?

    1.dos命令安装mysqld --stall.启动net start mysql.进入MySQL数据库mysql -uroot -p后,输入select user();当前用户 select cur ...

  6. jQuery中取消后续执行内容

    <html xmlns="http://www.w3.org/1999/xhtml"><head>    <title></title&g ...

  7. [AlwaysOn Availability Groups] 健康模型 Part 2 ——扩展

    健康模型扩展 第一部分已经介绍了AlwayOn健康模型的概述.现在是创建一个自己的PBM策略,然后设置为制定的归类.创建这些策略,创建之后修改一下配置,dashboard就会自动评估这些策略. 场景, ...

  8. WebAPI 2参数绑定方法

    简单类型参数 Example 1: Sending a simple parameter in the Url [RoutePrefix("api/values")] public ...

  9. JS中关于字符串的几个常用又容易忘记的方法

    1>.字符串连接:concat(): 左边字符串. concat(连接的字符串1,字符串2,....); 获取指定位置的字符:charAt(): 返回指定位置的字符:  字符串.charAt(i ...

  10. Javascript实践技巧

    最近辞职了,准备北上.期待有个好结果~   本文以<Javascript高级程序设计>为基础,结合自身经验来总结下Javascript实际工作方面的知识.   一.可维护性 1.代码约定 ...