一位前端小姐姐的進階筆記(一)

本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。交流qq羣:859640274javascript

一位前端小姐姐的五萬字面試寶典 這篇文章以後。徐漂漂小姐姐再次投稿,本文是最近小姐姐整理的前端進階筆記。乾貨依然成噸,全程依舊高能。但願你們多點贊、評論、關注,給小姐姐繼續寫文章的動力!前端

小姐姐的我的博客java

小姐姐依然在看機會喲。base 北京,郵箱已經附在 GitHub 上了。歡迎有坑位的同窗進行推薦。node

1、apply/call/bind 一網打盡

首先,這三個方法是用來改變 this 指向的,接下來咱們看一下它們的異同。webpack

1. apply

  • 調用一個對象的一個方法,用另外一個對象替換當前對象。例如:B.apply(A, arguments); 即 A 對象應用 B 對象的方法。
  • 要注意的是第一個參數,若是這個函數處於非嚴格模式下,則指定爲 null 或 undefined 時會自動替換爲指向全局對象,而其餘原始值則會被相應的包裝對象(wrapper object)所替代。

1.1 如何實現一個apply

回顧一下 apply 的效果,咱們能夠大體按如下思路走git

  1. 實現第一個參數的功能,改變 this 指向
  2. 實現第二個參數的功能。第二個參數是做爲調用函數的參數
  3. 返回值:使用調用者提供的 this 值和參數調用該函數的返回值。若該方法沒有返回值,則返回 undefined。

接下來,咱們按以上思路來實現一下。es6

1.1.1 第一步,綁定 this

微信公衆號:世界上有意思的事

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
}
複製代碼

1.1.2 第二步,給函數傳遞參數

接下來咱們想辦法實現一下 apply 的第二個參數。其實我最快想到的是 ES6 的方法。用... 直接展開就好了。不過 apply 才 ES3😂,仍是再想一想老的辦法吧。web

難點是這個數組的長度是不肯定的,也就是說咱們沒辦法很準確地給函數一個個傳參。咱們所能作的處理也就是把arguments轉成字符串形式'arguments[1], arguments[2], ...'。那麼如何讓字符串能運行起來呢??答案就是 eval面試

稍稍總結一下, 目前想到的 2 種方法

  1. es6。context.__fn(...arguments)
  2. 把 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
}
複製代碼

1.1.3 第三步,返回值

返回函數調用後的結果就行:

微信公衆號:世界上有意思的事

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
}
複製代碼

1.1.4 更進一步,嚴格模式下的 this

咱們以前有提到:第一個參數,若是這個函數處於非嚴格模式下,則指定爲 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
}
複製代碼

1.1.5 再進一步,確保 __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;
}
複製代碼

2. call

  • 和 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
}
複製代碼

3. bind

咱們常將 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 值會被忽略,但前置參數仍會提供給模擬函數。

咱們仍是先大體思考一下該怎麼作:

  1. 實現第一個參數的功能,改變 this 指向。這個和 apply/call 是同樣的。
  2. 返回值:返回一個新的函數。
  3. 實現其它參數。其它參數將做爲新函數的初始參數,供調用時使用。這個和 call 有些類似。
  4. 使用 new 操做符時,應該忽略第一個參數

後續的步驟我會用 apply/call 來實現bind。若是不想直接用 apply/call,也能夠按照上文先實現一個 apply/call。

3.1.1 第一步,返回一個綁定了 this 的新函數

Function.prototype.bind = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}
複製代碼

3.1.2 第二步,給新函數設定初始參數

微信公衆號:世界上有意思的事

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));
    }
}
複製代碼

3.1.3 第三步,做爲構造函數調用時,忽略要綁定的 this

這裏的難點是怎麼知道是由 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
}
複製代碼

2、如何實現一個深拷貝

這部分我是看了 lodash 的相關源碼,它真的實現得很是完整!
總結成一句話,就是須要考慮不少數據類型,而後針對這些數據類型拷貝就好了😏

對於引用類型來講,咱們基本能夠按照如下思路走:

  1. 初始化。即調用相應的構造函數
  2. 遞歸地賦值
  3. 有循環引用的話須要處理一下

1. 拷貝基本類型

基本類型直接賦值就能夠。

function deepClone (value) {
    // 基本類型
    if (!isObject(value)) {
        return value
    }
}

// 判斷是否是對象
function isObject(value) {
    const type = typeof value
    return value != null && (type === 'object' || type === 'function')
}
複製代碼

接下來是怎麼拷貝引用類型。我會按照如下順序來介紹:

  1. 數組
  2. 函數
  3. 對象
  4. 特殊類型。Boolean、Date、Map、Number 等等

另外,lodash 還實現了 Buffer(node.js)等拷貝,但我實際用得很少,就不展開了,有興趣的能夠去看看源碼。

2. 拷貝數組

2.1 初始化

先初始化一個長度爲原數組長度的數組

微信公衆號:世界上有意思的事

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
}
複製代碼

2.2 賦值

微信公衆號:世界上有意思的事

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
}
複製代碼

3. 拷貝函數

函數的拷貝的話,咱們仍是返回以前的引用。

微信公衆號:世界上有意思的事

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
}
複製代碼

4. 拷貝對象

初始化一個對象,而後賦值。
要注意的是這個拷貝後的對象和原對象的原型鏈是同樣的

微信公衆號:世界上有意思的事

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))
      : {}
}
複製代碼

5. 拷貝特殊對象

包括 Boolean, Date, Map, Number, RegExp, Set, String, Symbol

接下來的思路也是同樣的,先調用對應的構造函數。而後賦值就好了。稍微麻煩一點的多是 Regexp 正則對象和 Symbol 對象

5.1 初始化

微信公衆號:世界上有意思的事

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))
}
複製代碼

5.2 賦值

雖然是特殊對象,但也是對象,因此咱們的思路仍是獲取該對象的全部屬性,而後賦值就能夠了。
須要注意的是

  1. Object.keys 不能獲取 Symbol 屬性,能夠再加上 Object.getOwnPropertySymbols()來獲取全部 Symbol 屬性名
  2. Set 和 Map 的賦值是經過 add 和 set 來的
微信公衆號:世界上有意思的事

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

}
複製代碼

6. 測試

微信公衆號:世界上有意思的事

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
複製代碼

7. 解決循環引用

以上咱們已經初步實現了一個深拷貝了。可是在循環引用的場景下,會出現棧溢出的現象。 例如 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

}
複製代碼

3、寫一個簡易的打包工具

1. 前言

不知道有沒有人和我同樣,無論看了幾遍文檔,仍是不會本身寫 webpack,只能在別人寫的配置上修修補補,更別提什麼優化了。
因而我痛定思痛,決定從源頭上解決這個問題!爲了更好地應用 webpack,咱們應該瞭解它背後的工做原理。
所以,我閱讀了 miniwebpack 這個倉庫。這個倉庫實現了一個最簡單的打包工具。接下來我會按照個人理解來解釋一下怎麼實現一個簡單的打包工具

2. 主要思路

  1. 代碼處理。咱們日常寫代碼的時候,用的多是ES六、ES7等高版本的語法,咱們須要將它們轉換成瀏覽器能運行的語法
  2. 打包。須要根據一個 entry 來輸出一個 output,咱們經過維護一個依賴關係圖來解決這個問題

圖1:流程圖.png

3. 代碼處理

  1. 解析(parse)。將源代碼變成AST。
  2. 轉換(transform)。操做AST,這也是咱們能夠操做的部分,去改變代碼。
  3. 生成(generate)。將更改後的AST,再變回代碼。

參考:Babel用戶手冊

下面我將介紹一些這個過程當中須要用到的工具。

3.1 解析器 babylon

用來將源代碼轉換爲 AST。
(不瞭解 AST 的,能夠先看看在線AST轉換器。)

3.1.1 安裝

npm install --save babylon
複製代碼

3.1.2 使用

import * as babylon from "babylon";

babylon.parse(code, [options])
複製代碼

3.2 轉換器 babel-traverse

用來操做 AST

3.2.1 安裝

npm install --save babel-traverse
複製代碼

3.2.2 使用

該模塊僅暴露出一個 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';
    }
  }
})
複製代碼

3.3 生成器 babel-generator

能夠根據 AST 生成代碼

3.3.1 安裝

npm install --save babel-generator
複製代碼

3.3.2 使用

import generate from "babel-generator";

const genCode = generate(ast, {}, code);
複製代碼

4. 實現細節

4.1 第一步,提取某文件的依賴

最開始咱們提到,須要構建一個依賴關係圖。那麼咱們先從第一步開始,實現根據某個文件(輸入絕對路徑)提取依賴。大體能夠分紅如下幾步:

  1. 讀取文件內容
  2. 生成 AST
  3. 遍歷 AST 來理解這個模塊依賴哪些模塊
  4. 爲該模塊分配惟一標識符
  5. 使代碼支持全部瀏覽器

4.1.1 讀取文件內容

咱們用 node.js 的 fs 模塊就能夠

const fs = require('fs');
const content = fs.readFileSync(filename, 'utf-8');
複製代碼

4.1.2 生成 AST

用到咱們以前提到的 babylon

const ast = babylon.parse(content, {
  sourceType: 'module',
});
複製代碼

4.1.3 遍歷 AST 來試着理解這個模塊依賴哪些模塊

這裏咱們須要操做 AST,因此用到 babel-traverse

const dependencies = [];
// 要作到這一點,咱們檢查`ast`中的每一個 `import` 聲明.
traverse(ast, {
// `Ecmascript`模塊至關簡單,由於它們是靜態的. 這意味着你不能`import`一個變量,
// 或者有條件地`import`另外一個模塊. 
// 每次咱們看到`import`聲明時,咱們均可以將其數值視爲`依賴性`.
  ImportDeclaration: ({node}) => {
    dependencies.push(node.source.value);
  },
});

複製代碼

4.1.4 爲模塊分配惟一標識符

咱們簡單地用 id 表示

// 遞增簡單計數器
const id = ID++;
複製代碼

4.1.5 使代碼支持全部瀏覽器

使用 babel

const {transformFromAst} = require('babel-core');

// 該`presets`選項是一組規則,告訴`babel`如何傳輸咱們的代碼. 
// 咱們用`babel-preset-env``將咱們的代碼轉換爲瀏覽器能夠運行的東西. 
const {code} = transformFromAst(ast, null, {
  presets: ['env'],
});
複製代碼

那麼 code 到底長什麼樣呢

  1. 首先,babel 能將 es6 等更新的代碼轉成瀏覽器能執行的低版本代碼,這個以前一直在強調的
  2. 其次,對於模塊的轉換。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 + "!"; 
複製代碼

4.1.6 返回模塊信息

return {
  id,
  filename,
  dependencies,
  code,
};
複製代碼

以上,咱們就處理好了一個模塊。包含着如下 4 項信息

  • 模塊 id
  • 文件的絕對路徑
  • 該模塊的依賴。保存着的是依賴們的相對路徑
  • 該模塊內部代碼(瀏覽器可運行)

4.2 第二步,生成依賴圖

經過第一步,咱們已經能生成某個模塊的依賴了。接下來,咱們就能夠順藤摸瓜,從入口文件開始,生成入口文件的依賴,再生成入口文件的依賴的依賴,再生成入口文件的依賴的依賴依...(禁止套娃),直到全部模塊處理完畢

微信公衆號:世界上有意思的事

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: {}
}]
複製代碼

4.3 第三步,根據依賴圖生成代碼

目前,咱們已經有了依賴圖

graph: Module[]

interface Module {
  id: number // 模塊id;在【提取某文件的依賴】這一步中咱們使用的是一個遞增的 id
  filename: string 
  dependencies: Module[]
  code: string // 該模塊的代碼(通過轉換的,能在瀏覽器中運行) 
  mapping: Record<string, number> // 將依賴的相對路徑轉換成id。是咱們在【生成依賴圖】這一步所作的工做
}
複製代碼

既然已經到了這一步了,就說明咱們得處理一下 code 了。在【使代碼支持全部瀏覽器】這一步中,咱們已經知道了,code 是符合 CommonJS 規範的。但CommonJS 中有如下幾個東西,是瀏覽器中沒有的:

  • require
  • module
  • exports

那麼接下來就是咱們本身實現這3個東西!

首先把咱目前的模塊信息整合一下:

  • mapping 是確定要的。由於咱們模塊的被轉換後會經過相對路徑來調用 require() ,而咱們須要知道對應去加載哪一個模塊
  • code 須要稍微改一下。每一個模塊的做用域應該是獨立的。因此咱們改爲這樣:
    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>]>
複製代碼

接下來咱們寫主程序,咱們主程序要作的工做有

  1. 實現 require, module, exports
  2. 默認調用入口文件
  3. 自執行
微信公衆號:世界上有意思的事

(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)
複製代碼

4、尾巴

後續小姐姐還會投稿更多的前端進階相關的文章。你們趕忙關注、點贊一波,防止錯過更多的精彩內容!

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息