首頁 > 軟體

Tree Shaking實現方法指南

2023-03-07 06:02:04

正文

當使用JavaScript框架或庫時,程式碼中可能會存在許多未使用的函數和變數,這些未使用的程式碼會使應用程式的檔案大小變大,從而影響應用程式的效能。Tree shaking可以解決這個問題,它可以通過檢測和刪除未使用的程式碼來減小檔案大小並提高應用程式效能。

接下來我們將通過兩種方式實現Tree shaking

方式一:JavaScript模擬

1、首先,你需要使用ES6模組來匯出和匯入程式碼。ES6模組可以靜態分析,從而使Tree shaking技術成為可能。例如,在一個名為“math.js”的檔案中,你可以使用以下程式碼來匯出函數:

export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
export function multiply(a, b) {
  return a * b;
}

2、在應用程式入口處標記使用的程式碼: 在應用程式的入口處,你需要標記使用的程式碼。這可以通過建立一個名為"usedExports"的集合來實現,其中包含你在入口檔案中使用的匯出。

import { add } from './math.js';
const usedExports = new Set([add]);

在這個範例中,我們建立了一個名為"usedExports"的集合,並將我們在應用程式入口檔案中使用的add函數新增到集合中。

3、遍歷並檢測未使用的程式碼: 在應用程式的所有模組中遍歷匯出並檢查它們是否被使用。你可以使用JavaScript的反射API來實現這一點。以下是程式碼範例:

function isUsedExport(exportName) {
  return usedExports.has(eval(exportName));
}
for (const exportName of Object.keys(exports)) {
  if (!isUsedExport(exportName)) {
    delete exports[exportName];
  }
}

在這個範例中,我們定義了一個isUsedExport函數來檢查是否使用了給定的匯出名稱。然後,我們遍歷應用程式中的所有匯出,並將每個匯出的名稱作為引數傳遞給isUsedExport函數。如果匯出沒有被使用,則從exports物件中刪除該匯出。

4、最後,我們需要在控制檯中呼叫一些函數以確保它們仍然可以正常工作。由於我們只在"usedExports"集合中新增了add函數,因此subtract()和multiply()函數已經被刪除了。

方式二:利用AST實現

假設我們有以下的 source 程式碼:

import { sum } from './utils';
export function add(a, b) {
  return sum(a, b);
}
export const PI = 3.14;

我們首先需要使用 @babel/parser 將原始碼解析成 AST:

const parser = require("@babel/parser");
const fs = require("fs");
const sourceCode = fs.readFileSync("source.js", "utf8");
const ast = parser.parse(sourceCode, {
  sourceType: "module",
});

接著,我們需要遍歷 AST 並找到所有被使用的匯出變數和函數:

// 建立一個 Set 來儲存被使用的匯出
const usedExports = new Set();
// 標記被使用的匯出
function markUsedExports(node) {
  if (node.type === "Identifier") {
    usedExports.add(node.name);
  } else if (node.type === "ExportSpecifier") {
    usedExports.add(node.exported.name);
  }
}
// 遍歷 AST 樹並標記被使用的匯出
function traverse(node) {
  if (node.type === "CallExpression") {
    markUsedExports(node.callee);
    node.arguments.forEach(markUsedExports);
  } else if (node.type === "MemberExpression") {
    markUsedExports(node.property);
    markUsedExports(node.object);
  } else if (node.type === "Identifier") {
    usedExports.add(node.name);
  } else if (node.type === "ExportNamedDeclaration") {
    if (node.declaration) {
      if (node.declaration.type === "FunctionDeclaration") {
        usedExports.add(node.declaration.id.name);
      } else if (node.declaration.type === "VariableDeclaration") {
        node.declaration.declarations.forEach((decl) => {
          usedExports.add(decl.id.name);
        });
      }
    } else {
      node.specifiers.forEach((specifier) => {
        usedExports.add(specifier.exported.name);
      });
    }
  } else if (node.type === "ImportDeclaration") {
    node.specifiers.forEach((specifier) => {
      usedExports.add(specifier.local.name);
    });
  } else {
    for (const key of Object.keys(node)) {
      // 遍歷物件的屬性,如果屬性的值也是物件,則遞迴呼叫 traverse 函數
      if (key !== "loc" && node[key] && typeof node[key] === "object") {
        traverse(node[key]);
      }
    }
  }
}
// 遍歷整個 AST 樹
traverse(ast);

在這裡,我們建立了一個 Set 來儲存被使用的匯出,然後遍歷 AST 樹並標記被使用的匯出。具體來說,我們會:

  • 標記被呼叫的函數;
  • 標記被存取的物件屬性;
  • 標記被直接參照的變數和函數;
  • 標記被匯出的變數和函數;
  • 標記被匯入的變數和函數。

我們通過遍歷 AST 樹並呼叫 markUsedExports 函數來標記被使用的匯出,最終將這些匯出儲存在 usedExports Set 中。

接下來,我們需要遍歷 AST 並刪除未被使用的程式碼:

// 移除未使用的程式碼
function removeUnusedCode(node) {
  // 處理常式宣告
  if (node.type === "FunctionDeclaration") {
    if (!usedExports.has(node.id.name)) { // 如果該函數未被使用
      node.body.body = []; // 將該函數體清空
    }
  }
  // 處理變數宣告
  else if (node.type === "VariableDeclaration") {
    node.declarations = node.declarations.filter((decl) => {
      return usedExports.has(decl.id.name); // 過濾出被使用的宣告
    });
    if (node.declarations.length === 0) { // 如果沒有被使用的宣告
      node.type = "EmptyStatement"; // 將該宣告置為 EmptyStatement
    }
  }
  // 處理匯出宣告
  else if (node.type === "ExportNamedDeclaration") {
    if (node.declaration) {
      // 處理常式匯出宣告
      if (node.declaration.type === "FunctionDeclaration") {
        if (!usedExports.has(node.declaration.id.name)) { // 如果該函數未被使用
          node.declaration.body.body = []; // 將該函數體清空
        }
      }
      // 處理變數匯出宣告
      else if (node.declaration.type === "VariableDeclaration") {
        node.declaration.declarations = node.declarations.filter((decl) =>
        return usedExports.has(decl.id.name); // 過濾出被使用的宣告
      });
      if (node.declaration.declarations.length === 0) { // 如果沒有被使用的宣告
        node.type = "EmptyStatement"; // 將該宣告置為 EmptyStatement
      }
    } else {
      // 處理匯出的具體內容
      node.specifiers = node.specifiers.filter((specifier) => {
        return usedExports.has(specifier.exported.name); // 過濾出被使用的內容
      });
      if (node.specifiers.length === 0) { // 如果沒有被使用的內容
        node.type = "EmptyStatement"; // 將該宣告置為 EmptyStatement
      }
    }
  }
  // 處理匯入宣告
  else if (node.type === "ImportDeclaration") {
    node.specifiers = node.specifiers.filter((specifier) => {
      return usedExports.has(specifier.local.name); // 過濾出被使用的宣告
    });
    if (node.specifiers.length === 0) { // 如果沒有被使用的宣告
      node.type = "EmptyStatement"; // 將該宣告置為 EmptyStatement
    }
  }
  // 處理表示式語句
  else if (node.type === "ExpressionStatement") {
    if (node.expression.type === "AssignmentExpression") {
      if (!usedExports.has(node.expression.left.name)) { // 如果該表示式未被使用
        node.type = "EmptyStatement"; // 將該語句置為 EmptyStatement
      }
    }
  }
  // 處理其他情況
  else {
    for (const key of Object.keys(node)) {
      if (key !== "loc" && node[key] && typeof node[key] === "object") {
        removeUnusedCode(node[key]); // 遞迴處理子節點
      }
    }
  }
}
removeUnusedCode(ast); // 執行移除未使用程式碼的

在這裡,我們遍歷 AST 並刪除所有未被使用的程式碼。具體地,我們會:

  • 刪除未被使用的函數和變數的函數體和宣告語句;
  • 刪除未被使用的匯出和匯入宣告;
  • 刪除未被使用的賦值語句。

最後,我們將修改後的 AST 重新轉換回 JavaScript 程式碼:

const { transformFromAstSync } = require("@babel/core");
const { code } = transformFromAstSync(ast, null, {
  presets: ["@babel/preset-env"],
});
console.log(code);

這裡我們使用了 @babel/core 將 AST 轉換回 JavaScript 程式碼。由於我們使用了 @babel/preset-env,它會自動將我們的程式碼轉換成 ES5 語法,以便於在各種瀏覽器上執行。

這只是一個簡單的例子,實際上還有很多細節需要處理,比如處理 ES modules、CommonJS 模組和 UMD 模組等。不過,這個例子可以幫助我們理解 Tree Shaking 的工作原理,以及如何手動實現它。

以上就是Tree Shaking實現方法指南的詳細內容,更多關於Tree Shaking實現的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com