本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。交流qq羣:859640274javascript
繼 一位前端小姐姐的五萬字面試寶典 這篇文章以後。徐漂漂小姐姐再次投稿,本文是最近小姐姐整理的前端進階筆記。乾貨依然成噸,全程依舊高能。但願你們多點贊、評論、關注,給小姐姐繼續寫文章的動力!前端
小姐姐的我的博客java
小姐姐依然在看機會喲。base 北京,郵箱已經附在 GitHub 上了。歡迎有坑位的同窗進行推薦。node
首先,這三個方法是用來改變 this 指向的,接下來咱們看一下它們的異同。webpack
B.apply(A, arguments)
; 即 A 對象應用 B 對象的方法。回顧一下 apply 的效果,咱們能夠大體按如下思路走git
接下來,咱們按以上思路來實現一下。es6
微信公衆號:世界上有意思的事
f.apply(o);
// 與下面代碼的功能相似(假設對象o中預先不存在名爲m的屬性)。
o.m=f; //將f存儲爲o的臨時方法
o.m(); //調用它,不傳入參數
delete o.m;//將臨時方法刪除
複製代碼
(以上代碼摘錄自犀牛書)
依樣畫葫蘆,咱們能夠這麼寫:github
微信公衆號:世界上有意思的事
Function.prototype.apply = function (context) {
// context 就是須要綁定的對象,至關於上面的 o
// this 就是調用了 apply 的函數,至關於 f
context.__fn = this // 假設原先沒有__fn
context.__fn()
delete context.__fn
}
複製代碼
接下來咱們想辦法實現一下 apply 的第二個參數。其實我最快想到的是 ES6 的方法。用...
直接展開就好了。不過 apply 才 ES3😂,仍是再想一想老的辦法吧。web
難點是這個數組的長度是不肯定的,也就是說咱們沒辦法很準確地給函數一個個傳參。咱們所能作的處理也就是把arguments
轉成字符串形式'arguments[1], arguments[2], ...'
。那麼如何讓字符串能運行起來呢??答案就是 eval
!面試
稍稍總結一下, 目前想到的 2 種方法
- es6。
context.__fn(...arguments)
- 把 arguments 轉換成string,放到 eval 裏面運行
eval('context.__fn('+ 'arguments[1], arguments[2]' +')')
如下是第二種思路的代碼:
微信公衆號:世界上有意思的事
Function.prototype.apply = function (context, others) {
// context 就是須要綁定的對象,至關於上面的 o
// this 就是調用了 apply 的函數,至關於 f
context.__fn = this // 假設原先沒有__fn
var args = [];
// args: 'others[0], others[1], others[2], ...'
for (var i = 0, len = others.length; i < len; i++) {
args.push('others[' + i + ']');
}
eval('context.__fn(' + args.toString() + ')')
delete context.__fn
}
複製代碼
返回函數調用後的結果就行:
微信公衆號:世界上有意思的事
Function.prototype.apply = function (context, others) {
// context 就是須要綁定的對象,至關於上面的 o
// this 就是調用了 apply 的函數,至關於 f
context.__fn = this // 假設原先沒有__fn
var result;
var args = [];
// args: 'others[0], others[1], others[2], ...'
for (var i = 0, len = others.length; i < len; i++) {
args.push('others[' + i + ']');
}
result = eval('context.__fn(' + args.toString() + ')')
delete context.__fn
}
複製代碼
咱們以前有提到:第一個參數,若是這個函數處於非嚴格模式下,則指定爲 null 或 undefined 時會自動替換爲指向全局對象,而其餘原始值則會被相應的包裝對象(wrapper object)所替代
微信公衆號:世界上有意思的事
Function.prototype.apply = function (context, others) {
if (typeof argsArray === 'undefined' || argsArray === null) {
context = window
}
// context 是一個 object
context = new Object(context)
// context 就是須要綁定的對象,至關於上面的 o
// this 就是調用了 apply 的函數,至關於 f
context.__fn = this // 假設原先沒有__fn
var result;
var args = [];
for (var i = 0, len = others.length; i < len; i++) {
args.push('others[' + i + ']');
}
result = eval('context.__fn(' + args.toString() + ')')
delete context.__fn
}
複製代碼
咱們以前的代碼都是創建在 __fn
不存在的狀況下,那麼萬一存在呢?所以咱們接下來就要找一個 context
中沒有存在過的屬性。
🤔咱們很快能夠想到 ES6 的 symbol。
// 像這樣
var __fn = new Symbol()
context[__fn] = this
複製代碼
🤔若是不用 ES6,那麼另外一種方法,是根據 這篇文章中提到的,本身用 Math.random() 模擬實現獨一無二的 key。面試時能夠直接用生成時間戳便可。
微信公衆號:世界上有意思的事
// 生成 UUID 通用惟一識別碼
// 大概生成 這樣一串 '18efca2d-6e25-42bf-a636-30b8f9f2de09'
function generateUUID(){
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
.toString(16);
}
return uuid;
}
// 簡單實現
// '__' + new Date().getTime();
複製代碼
若是這個key萬一這對象中仍是有,爲了保險起見,能夠作一次緩存操做(就是先把以前的值保存起來)
// 像這樣
var originalvalue = context.__fn
var hasOriginalValue = context.hasOwnProperty('__fn')
context.__fn = this
if(hasOriginalValue){
context.__fn = originalvalue;
}
複製代碼
和 apply 的做用是同樣的,只是 call()
方法接受的是一個參數列表,而 apply()
方法接受的是一個包含多個參數的數組。
例如 func.apply(obj, [1,2])
至關於 func.call(obj, 1, 2)
思路和 apply 同樣。惟一區別就在於參數形式。咱們按照 call 的要求來處理參數就能夠了:
微信公衆號:世界上有意思的事
Function.prototype.apply = function (context) {
// context 就是須要綁定的對象,至關於上面的 o
// this 就是調用了 apply 的函數,至關於 f
context.__fn = this // 假設原先沒有__fn
var result;
var args = [];
// 咱們從 arguments[1] 開始拼就行了
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
result = eval('context.__fn(' + args.toString() + ')')
delete context.__fn
}
複製代碼
咱們常將 bind 和以上兩個方法區分開,是由於 bind 是 ECMAScript 5 中的方法,且除了將函數綁定至一個對象外還多了一些特色。
bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的初始參數,供調用時使用。
func.apply(obj, [1,2])
// 至關於
func.call(obj, 1, 2)
// 至關於
var boundFun = func.bind(obj, 1, 2)
boundFun()
// 也能夠這樣
var boundFun = func.bind(obj, 1)
boundFun(2)
複製代碼
綁定函數也可使用 new 運算符構造,它會表現爲目標函數已經被構建完畢了似的。提供的 this 值會被忽略,但前置參數仍會提供給模擬函數。
咱們仍是先大體思考一下該怎麼作:
後續的步驟我會用 apply/call 來實現bind。若是不想直接用 apply/call,也能夠按照上文先實現一個 apply/call。
Function.prototype.bind = function (context) {
var self = this;
return function () {
return self.apply(context);
}
}
複製代碼
微信公衆號:世界上有意思的事
Function.prototype.bind = function (context) {
var self = this;
// 獲取 bind 函數從第二個參數到最後一個參數
var initialArgs = Array.prototype.slice.call(arguments, 1);
// 返回一個綁定好 this 的新函數
return function () {
// 這個是調用新函數時傳入的參數
var boundArgs = Array.prototype.slice.call(arguments);
// 最終的參數應該是初始參數+新函數的參數
return self.apply(context, args.concat(bindArgs));
}
}
複製代碼
這裏的難點是怎麼知道是由 new 調用的。
先說一下答案吧
// 假若有如下函數
function Person () {
console.log(this)
}
複製代碼
對於
var gioia = new Person()
來講
使用 new 時,this 會指向 gioia,而且 gioia 是 Person 的實例。 所以,若是this instance Person
,就說明是 new 調用的
new 這一部分這裏先不展開講,有興趣的能夠看一下 JavaScript深刻之new的模擬實現
接下來咱們能夠寫代碼了:
微信公衆號:世界上有意思的事
Function.prototype.bind = function (context) {
var self = this;
// 獲取 bind 函數從第二個參數到最後一個參數
var initialArgs = Array.prototype.slice.call(arguments, 1);
// 返回一個綁定好 this 的新函數
function Bound() {
// 這個是調用新函數時傳入的參數
var boundArgs = Array.prototype.slice.call(arguments);
// 最終的參數應該是初始參數+新函數的參數
return self.apply(this instance Bound ? this : context, args.concat(bindArgs));
}
Bound.prototype = this.prototype
return Bound
}
複製代碼
這部分我是看了 lodash 的相關源碼,它真的實現得很是完整!
總結成一句話,就是須要考慮不少數據類型,而後針對這些數據類型拷貝就好了😏
對於引用類型來講,咱們基本能夠按照如下思路走:
基本類型直接賦值就能夠。
function deepClone (value) {
// 基本類型
if (!isObject(value)) {
return value
}
}
// 判斷是否是對象
function isObject(value) {
const type = typeof value
return value != null && (type === 'object' || type === 'function')
}
複製代碼
接下來是怎麼拷貝引用類型。我會按照如下順序來介紹:
另外,lodash 還實現了 Buffer(node.js)等拷貝,但我實際用得很少,就不展開了,有興趣的能夠去看看源碼。
先初始化一個長度爲原數組長度的數組
微信公衆號:世界上有意思的事
export function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
// 數組
if (isArr) {
result = initCloneArray(value)
}
// 待續
}
const hasOwnProperty = Object.prototype.hasOwnProperty
// 數組初始化
function initCloneArray(array) {
const { length } = array
const result = new array.constructor(length)
// 由於 RegExp.prototype.exec() 會返回一個數組或 null,這個數組裏有兩個特殊的屬性:input、index
// 相似 ["foo", index: 6, input: "table football, foosball", groups: undefined]
// 因此須要進行特殊處理
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
複製代碼
微信公衆號:世界上有意思的事
export function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
// 數組
if (isArr) {
result = initCloneArray(value)
}
// 賦值
if (isArr) {
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
}
return result
}
複製代碼
函數的拷貝的話,咱們仍是返回以前的引用。
微信公衆號:世界上有意思的事
export function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
// 數組
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函數
if (isFunc) {
return value
}
}
// 賦值
if (isArr) {
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
}
return result
}
複製代碼
初始化一個對象,而後賦值。
要注意的是這個拷貝後的對象和原對象的原型鏈是同樣的
微信公衆號:世界上有意思的事
function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
const tag = getTag(value)
// 數組
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函數
if (isFunc) {
return value
}
// 對象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
}
}
if (isArr) {
// 數組賦值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 對象賦值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})
}
return result
}
// 能更細緻地判斷是什麼類型
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
const objectProto = Object.prototype
function isPrototype(value) {
const Ctor = value && value.constructor
const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto
return value === proto
}
// 初始化對象
function initCloneObject(object) {
return (typeof object.constructor === 'function' && !isPrototype(object))
? Object.create(Object.getPrototypeOf(object))
: {}
}
複製代碼
包括 Boolean
, Date
, Map
, Number
, RegExp
, Set
, String
, Symbol
接下來的思路也是同樣的,先調用對應的構造函數。而後賦值就好了。稍微麻煩一點的多是 Regexp 正則對象和 Symbol 對象
微信公衆號:世界上有意思的事
function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
const tag = getTag(value)
// 數組
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函數
if (isFunc) {
return value
}
// 對象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
} else {
// 特殊對象的初始化
result = initCloneByTag(value, tag)
}
}
if (isArr) {
// 數組賦值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 對象賦值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})
}
return result
}
const toString = Object.prototype.toString
// 能更細緻地判斷是什麼類型
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
const objectProto = Object.prototype
function isPrototype(value) {
const Ctor = value && value.constructor
const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto
return value === proto
}
// 特殊對象的初始化
function initCloneByTag(object, tag, isDeep) {
const Ctor = object.constructor
switch (tag) {
case '[object Boolean]':
case '[object Date]':
return new Ctor(+object)
case '[object Set]':
case '[object Map]':
return new Ctor
case '[object Number]':
case '[object String]':
return new Ctor(object)
case '[object RegExp]':
return cloneRegExp(object)
case '[object Symbol]':
return cloneSymbol(object)
}
}
const reFlags = /\w*$/
// 拷貝一個正則
function cloneRegExp(regexp) {
// RegExp 構造函數有兩個參數。pattern(正則表達式的文本。),flags(標誌)
// source 屬性返回一個值爲當前正則表達式對象的模式文本的字符串,該字符串不會包含正則字面量兩邊的斜槓以及任何的標誌字符。
// reFlags.exec(regexp) 其實是 reFlags.exec(regexp.toString())。提取出了標誌字符
const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
result.lastIndex = regexp.lastIndex
return result
}
const symbolValueOf = Symbol.prototype.valueOf
// 拷貝一個 Symbol
function cloneSymbol(symbol) {
return Object(symbolValueOf.call(symbol))
}
複製代碼
雖然是特殊對象,但也是對象,因此咱們的思路仍是獲取該對象的全部屬性,而後賦值就能夠了。
須要注意的是
Object.keys
不能獲取 Symbol 屬性,能夠再加上 Object.getOwnPropertySymbols()
來獲取全部 Symbol 屬性名微信公衆號:世界上有意思的事
function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
const tag = getTag(value)
// 數組
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函數
if (isFunc) {
return value
}
// 對象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
} else {
// 特殊對象的初始化
result = initCloneByTag(value, tag)
}
}
// Map 賦值
if (tag === mapTag) {
value.forEach((subValue, key) => {
result.set(key, deepClone(subValue))
})
return result
}
// Set 賦值
if (tag === setTag) {
value.forEach((subValue) => {
result.add(deepClone(subValue))
})
return result
}
if (isArr) {
// 數組賦值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 對象賦值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
// 過濾掉不可枚舉的 Symbol 屬性並賦值
Object.getOwnPropertySymbols(value)
.filter((symbol) => propertyIsEnumerable.call(value, symbol))
.forEach(k => {
result[k] = deepClone(value[k])
})
}
return result
}
複製代碼
微信公衆號:世界上有意思的事
const set = new Set()
set.add('gioia')
set.add('me')
const map = new Map()
map.set(0, 'zero')
map.set(1, 'one')
const original = {
name: 'gioia',
getName: function () {
return this.name
},
[Symbol()]: 'symbol prop',
sym: Symbol('symbol value'),
friends: [{
name: 'xkld',
}, 'cln'],
dress: {
pants: {
color: 'black'
},
shirts: {
colors: ['blue', 'white']
}
},
map: map,
set: set
}
const copy = deepClone(original)
console.log(copy)
console.log(copy.sym === original.sym)
console.log(copy.friends === original.friends)
console.log(copy.map === original.map)
複製代碼
{
name: 'gioia',
getName: [Function: getName],
sym: Symbol(symbol value),
friends: [ { name: 'xkld' }, 'cln' ],
dress: { pants: { color: 'black' }, shirts: { colors: [ 'blue', 'white' ] } },
map: Map { 0 => 'zero', 1 => 'one' },
set: Set { 'gioia', 'me' },
[Symbol()]: 'symbol prop'
}
true
false
false
複製代碼
以上咱們已經初步實現了一個深拷貝了。可是在循環引用的場景下,會出現棧溢出的現象。 例如 original.circle = original
這種狀況,咱們要是還遞歸地賦值的話,就永遠也沒有盡頭🥱
解決辦法就是,看看咱們要拷貝的對象以前有沒有處理過,有的話就直接引用就好了;沒有的話再進行賦值並記錄在案。你能夠選擇不少存儲方案,像 Map,只要能記錄鍵值就能夠了。
微信公衆號:世界上有意思的事
function deepClone(value) {
let result
// 基本類型
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
const tag = getTag(value)
// 數組
if (isArr) {
result = initCloneArray(value)
} else {
const isFunc = typeof value === 'function'
// 函數
if (isFunc) {
return value
}
// 對象或 arguments
if (tag == '[object Object]' || tag == '[object Arguments]') {
result = initCloneObject(value)
} else {
// 特殊對象的初始化
result = initCloneByTag(value, tag)
}
}
// 檢查循環引用並返回其對應的拷貝
cache || (cache = new Map())
const cached = cache.get(value)
if (cached) {
return cached
}
cache.set(value, result)
// Map 賦值
if (tag === mapTag) {
value.forEach((subValue, key) => {
result.set(key, deepClone(subValue))
})
return result
}
// Set 賦值
if (tag === setTag) {
value.forEach((subValue) => {
result.add(deepClone(subValue))
})
return result
}
if (isArr) {
// 數組賦值
for (let i = 0; i< value.length; i++) {
result[i] = deepClone(value[i])
}
} else {
// 對象賦值
Object.keys(Object(value)).forEach(k => {
result[k] = deepClone(value[k])
})
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
// 過濾掉不可枚舉的 Symbol 屬性並賦值
Object.getOwnPropertySymbols(value)
.filter((symbol) => propertyIsEnumerable.call(value, symbol))
.forEach(k => {
result[k] = deepClone(value[k])
})
}
return result
}
複製代碼
不知道有沒有人和我同樣,無論看了幾遍文檔,仍是不會本身寫 webpack,只能在別人寫的配置上修修補補,更別提什麼優化了。
因而我痛定思痛,決定從源頭上解決這個問題!爲了更好地應用 webpack,咱們應該瞭解它背後的工做原理。
所以,我閱讀了 miniwebpack 這個倉庫。這個倉庫實現了一個最簡單的打包工具。接下來我會按照個人理解來解釋一下怎麼實現一個簡單的打包工具
參考:Babel用戶手冊
下面我將介紹一些這個過程當中須要用到的工具。
用來將源代碼轉換爲 AST。
(不瞭解 AST 的,能夠先看看在線AST轉換器。)
npm install --save babylon
複製代碼
import * as babylon from "babylon";
babylon.parse(code, [options])
複製代碼
用來操做 AST
npm install --save babel-traverse
複製代碼
該模塊僅暴露出一個 traverse 方法。traverse 方法是一個遍歷方法, path 封裝了每個節點,而且還提供容器 container ,做用域 scope 這樣的字段。提供個更多關於節點的相關的信息,讓咱們更好的操做節點。
示例:
//
import traverse from "babel-traverse";
traverse(ast, {
enter(path) {
if (path.node.type === "Identifier"
&& path.node.name === 'text') {
path.node.name = 'alteredText';
}
}
})
複製代碼
能夠根據 AST 生成代碼
npm install --save babel-generator
複製代碼
import generate from "babel-generator";
const genCode = generate(ast, {}, code);
複製代碼
最開始咱們提到,須要構建一個依賴關係圖。那麼咱們先從第一步開始,實現根據某個文件(輸入絕對路徑)提取依賴。大體能夠分紅如下幾步:
咱們用 node.js 的 fs
模塊就能夠
const fs = require('fs');
const content = fs.readFileSync(filename, 'utf-8');
複製代碼
用到咱們以前提到的 babylon
const ast = babylon.parse(content, {
sourceType: 'module',
});
複製代碼
這裏咱們須要操做 AST,因此用到 babel-traverse
const dependencies = [];
// 要作到這一點,咱們檢查`ast`中的每一個 `import` 聲明.
traverse(ast, {
// `Ecmascript`模塊至關簡單,由於它們是靜態的. 這意味着你不能`import`一個變量,
// 或者有條件地`import`另外一個模塊.
// 每次咱們看到`import`聲明時,咱們均可以將其數值視爲`依賴性`.
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
},
});
複製代碼
咱們簡單地用 id 表示
// 遞增簡單計數器
const id = ID++;
複製代碼
使用 babel
const {transformFromAst} = require('babel-core');
// 該`presets`選項是一組規則,告訴`babel`如何傳輸咱們的代碼.
// 咱們用`babel-preset-env``將咱們的代碼轉換爲瀏覽器能夠運行的東西.
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
複製代碼
那麼 code 到底長什麼樣呢
- 首先,babel 能將 es6 等更新的代碼轉成瀏覽器能執行的低版本代碼,這個以前一直在強調的
- 其次,對於模塊的轉換。Babel 對 ES6 模塊轉碼就是轉換成 CommonJS 規範
Babel 對於模塊輸出的轉換,就是把全部輸出都賦值到 exports 對象的屬性上,並加上 ESModule: true 的標識。表示這個模塊是由 ESModule 轉換來的 CommonJS 輸出 輸入就是 require
例如,對於如下文件
// entry.js
import message from './message.js';
console.log(message);
複製代碼
// message.js
import {name} from './name.js';
export default `hello ${name}!`;
複製代碼
按照上面的規範,轉換後的代碼大概是這樣大概是這樣:
// entry.js
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
複製代碼
// message.js
"use strict";
// 加上 ESModule: true 的標識
Object.defineProperty(exports, "__esModule", {
value: true
});
var _name = require("./name.js");
// 把全部輸出都賦值到 exports 對象的屬性上
exports.default = "hello " + _name.name + "!";
複製代碼
return {
id,
filename,
dependencies,
code,
};
複製代碼
以上,咱們就處理好了一個模塊。包含着如下 4 項信息
經過第一步,咱們已經能生成某個模塊的依賴了。接下來,咱們就能夠順藤摸瓜,從入口文件開始,生成入口文件的依賴,再生成入口文件的依賴的依賴,再生成入口文件的依賴的依賴依...(禁止套娃),直到全部模塊處理完畢
微信公衆號:世界上有意思的事
const path = require('path');
// entry 爲入口文件的路徑
function createGraph(entry) {
// createAsset 是咱們在【第一步,提取某文件的依賴】中實現的函數
// mainAsset 就是入口模塊的信息了
const mainAsset = createAsset(entry);
// 使用一個隊列,剛開始只有入口模塊
const queue = [mainAsset];
for (const asset of queue) {
// mapping 用來將【依賴的相對路徑】映射到【該依賴的模塊 id】
asset.mapping = {};
// 這個模塊所在的目錄.
const dirname = path.dirname(asset.filename);
// 遍歷每個依賴。
asset.dependencies.forEach(relativePath => {
// 獲得依賴的絕對路徑
const absolutePath = path.join(dirname, relativePath);
// 獲得 child 的模塊信息
const child = createAsset(absolutePath);
// 將【依賴的相對路徑】映射到【該依賴的模塊 id】
// 由於若是不作映射。最終打包到一個文件後,編碼時的相對路徑就無論用了。咱們就無法知道像 require('./child') 這種代碼到底應該加載哪個模塊
asset.mapping[relativePath] = child.id;
// 把這個子模塊也放進隊列裏面
queue.push(child);
});
}
// 到這一步,隊列 就是一個包含目標應用中 每一個模塊 的數組
// 實際上這個就是咱們最終的依賴關係圖了
return queue;
}
複製代碼
對於如下文件
// ./example/entry.js
import message from './message.js';
console.log(message);
複製代碼
// ./example/message.js
import {name} from './name.js';
export default `hello ${name}!`;
複製代碼
// ./example/name.js
export const name = 'world';
複製代碼
咱們處理後的依賴關係圖應該是這樣的
微信公衆號:世界上有意思的事
[{
id: 0,
filename: './example/entry.js',
dependencies: ['./message.js'],
code: ,// 略
mapping: {
'./message.js': 1
}
}, {
id: 1,
filename: './example/message.js',
dependencies: ['./name.js'],
code: ,// 略
mapping: {
'./name.js': 2
}
}, {
id: 2,
filename: './example/name.js',
dependencies: [],
code: ,// 略
mapping: {}
}]
複製代碼
目前,咱們已經有了依賴圖
graph: Module[]
interface Module {
id: number // 模塊id;在【提取某文件的依賴】這一步中咱們使用的是一個遞增的 id
filename: string
dependencies: Module[]
code: string // 該模塊的代碼(通過轉換的,能在瀏覽器中運行)
mapping: Record<string, number> // 將依賴的相對路徑轉換成id。是咱們在【生成依賴圖】這一步所作的工做
}
複製代碼
既然已經到了這一步了,就說明咱們得處理一下 code
了。在【使代碼支持全部瀏覽器】這一步中,咱們已經知道了,code
是符合 CommonJS 規範的。但CommonJS 中有如下幾個東西,是瀏覽器中沒有的:
那麼接下來就是咱們本身實現這3個東西!
首先把咱目前的模塊信息整合一下:
function (require, module, exports) {
{code}
}
複製代碼
最終把全部這樣的模塊放在 modules 中,大概是這樣:
/* {0: [ function (require, module, exports) { {code} }, mapping: { './message.js': 1 } ]} */
modules: Record<number, [(require, module, exports) => any, Record<string, number>]>
複製代碼
接下來咱們寫主程序,咱們主程序要作的工做有
require
, module
, exports
微信公衆號:世界上有意思的事
(function(modules) {
function require(id) {
// 從 modules 拿到 【執行函數】和【mapping】
const [fn, mapping] = modules[id];
// 本身實現的 require,能夠根據相對路徑加載依賴
function localRequire(name) {
return require(mapping[name]);
}
// // 本身實現的 module 和 exports
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
// 調用入口文件
require(0);
})(modules)
複製代碼
後續小姐姐還會投稿更多的前端進階相關的文章。你們趕忙關注、點贊一波,防止錯過更多的精彩內容!