abstrakt:在上篇手記我們簡單實(shí)現(xiàn)了一個(gè) jQuery 的基礎(chǔ)結(jié)構(gòu),不過為了順應(yīng)潮流,這次咱把它改為模塊化的寫法,此舉得以有效提升項(xiàng)目的可維護(hù)性,因此在后續(xù)也將以模塊化形式進(jìn)行持續(xù)開發(fā)。1. 基本配置為了讓 rollup 得以靜態(tài)解析模塊,從而減少可能存在的冗余代碼,我們得用上 ES6 的解構(gòu)賦值語法,因此得配合 babel 輔助開發(fā)。在目錄下我們新建一個(gè) babel 配置“.babelrc”:{
在上篇手記我們簡單實(shí)現(xiàn)了一個(gè) jQuery 的基礎(chǔ)結(jié)構(gòu),不過為了順應(yīng)潮流,這次咱把它改為模塊化的寫法,此舉得以有效提升項(xiàng)目的可維護(hù)性,因此在后續(xù)也將以模塊化形式進(jìn)行持續(xù)開發(fā)。
1. 基本配置
為了讓 rollup 得以靜態(tài)解析模塊,從而減少可能存在的冗余代碼,我們得用上 ES6 的解構(gòu)賦值語法,因此得配合 babel 輔助開發(fā)。
在目錄下我們新建一個(gè) 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
后續(xù)咱們直接執(zhí)行:
node rollup.config.js
即可實(shí)現(xiàn)打包。
2. 模塊拆分
從模塊功能性入手,我們暫時(shí)先簡單地把上次的整個(gè) IIFE 代碼段拆分為:
src/jquery.js //出口模塊 src/core.js //jQuery核心模塊 src/global.js //全局變量處理模塊 src/init.js //初始化模塊
它們的內(nèi)容分別如下:
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 中我們先加了一層判斷,如果使用者走的模塊化形式,那是無須考慮全局變量沖突處理的,直接繞過該模塊即可。
執(zhí)行打包后效果如下(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; }));
3. extend 完善
如上章所說,我們可以通過 $.extend / $.fn.extend 接口來擴(kuò)展 JQ 的靜態(tài)方法/實(shí)例方法,也可以簡單地實(shí)現(xiàn)對象的合并和深/淺拷貝。這是非常重要且實(shí)用的功能,在這里我們得完善它。
在 core.js 中我們新增如下代碼段:
jQuery.extend = jQuery.fn.extend = function() { var options, target = arguments[ 0 ] || {}, //target為要被合并的目標(biāo)對象 i = 1, length = arguments.length, deep = false; //默認(rèn)為淺拷貝 // 若第一個(gè)參數(shù)為Boolean,表示其為決定是否要深拷貝的參數(shù) if ( typeof target === "boolean" ) { deep = target; // 那么 target 參數(shù)就得往后挪一位了 target = arguments[ i ] || {}; i++; } // 若 target 類型不是對象的處理 if ( typeof target !== "object" && typeof target !== "function" ) { target = {}; } // 若 target 后沒有其它參數(shù)(要被拷貝的對象)了,則直接擴(kuò)展jQuery自身(把target合并入jQuery) if ( i === length ) { target = this; i--; //減1是為了方便取原target(它反過來變成被拷貝的源對象了) } for ( ; i < length; i++ ) { // 只處理源對象值不為 null/undefined 的情況 if ( ( options = arguments[ i ] ) != null ) { // TODO - 完善Extend } } // 返回修改后的目標(biāo)對象 return target; };
該段代碼可以判斷如下寫法并做對應(yīng)處理:
$.extend( targetObj, copyObj1[, copyObj2...] ) $.extend( true, targetObj, copyObj1[, copyObj2...] ) $.extend( copyObj ) $.extend( true, copyObj )
其它情況會(huì)被繞過(返回空對象)。
我們繼續(xù)完善內(nèi)部的遍歷:
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 ]; // 避免自己合自己,導(dǎo)致無限循環(huán) if ( target === copy ) { continue; } // 深拷貝,且確保被拷貝屬性值為對象/數(shù)組 if ( deep && copy && ( isObject( copy ) || ( copyIsArray = isArray( copy ) ) ) ) { //被拷貝屬性值為數(shù)組 if ( copyIsArray ) { copyIsArray = false; //若被合并屬性不是數(shù)組,則設(shè)為[] clone = src && isArray( src ) ? src : []; } else { //被拷貝屬性值為對象 //若被合并屬性不是數(shù)組,則設(shè)為{} clone = src && isObject( src ) ? src : {}; } // 右側(cè)遞歸直到最內(nèi)層屬性值非對象,再把返回值賦給 target 對應(yīng)屬性 target[ name ] = jQuery.extend( deep, clone, copy ); // 非對象/數(shù)組,或者淺拷貝情況(注意排除 undefined 類型) } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // 返回被修改后的目標(biāo)對象 return target;
這里需要留意的有,我們會(huì)通過
jQuery.extend( deep, clone, copy )
來遞歸生成被合并的 target 屬性值,這是為了避免擴(kuò)展后的 target 屬性和被擴(kuò)展的 copyObj 屬性引用了同一個(gè)對象,導(dǎo)致互相影響。
通過 extend 遞歸解剖 copyObj 源對象的屬性直到最內(nèi)層,最內(nèi)層屬性的值(上方代碼里的 copy)大致有這么兩種情況:
1. copy 為空對象/空數(shù)組:
for ( ; i < length; i++ ) { //遍歷被拷貝對象 // 只處理源對象值不為 null/undefined 的情況 if ( ( options = arguments[ i ] ) != null ) { //空數(shù)組/空對象沒有可枚舉的元素/屬性,這里會(huì)忽略 } } // 返回被修改后的目標(biāo)對象 return target; //直接返回空數(shù)組/空對象
2. copy 為非對象(如“vajoy”):
if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = jQuery.isArray( copy ) ) ) ) { //不會(huì)執(zhí)行這里 } else if ( copy !== undefined ) {// 執(zhí)行這里 target[ name ] = copy; } } } } // 返回如 ['vajoy'] 或者 {'name' : 'vajoy'} return target;
從而確保 target 所擴(kuò)展的每一層屬性都跟 copyObj 的是互不關(guān)聯(lián)的。
P.S. jQuery 里的深拷貝實(shí)現(xiàn)其實(shí)比較簡單,如果希望能做到更全面的兼容,可以參考 lodash 中的實(shí)現(xiàn)。
4. 建立基礎(chǔ)工具模塊
在上方的 extend 代碼塊中其實(shí)存在兩個(gè)不合理的地方:
1. 僅通過 Object.toString.call(obj) === "[object Object]" 作為對象判斷條件在我們擴(kuò)展對象的邏輯中有些片面,適合擴(kuò)展的對象應(yīng)當(dāng)是“純粹/簡單”(plain)的 js Object 對象,但在某些瀏覽器中,像 document 在 Object.toSting 調(diào)用時(shí)也會(huì)返回和 Object 相同結(jié)果;
2. 像 Object.hasOwnProperty 和 Object.prototype.toString.call 等方法在我們后續(xù)開發(fā)中會(huì)經(jīng)常使用上,如果能把它們寫到一個(gè)模塊中封裝起來復(fù)用就更好了。
基于上述兩點(diǎn),我們新增一個(gè) var.js 來封裝這些常用的輸出:
export var class2type = {}; //在core.js中會(huì)被賦予各類型屬性值 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構(gòu)造函數(shù)字符串"function Object() { [native code] }",用于判斷 plainObj
然后在 core.js 導(dǎo)入所需接口即可:
import { class2type, toString, getProto, hasOwn, fnToString, ObjectFunctionString } from './var.js';
我們進(jìn)一步修改 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 ) ) { //修改點(diǎn)1 target = {}; } 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 arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || //修改點(diǎn)2 ( copyIsArray = jQuery.isArray( copy ) ) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray( src ) ? src : []; //修改點(diǎn)3 } else { clone = src && jQuery.isPlainObject( src ) ? src : {}; } target[ name ] = jQuery.extend( deep, clone, copy ); } else if ( copy !== undefined ) { target[ name ] = copy; } } } } return target; };//新增修改點(diǎn)1,class2type注入各JS類型鍵值對,配合 jQuery.type 使用,后面會(huì)用上"Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ").forEach(function(name){ class2type[ "[object " + name + "]" ] = name.toLowerCase(); });//新增修改點(diǎn)2jQuery.extend( { isArray: Array.isArray, isPlainObject: function( obj ) { var proto, Ctor; // 明顯的非對象判斷,直接返回false if ( !obj || toString.call( obj ) !== "[object Object]" ) { return false; } proto = getProto( obj ); //獲取 prototype // 通過 Object.create( null ) 形式創(chuàng)建的 {} 是沒有prototype的 if ( !proto ) { return true; } // 簡單對象的構(gòu)造函數(shù)等于最頂層 Object 構(gòu)造函數(shù) 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 四個(gè) jQuery 靜態(tài)方法,其中 isPlainObject 比較有趣,為了過濾某些瀏覽器中的 document 等特殊類型,會(huì)對 obj.prototype 及其構(gòu)造函數(shù)進(jìn)行判斷:
1. 通過Object.create( null ) 形式創(chuàng)建的 {} ,或者實(shí)例對象都是沒有 prototype 的,直接返回 true;2. 判斷其構(gòu)造函數(shù)合法性(存在且等于原生的對象構(gòu)造器 function Object(){ [native code] })
關(guān)于第二點(diǎn),實(shí)際是直接判斷兩個(gè)構(gòu)造器字符串化后是否相同:
Function.toString.call(constructor) === Function.toString.call(Object)
另外,需要留意的是,通過這段代碼:
//新增修改點(diǎn)1,class2type注入各JS類型鍵值對,配合 jQuery.type 使用,后面會(huì)用上 "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" }
所以后續(xù)只需要通過
class2type[ Object.prototype.toString(obj) ]
就能獲取 obj 的類型名稱。isFunction 接口便是利用這種鉤子模式判斷傳入?yún)?shù)是否函數(shù)類型的:
isFunction: function( obj ) { return jQuery.type( obj ) === "function"; }
最后。我們執(zhí)行打包處理:
node rollup.config.js
在 HTML 頁面運(yùn)行下述代碼:
var $div = $('div'); $div.setBackground().setColor(); var arr = [1, 2, 3]; console.log($.type(arr))
效果如下:
留意 $.type 靜態(tài)方法是我們上方通過 jQuery.extend 擴(kuò)展進(jìn)去的:
//新增修改點(diǎn)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- 函數(shù)表達(dá)式類型不正確情況 class2type[ toString.call( obj ) ] || "object" : typeof obj; } });
它返回傳入?yún)?shù)的類型(小寫)。該方法在我們下一章也會(huì)直接在模塊中使用到。