babel源碼解析之(@babel/plugin-transform-runtime)

前言

前面我們用了一篇很長的文章介紹了@babel/preset-env,感興趣的可以去看我之前的一篇文章babel源碼解析之(@babel/preset-env),今天我們要分析的是babel的一個插件,叫@babel/plugin-transform-runtime.

簡介

我們看一下官網對它的描述:

A plugin that enables the re-use of Babel’s injected helper code to save on codesize.

很簡短的一個描述信息,翻譯一下大概是:“抽離babel的一些公共工具類用來減少代碼的大小”,雖然描述很少,但是理解起來好像比較抽象,下面我們一起結合demo一步步分析一下。

開始

我們還是繼續使用我們前面的demo項目

我們先安裝一下@babel/plugin-transform-runtime插件,

npm install -D @babel/plugin-transform-runtime

然後我們在src目錄底下創建一個demo.runtime.js用來測試,

src/demo.runtime.js:

const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}

可以看到,除了之前的一些代碼外,我們還加入了一個es6的generator函數,我們直接用一下@babel/plugin-transform-runtime插件,然後用它的默認設置,

babel.config.js:

module.exports = {
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "absoluteRuntime": false,
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false,
                "version": "7.0.0-beta.0"
            }
        ]
    ]
};

我們運行babel編譯看結果:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo.runtime.js -o ./lib/demo.runtime.js

lib/demo.runtime.js:

const fn = () => {};

new Promise(() => {});

class Test {
  say() {}

}

const c = [1, 2, 3].includes(1);
var a = 10;

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

可以看到,經過runtime插件處理後代碼並沒有改變,這是爲什麼呢?因爲在我們runtime插件的配置中我們默認是關閉掉一些功能的,比如我們把runtime的corejs打開,

babel.config.js:

module.exports = {
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "absoluteRuntime": false,
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false,
                "version": "7.0.0-beta.0"
            }
        ]
    ]
};

再次運行看結果:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo.runtime.js                         
import _Promise from "@babel/runtime-corejs2/core-js/promise";

const fn = () => {};

new _Promise(() => {});

class Test {
  say() {}

}

const c = [1, 2, 3].includes(1);
var a = 10;

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

➜  babel-demo git:(v0.0.1)

可以看到,自動幫我們引入了一個polyfill(_Promise),那小夥伴要疑問了,es6的語法沒轉換?是的! 因爲runtime不做這些語法的轉換,它只能算是一個轉換幫助類、一個自動添加polyfill的工具,es6語法轉換我們上一節用了preset-env,所以我們把preset-env加上,然後把polyfill去掉,最後runtime配置還原到默認配置,

babel.config.js:

module.exports = {
    presets:[
        [
            "@babel/preset-env"
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "absoluteRuntime": false,
                "corejs": false,
                "helpers": true,
                "regenerator": true,
                "useESModules": false,
                "version": "7.0.0-beta.0"
            }
        ]
    ]
};

再次運行babel看效果:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo.runtime.js -o ./lib/demo.runtime.js

lib/demo.runtime.js:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regeneratorRuntime2 = require("@babel/runtime/regenerator");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _marked = /*#__PURE__*/_regeneratorRuntime2.mark(helloWorldGenerator);

var fn = function fn() {};

new Promise(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    (0, _classCallCheck2.default)(this, Test);
  }

  (0, _createClass2.default)(Test, [{
    key: "say",
    value: function say() {}
  }]);
  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

function helloWorldGenerator() {
  return _regenerator.default.wrap(function helloWorldGenerator$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'hello';

        case 2:
          _context.next = 4;
          return 'world';

        case 4:
          return _context.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

看結果也看不出什麼,那runtime到底爲我們做了什麼呢?我們試一下如果我們不使用runtime插件,直接使用preset-env看結果:

babel.config.js

module.exports = {
    presets:[
        [
            "@babel/preset-env"
        ]
    ],
    plugins: [
        // [
        //     "@babel/plugin-transform-runtime",
        //     {
        //         "absoluteRuntime": false,
        //         "corejs": false,
        //         "helpers": true,
        //         "regenerator": true,
        //         "useESModules": false,
        //         "version": "7.0.0-beta.0"
        //     }
        // ]
    ]
};

運行babel看結果:

"use strict";

var _marked = /*#__PURE__*/regeneratorRuntime.mark(helloWorldGenerator);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var fn = function fn() {};

new Promise(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

function helloWorldGenerator() {
  return regeneratorRuntime.wrap(function helloWorldGenerator$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'hello';

        case 2:
          _context.next = 4;
          return 'world';

        case 4:
          return _context.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

ok! 可以看到,在沒有使用runtime的時候,我們的_classCallCheck、_defineProperties、_createClass都是在當前代碼中,如果使用了runtime後,這些方法都會直接從@babel/runtime/helpers中導入:

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regeneratorRuntime2 = require("@babel/runtime/regenerator");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _marked = /*#__PURE__*/_regeneratorRuntime2.mark(helloWorldGenerator);

所以,如果當我們有很多需要編譯的文件的時候,每個文件中都會有這些方法的定義,這樣整個包就會很大,runtime把這些方法抽離到一個公共的地方,所以可以讓我們打包出來的源碼變小。

配置

corejs

false, 2, 3 or { version: 2 | 3, proposals: boolean }, defaults to false.

比如:['@babel/plugin-transform-runtime', { corejs: 3 }]

corejs是可以讓當前環境支持es的最新特性的api墊片(polyfill),在babel之前版本在用@babel/polyfill,從7.4.0版本後就用core-js代替了polyfill,比如我們之前在代碼中加入全部的polyfill的是這樣的:

import "@babel/polyfill";

換成core-js後可以是這樣的:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

所以core-js是包含了polyfill的特性,更多的core-js內容大家可以看官網https://github.com/zloirock/core-js

這裏的corejs配置的就是我們將要使用的runtime-corejs的版本,有2跟3的版本,2版本是3之前的版本,所以3有一些es最新的一些特性,比如我們demo中的Array.prototy.includes方法,只有core-js3上纔有:

var c = [1, 2, 3].includes(1);
選用corejs的版本 Install command
false npm install --save @babel/runtime
2 npm install --save @babel/runtime-corejs2
3 npm install --save @babel/runtime-corejs3

爲了方便更好的分析,我們直接安裝一下runtime-core2跟runtime-core3:

npm install -D @babel/runtime-corejs2 && npm install -D @babel/runtime-corejs3

我們修改一下我們demo項目的配置文件,然後先把corejs改成2,

babel.config.js:

module.exports = {
    presets:[
        [
            "@babel/preset-env"
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 2,
            }
        ]
    ]
};

然後我運行babel看效果:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo.runtime.js -o ./lib/demo.runtime.js

lib/demo.runtime.js:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _regeneratorRuntime2 = require("@babel/runtime-corejs2/regenerator");

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var _marked = /*#__PURE__*/_regeneratorRuntime2.mark(helloWorldGenerator);

var fn = function fn() {};

new _promise.default(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    (0, _classCallCheck2.default)(this, Test);
  }

  (0, _createClass2.default)(Test, [{
    key: "say",
    value: function say() {}
  }]);
  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

function helloWorldGenerator() {
  return _regenerator.default.wrap(function helloWorldGenerator$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'hello';

        case 2:
          _context.next = 4;
          return 'world';

        case 4:
          return _context.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

可以看到,只幫我們加了一個_promise(Promise的polyfill),我們並沒看到Array.prototype.includes的墊片。

我們修改一下配置文件,把corejs的版本改成3,

babel.config.js:

module.exports = {
    presets:[
        [
            "@babel/preset-env"
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 3,
            }
        ]
    ]
};

再次運行看結果,

lib/demo.runtime.js:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var _context;

var _marked = /*#__PURE__*/_regenerator.default.mark(helloWorldGenerator);

var fn = function fn() {};

new _promise.default(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    (0, _classCallCheck2.default)(this, Test);
  }

  (0, _createClass2.default)(Test, [{
    key: "say",
    value: function say() {}
  }]);
  return Test;
}();

var c = (0, _includes.default)(_context = [1, 2, 3]).call(_context, 1);
var a = 10;

function helloWorldGenerator() {
  return _regenerator.default.wrap(function helloWorldGenerator$(_context2) {
    while (1) {
      switch (_context2.prev = _context2.next) {
        case 0:
          _context2.next = 2;
          return 'hello';

        case 2:
          _context2.next = 4;
          return 'world';

        case 4:
          return _context2.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context2.stop();
      }
    }
  }, _marked);
}

可以看到corejs3給我們添加了一個_includes方法當成了polyfill,如果看過之前preset-env那篇文章的同學可能會發現了,用transform-runtime插件添加的polyfill都是帶有 "__"符號的變量(可以看成局部變量),是不會污染全局變量的,我們再來回顧一下preset-env,我們修改一下配置文件,把runtime插件去掉,然後開啓preset-env的polyfill,preset-env的內容不懂的小夥伴可以看我之前的那篇文章哦,

babel.config.js:

module.exports = {
    presets:[
        [
            "@babel/preset-env",
            {
                corejs: 3,
                useBuiltIns: "usage"
            }
        ]
    ],
    plugins: [
        // [
        //     "@babel/plugin-transform-runtime",
        //     {
        //         "corejs": 3,
        //     }
        // ]
    ]
};

運行看效果,

lib/demo.runtime.js:

"use strict";

require("core-js/modules/es.array.includes");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("regenerator-runtime/runtime");

var _marked = /*#__PURE__*/regeneratorRuntime.mark(helloWorldGenerator);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var fn = function fn() {};

new Promise(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

function helloWorldGenerator() {
  return regeneratorRuntime.wrap(function helloWorldGenerator$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'hello';

        case 2:
          _context.next = 4;
          return 'world';

        case 4:
          return _context.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

可以看到,首先的效果跟runtime插件是一樣的,但是preset-env加的polyfill是直接導入corejs然後替換掉全局變量的,這樣會造成全局變量的污染。

好啦,我們順便把runtime插件跟preset-env的區別都給講了,下面我們結合babel的源碼具體分析一下transfrom-runtime插件是怎樣結合@babel/runtime還有corejs對我們代碼進行轉換的。

packages/babel-plugin-transform-runtime/src/index.js:

export default declare((api, options, dirname) => {
  api.assertVersion(7);

  const {
    corejs,
    helpers: useRuntimeHelpers = true,
    regenerator: useRuntimeRegenerator = true,
    useESModules = false,
    version: runtimeVersion = "7.0.0-beta.0",
    absoluteRuntime = false,
  } = options;
		let proposals = false;
  let rawVersion;
	
  //如果傳遞的是corejs: {version:3,proposals:true}對象類型的時候就拆分version跟proposals字段
  if (typeof corejs === "object" && corejs !== null) {
    rawVersion = corejs.version;
    proposals = Boolean(corejs.proposals);
  } else {
    rawVersion = corejs;
  }
	//獲取corejs版本號
  const corejsVersion = rawVersion ? Number(rawVersion) : false;
  //校驗版本號
  if (![false, 2, 3].includes(corejsVersion)) {
    throw new Error(
      `The \`core-js\` version must be false, 2 or 3, but got ${JSON.stringify(
        rawVersion,
      )}.`,
    );
  }
	//校驗proposals參數只能出現在corejsVersion版本爲3的情況
  if (proposals && (!corejsVersion || corejsVersion < 3)) {
    throw new Error(
      "The 'proposals' option is only supported when using 'corejs: 3'",
    );
  }
  ...
  
  /*
  	如果是core3版本的話就依賴“@babel/runtime-corejs3”
  	如果是core2版本的話就依賴“@babel/runtime-corejs2”
  	默認是依賴“@babel/runtime”
  */
   const moduleName = injectCoreJS3
    ? "@babel/runtime-corejs3"
    : injectCoreJS2
    ? "@babel/runtime-corejs2"
    : "@babel/runtime";
	/*
		如果是core3版本並且開啓提案選項的時候就會把corejs的根目錄設置爲“core-js”(包含了最新提案的core-js)
		反之會將corejs的根目錄設置爲“core-js-stable”(穩定版本的core-js)
	*/
  const corejsRoot = injectCoreJS3 && !proposals ? "core-js-stable" : "core-js";
  ...
}

ok,我們看到這裏:

/*
		如果是core3版本並且開啓提案選項的時候就會把corejs的根目錄設置爲“core-js”(包含了最新提案的core-js)
		反之會將corejs的根目錄設置爲“core-js-stable”(穩定版本的core-js)
	*/
  const corejsRoot = injectCoreJS3 && !proposals ? "core-js-stable" : "core-js";

我們沒有將proposals設置爲true的時候我們看一下編譯結果,

babel.config.js:

module.exports = {
    presets:[
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 3,
            }
        ]
    ]
};

lib/demo.runtime.js:

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
...

可以看到,runtime插件幫我們安裝的polyfill都是依賴的core-js-stable版本的corejs,如果我們將proposals設置爲true我們看一下效果,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": {version: 3, proposals: true},
            }
        ]
    ]
};

lib/demo.runtime.js:

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes"));
...
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/promise"));

可以看到,當設置proposals爲true的時候,runtime插件依賴的是core-js目錄的polyfill,我們分別點開“core-js-stable”跟“core-js”的promise目錄看一下有什麼區別,

首先是“core-js-stable”的“@babel/runtime-corejs3/core-js-stable/promise”,

xxxbabel-demo/node_modules/@babel/runtime-corejs3/core-js-stable/promise.js:

module.exports = require("core-js-pure/stable/promise");

然後是“core-js”的“@babel/runtime-corejs3/core-js/promise”,

xxx/babel-demo/node_modules/@babel/runtime-corejs3/core-js/promise.js:

module.exports = require("core-js-pure/features/promise");

可以看到,都是引用了“core-js-pure”,那麼“core-js-pure”又是啥呢?其實是core-js的另外一個版本,叫:“純淨的core-js”,也就是說不會污染全局變量的意思,具體小夥伴可以看core-js的官網裏面有詳細說明的。

都是依賴的“core-js-pure”但是下級目錄就不一樣了,一個是“stable”一個是“features”,我們繼續往下看,找到這兩個文件,

node_modules/core-js-pure/features/promise/index.js:

var parent = require('../../es/promise');
require('../../modules/esnext.aggregate-error');
// TODO: Remove from `core-js@4`
require('../../modules/esnext.promise.all-settled');
require('../../modules/esnext.promise.try');
require('../../modules/esnext.promise.any');

module.exports = parent;

node_modules/core-js-pure/es/promise/index.js:

require('../../modules/es.object.to-string');
require('../../modules/es.string.iterator');
require('../../modules/web.dom-collections.iterator');
require('../../modules/es.promise');
require('../../modules/es.promise.all-settled');
require('../../modules/es.promise.finally');
var path = require('../../internals/path');

module.exports = path.Promise;

可以看到,feature的少了很多內容,然後還有依賴了一些“esnext”打頭的模塊,“esnext”打頭的也就是說下一個es版本中可能會出現的一些內容(處於stage階段,還不怎麼穩定)。

ok!我們瞭解corejs2跟3的區別,然後還分析了proposals參數,當runtime插件拿到了我們的corejs之後又是怎樣動態的注入到我們的代碼中的呢?

比如我們“src/demo.runtime.js”文件中有一個Promise,那麼runtime是怎麼注入的呢?看過前面preset-env文章的童鞋應該是多多少少有點感覺了,其實就是遍歷ast的節點,然後遍歷到Promise的時候動態的添加上polyfill代碼,也就是一下代碼:

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

我們看一下源碼,

Xxxx/babel-demo/node_modules/@babel/plugin-transform-runtime/lib/index.js:

... 
visitor: {
      ReferencedIdentifier(path) {
        const {
          node,
          parent,
          scope
        } = path;
        const {
          name
        } = node;

        if (name === "regeneratorRuntime" && useRuntimeRegenerator) {
          path.replaceWith(this.addDefaultImport(`${modulePath}/regenerator`, "regeneratorRuntime"));
          return;
        }

        if (!injectCoreJS) return;
        if (_core.types.isMemberExpression(parent)) return;
        if (!hasMapping(BuiltIns, name)) return;
        if (scope.getBindingIdentifier(name)) return;
        path.replaceWith(this.addDefaultImport(`${modulePath}/${corejsRoot}/${BuiltIns[name].path}`, name));
      ...
      

之前寫過一篇文章介紹過babel的源碼,然後最後還自定義了一個插件,babel源碼解析一,插件返回的就是一個ast節點遍歷的鉤子函數,也就是說babel在遍歷每一個節點的時候會觸發對應插件的鉤子函數,也就是說當解析到"src/demo.runtime.js"中的這段代碼的時候:

new Promise(function () {});

會走上面runtime插件的ReferencedIdentifier方法,然後把當前節點傳過來,

... 
visitor: {
      ReferencedIdentifier(path) {
        const {
          node,
          parent,
          scope
        } = path;
        const {
          name
        } = node;
				//如果有generator函數並且useRuntimeRegenerator設置爲true的時候就添加generatorRuntime的polyfill,
        if (name === "regeneratorRuntime" && useRuntimeRegenerator) {
          path.replaceWith(this.addDefaultImport(`${modulePath}/regenerator`, "regeneratorRuntime"));
          return;
        }
				//corejs爲false就不添加polyfill直接返回
        if (!injectCoreJS) return
        if (_core.types.isMemberExpression(parent)) return;
        //看當前corejs中有沒有“Promise”的墊片polyfill
        if (!hasMapping(BuiltIns, name)) return;
        if (scope.getBindingIdentifier(name)) return;
	//添加promise polyfill路徑爲
  //"@babel/runtime-corejs3/core-js-stable/promise"       
                path.replaceWith(this.addDefaultImport(`${modulePath}/${corejsRoot}/${BuiltIns[name].path}`, name));
      ...
      

可以看到,如果有generator函數並且“useRuntimeRegenerator”設置爲“true”的時候就添加generatorRuntime的polyfill,“useRuntimeRegenerator”選項我們下面再說,然後當corejs選項不爲false的時候就按照前面說的路徑去添加“Promise”的polyfill代碼。

helpers& useESModules

helpers: boolean, defaults to true.

是否運行runtime插件添加babel的helpers函數,比如我們的classCallCheck、extends方法等等,默認是開啓的。

useESModules: boolean, defaults totrue`.

是否在添加esm方式的helpers函數的時候,默認是根據babel的配置來選擇。

我們測試一下這兩個參數,

src/demo.runtime.js:

const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                corejs: {version: 3, proposals: true},
                helpers: true,
                useESModules: false
            }
        ]
    ]
};

運行看結果,

lib/demo.runtime.js:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

...

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
...

可以看到,當helpers爲true然後useESModules爲false的時候會添加一些helper函數,比如我們的createClass跟classCallCheck等等,都是從corejs3的helpers目錄下直接取模塊,如果我們把useESModules設置爲true,我們看一下效果,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                corejs: {version: 3, proposals: true},
                helpers: true,
                useESModules: true
            }
        ]
    ]
};

運行代碼看效果,

lib/demo.runtime.js:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/esm/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/esm/createClass"));

...

可以看到,useESModules設置爲true的時候會從helpers的esm目錄加載對應的模塊,這就是useESModules配置的作用。

下面我們分析一下源碼,

packages/babel-plugin-transform-runtime/src/index.js:

 return {
    name: "transform-runtime",

    pre(file) {
      //是否開啓了helpers選項
      if (useRuntimeHelpers) {
        file.set("helperGenerator", name => {
     			//看當前helper是否在可用
          if (
            file.availableHelper &&
            !file.availableHelper(name, runtimeVersion)
          ) {
            return;
          }
			
          const isInteropHelper = HEADER_HELPERS.indexOf(name) !== -1;

          
          const blockHoist =
            isInteropHelper && !isModule(file.path) ? 4 : undefined;
					//根據useESModules配置選擇加載helper的目錄
          //useESModules: false(默認爲helpers)
          //useESModules: true(helpers/esm)
          const helpersDir =
            esModules && file.path.node.sourceType === "module"
              ? "helpers/esm"
              : "helpers";

          return this.addDefaultImport(
            `${modulePath}/${helpersDir}/${name}`,
            name,
            blockHoist,
          );
        });
      }

      const cache = new Map();

      this.addDefaultImport = (source, nameHint, blockHoist) => {
        // If something on the page adds a helper when the file is an ES6
        // file, we can't reused the cached helper name after things have been
        // transformed because it has almost certainly been renamed.
        const cacheKey = isModule(file.path);
        const key = `${source}:${nameHint}:${cacheKey || ""}`;

        let cached = cache.get(key);
        if (cached) {
          cached = t.cloneNode(cached);
        } else {
          cached = addDefault(file.path, source, {
            importedInterop: "uncompiled",
            nameHint,
            blockHoist,
          });

          cache.set(key, cached);
        }
        return cached;
      };
    },

OK!代碼中有註釋,我就不詳細說明了。

regenerator

boolean, defaults to true.

是否開啓添加regenerator函數的polyfill防止全局污染。

描述不是很好理解哈,別怕,我們結合demo跟源碼來分析,首先我們把regenerator選項關閉(false),然後看一下我們demo中的編譯情況,

src/demo.runtime.js:

const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}

可以看到,我們源代碼中有一個generator函數叫“helloWorldGenerator”,然後我關閉regenerator

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                corejs: {version: 3, proposals: true},
                helpers: true,
                useESModules: true,
                regenerator: false
            }
        ]
    ]
};

運行看結果,

lib/demo.runtime.js:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/esm/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/esm/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/promise"));

var _context;

var _marked = /*#__PURE__*/regeneratorRuntime.mark(helloWorldGenerator);

var fn = function fn() {};

new _promise.default(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    (0, _classCallCheck2.default)(this, Test);
  }

  (0, _createClass2.default)(Test, [{
    key: "say",
    value: function say() {}
  }]);
  return Test;
}();

var c = (0, _includes.default)(_context = [1, 2, 3]).call(_context, 1);
var a = 10;

function helloWorldGenerator() {
  return regeneratorRuntime.wrap(function helloWorldGenerator$(_context2) {
    while (1) {
      switch (_context2.prev = _context2.next) {
        case 0:
          _context2.next = 2;
          return 'hello';

        case 2:
          _context2.next = 4;
          return 'world';

        case 4:
          return _context2.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context2.stop();
      }
    }
  }, _marked);
}

可以看到,我們的helloWorldGenerator函數被preset-env改造過後變成了:


var _marked = /*#__PURE__*/regeneratorRuntime.mark(helloWorldGenerator);
function helloWorldGenerator() {
  return regeneratorRuntime.wrap(function helloWorldGenerator$(_context2) {
    while (1) {
      switch (_context2.prev = _context2.next) {
        case 0:
          _context2.next = 2;
          return 'hello';

        case 2:
          _context2.next = 4;
          return 'world';

        case 4:
          return _context2.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context2.stop();
      }
    }
  }, _marked);
}

由於preset-env沒有開啓polyfill選項,然後runtime插件又關閉了regenerator選項,所以我們的regeneratorRuntime對象並沒有被注入,所以我們打開我們的regenerator選項再試試,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                corejs: {version: 3, proposals: true},
                helpers: true,
                useESModules: true,
                regenerator: true
            }
        ]
    ]
};

運行看效果,

demo.runtime.js:

...
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
var _marked = /*#__PURE__*/_regenerator.default.mark(helloWorldGenerator);
function helloWorldGenerator() {
  return _regenerator.default.wrap(function helloWorldGenerator$(_context2) {
    while (1) {
      switch (_context2.prev = _context2.next) {
        case 0:
          _context2.next = 2;
          return 'hello';

        case 2:
          _context2.next = 4;
          return 'world';

        case 4:
          return _context2.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context2.stop();
      }
    }
  }, _marked);
}
...

可以看到,當開啓了regenerator選項的時候,runtime會自動的注入一個_regenerator對象,用來替換我們之前的regeneratorRuntime對象,並且不會像preset-env一樣會污染全局,

以下是“preset-env”添加的regenerator polyfill

require("regenerator-runtime/runtime");

var _marked = /*#__PURE__*/regeneratorRuntime.mark(helloWorldGenerator);
function helloWorldGenerator() {
  return regeneratorRuntime.wrap(function helloWorldGenerator$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'hello';

        case 2:
          _context.next = 4;
          return 'world';

        case 4:
          return _context.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

ok!我們的regenerator參數就講到這裏了,下面我們看一下源碼中的操作,

packages/babel-plugin-transform-runtime/src/index.js:

visitor: {
      ReferencedIdentifier(path) {
        const { node, parent, scope } = path;
        const { name } = node;

        // transform `regeneratorRuntime`
        if (name === "regeneratorRuntime" && useRuntimeRegenerator) {
          path.replaceWith(
            this.addDefaultImport(
              `${modulePath}/regenerator`,
              "regeneratorRuntime",
            ),
          );
          return;
        }

可以看到,源碼中當讀到"regeneratorRuntime"變量的時候,就替換掉"regeneratorRuntime"變量改爲以下代碼:

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
var _marked = /*#__PURE__*/_regenerator.default.mark(helloWorldGenerator);

absoluteRuntime

boolean or string, defaults to false.

設置runtime插件從哪個目錄導入helpers跟polyfill,默認是:@babel/runtime-corejs3、@babel/runtime-corejs2或者@babel/runtime,你也可以設置其它的路徑,我們用一下看效果:

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                corejs: {version: 3, proposals: true},
                helpers: true,
                useESModules: true,
                regenerator: true,
                absoluteRuntime: "./node_modules"
            }
        ]
    ]
};

運行看效果:

lib/demo.runtime.js

"use strict";

var _interopRequireDefault = require("xxx/babel-demo/node_modules/@babel/runtime-corejs3/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("xxx/babel-demo/node_modules/@babel/runtime-corejs3/regenerator"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/includes"));

var _classCallCheck2 = _interopRequireDefault(require("xxx/babel-demo/node_modules/@babel/runtime-corejs3/helpers/esm/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("xxx/babel-demo/node_modules/@babel/runtime-corejs3/helpers/esm/createClass"));

var _promise = _interopRequireDefault(require("xxx/babel-demo/node_modules/@babel/runtime-corejs3/core-js/promise"));

var _context;

var _marked = /*#__PURE__*/_regenerator.default.mark(helloWorldGenerator);

var fn = function fn() {};

new _promise.default(function () {});

var Test = /*#__PURE__*/function () {
  function Test() {
    (0, _classCallCheck2.default)(this, Test);
  }

  (0, _createClass2.default)(Test, [{
    key: "say",
    value: function say() {}
  }]);
  return Test;
}();

var c = (0, _includes.default)(_context = [1, 2, 3]).call(_context, 1);
var a = 10;

function helloWorldGenerator() {
  return _regenerator.default.wrap(function helloWorldGenerator$(_context2) {
    while (1) {
      switch (_context2.prev = _context2.next) {
        case 0:
          _context2.next = 2;
          return 'hello';

        case 2:
          _context2.next = 4;
          return 'world';

        case 4:
          return _context2.abrupt("return", 'ending');

        case 5:
        case "end":
          return _context2.stop();
      }
    }
  }, _marked);
}

可以看到,大部分的polyfill跟helpers函數都變成“xxx/babel-demo/node_modules/@babel/runtime-corejs3/xxx”,也就是說runtime插件可以讓用戶指定truntime依賴的位置,轉換過後就變成一個絕對路徑了。

version

runtime中corejs的版本,比如現在我們的@babel/runtime-corejs2的7.0.1之前是沒有Math的一些方法的,那麼如果你的version值設置的是<=7.0.0的時候runtime插件就不會Math的一些方法給加進來的。

packages/babel-plugin-transform-runtime/src/index.js:

  const { BuiltIns, StaticProperties, InstanceProperties } = (injectCoreJS2
    ? getCoreJS2Definitions
    : getCoreJS3Definitions)(runtimeVersion);

packages/babel-plugin-transform-runtime/src/runtime-corejs2-definitions.js:

export default runtimeVersion => {
  // Conditionally include 'Math' because it was not included in the 7.0.0
  // release of '@babel/runtime'. See issue https://github.com/babel/babel/pull/8616.
  ...
  const includeMathModule = hasMinVersion("7.0.1", runtimeVersion);
	 ...(includeMathModule
        ? {
            Math: {
              acosh: { stable: true, path: "math/acosh" },
              asinh: { stable: true, path: "math/asinh" },
              atanh: { stable: true, path: "math/atanh" },
              cbrt: { stable: true, path: "math/cbrt" },
              clz32: { stable: true, path: "math/clz32" },
              cosh: { stable: true, path: "math/cosh" },
              expm1: { stable: true, path: "math/expm1" },
              fround: { stable: true, path: "math/fround" },
              hypot: { stable: true, path: "math/hypot" },
              imul: { stable: true, path: "math/imul" },
              log10: { stable: true, path: "math/log10" },
              log1p: { stable: true, path: "math/log1p" },
              log2: { stable: true, path: "math/log2" },
              sign: { stable: true, path: "math/sign" },
              sinh: { stable: true, path: "math/sinh" },
              tanh: { stable: true, path: "math/tanh" },
              trunc: { stable: true, path: "math/trunc" },
            },
          }
        : {}),  
     ...
}

OK,我們的@babel/plugin-transform-runtime全部內容就已經解析完畢了。

demo項目

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章