每日一报
JS 工具集合
性能比较
JS 面试题
input 搜索如何中文输入
看过 element ui 框架源码的童鞋都应该知道,element ui是通过 compositionstart 和 compositionend 事件来做中文输入处理。
那么介绍一下 compositionstart 和 compositionend 两个事件触发的时机吧。
compositionstart: 事件在用户开始进行非直接输入的时候触发。compositionend:事件在非直接输入结束,也即用户点选候选词或者点击「选定」按钮之后,比如按回车键时触发。
<input
value={innerValue}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onInput={handleInput}
/>由于输入中文需要打开输入法,在开始编辑所需中文句子的时候会触发 compositionstart 事件,编辑完所需的中文句子时(即按下确认键)会触发 compositionend 事件。那么我们通过在这两个事件中做标记就可以知道输入完中文的确切时间。
const isInputting = useRef(false);
const [innerValue, setInnerValue] = useState<string | number>(0)
const handleCompositionStart = useCallback(() => {
console.log('handleCompositionStart');
isInputting.current = true;
}, []);
const handleCompositionEnd = useCallback(() => {
console.log('handleCompositionEnd');
isInputting.current = false;
}, []);
/**
* onInput 事件在用户输入的时候会一直处于触发状态。
*
* 在 React 中,onChange 事件会随着用户输入不断触发,主要原因在于 React 对输入框的处理方式与浏* 览器原生处理方式的不同。
*/
const handleInput = useCallback((a: React.ChangeEvent<HTMLInputElement>) => {
if (!isInputting.current) {
console.log('handleChange');
}
setInnerValue(a.currentTarget.value);
}, []);深究 JavaScript 数组
JavaScript 的数组通过哈希映射或者字典的方式来实现,所以不是连续的。我觉得这是一门劣等语言:连数组都不能正确的实现。
在众多编程语言中,数组定义为在内存中用一串 连续 的区域来存放一些值。而在 JavaScript 中,数组是哈希映射。它可以通过多种数据结构实现,其中一种是链表。
JavaScript 引擎已经在为同种数据类型的数组分配连续的存储空间了。优秀的开发者总是保持数组的数据类型一致,这样即时编译器 (JIT) 就能像 C 编译器一样通过计算读取数组了。但是,如果你想在同种类型的数组中插入不同类型的元素,JIT 会销毁整个数组然后用以前的办法重建。
在 ES2015/ES6 中, 数组还有其它改进。 TC39 决定在 JavaScript 中引入类型化数组,所以如今我们有 ArrayBuffer 了。ArrayBuffer 会有一大块连续的存储位置,你能用它做任何你想做的事情。不过,直接处理内存涉及非常底层的操作,相当复杂。可以通过 Views 来处理 ArrayBuffer。
var buffer = new ArrayBuffer(8);
var view = new Int32Array(buffer);
view[0] = 100;你还可以使用 SharedArrayBuffer 在多个 web-workers 间共享内存数据来提升性能。
我们可以测试对比以下两种写法方式
写法一:
var LIMIT = 10000000;
var arr = new Array(LIMIT);
console.time('Array insertion time');
for (var i = 0; i < LIMIT; i++) {
arr[i] = i;
}
console.timeEnd('Array insertion time');写法二:
var LIMIT = 10000000;
var buffer = new ArrayBuffer(LIMIT * 4);
var arr = new Int32Array(buffer);
console.time('ArrayBuffer insertion time');
for (var i = 0; i < LIMIT; i++) {
arr[i] = i;
}
console.timeEnd('ArrayBuffer insertion time');__esModule 的用途
ES模块和CommonJS模块的兼容性:__esModule 标识主要用于解决
ES模块和CommonJS模块之间的兼容性问题。当使用打包工具(如Webpack、Rollup等)将ES模块转换为CommonJS模块时,会添加这个标识。模块类型识别: 通过 __esModule 标识,运行时环境可以判断一个模块是原生的
CommonJS模块还是由ES模块转换而来的。这对于正确处理默认导出和命名导出非常重要。默认导出的处理: 在
ES模块中,可以使用export default语法。但在CommonJS中没有直接对应的概念。通过 __esModule 标识,可以正确地模拟ES模块的默认导出行为。导入行为的一致性: 当其他模块导入一个带有 __esModule 标识的模块时,可以保持与导入原生
ES模块时相似的行为,尤其是在处理default import和namespace import。跨环境兼容: 这个标识使得同一份代码可以在支持 ES 模块的环境和只支持 CommonJS 的环境中都能正常工作,增强了代码的可移植性。
交互操作(Interoperability): __esModule 标识不仅帮助识别模块类型,还促进了不同模块系统间的交互操作。它允许使用 CommonJS 语法的代码可以无缝地使用转换后的 ES 模块,反之亦然。
运行时行为模拟: 在某些情况下,打包工具会生成额外的代码来模拟 ES 模块的运行时行为。例如,处理循环依赖时,ES 模块和 CommonJS 模块的行为是不同的。__esModule 标识有助于正确模拟这些行为差异。
静态分析支持: 虽然 __esModule 主要用于运行时,但它也为静态分析工具提供了有用的信息。这些工具可以利用这个标识来更准确地分析和优化代码。
版本兼容性: 随着 JavaScript 生态系统的发展,__esModule 标识帮助新旧版本的库和工具保持兼容性,使得渐进式升级成为可能。
打包工具的实现差异: 不同的打包工具可能会以略微不同的方式实现 __esModule 标识。例如,有些工具可能会使用 Symbol 而不是简单的布尔值来避免潜在的命名冲突。
性能考虑: 虽然添加 __esModule 标识会略微增加代码体积,但它带来的兼容性收益通常远大于这个小小的性能开销。
未来发展: 随着 JavaScript 模块系统的不断发展,__esModule 的作用可能会逐渐减少。然而,目前它仍然是确保跨环境兼容性的重要机制。
这里有一个更复杂的例子来说明 __esModule 的作用:
// 假设这是一个 ES 模块
// myComplexModule.js
export default class MyClass {
constructor() {
this.name = 'Default';
}
}
export function helper() {
return 'Helper function';
}
// 这可能会被转换为:
('use strict');
Object.defineProperty(exports, '__esModule', { value: true });
class MyClass {
constructor() {
this.name = 'Default';
}
}
exports.default = MyClass;
function helper() {
return 'Helper function';
}
exports.helper = helper;
// 使用时:
const myModule = require('./myComplexModule');
if (myModule.__esModule) {
// 这是一个转换后的 ES 模块
const MyClass = myModule.default;
const { helper } = myModule;
} else {
// 这是一个原生 CommonJS 模块
const MyClass = myModule;
const helper = myModule.helper;
}这个例子展示了如何根据 __esModule 标识来正确处理默认导出和命名导出,确保模块在不同环境中的一致性使用。
__esModule 总结
为构建工具提供标识位,告知构件工具当前的 CommonJS 模块为 ESM 转译过来的,那么在处理 default import 和 namespace import 的时候,构建工具可以通过 __esModule 标记位来做处理,本质上就是做一层 CommonJS 的默认值处理。
import defaultValue from 'commonjs';
// => 最早按照如下方式转译,但是由于 CommonJS 没有默认导出,那么转译为 ESM 就会存在问题。
const m = require('commonjs');
const defaultValue = m.default;
/**
* => __ESM 核心尊重 __esModule,根据 __esModule 的值来判断 default 的值。
* __ESM 等价于 m && m.__esModule ? m : { ...m, default: m }
*/
const m = __ESM(require('commonjs'));
const defaultValue = m.default;esModuleInterop 的用途
原因
问题一:NameSpace Import 处理异常
由于历史原因,
TypeScript编译器默认将ESM(ECMAScript 模块)语法编译为CommonJS语法。例如:import * as foo from 'foo'被编译为const foo = require('foo')。可能是因为在TypeScript接受该语法时,ESM仍然是一个提案。但是,这种转译方式是存在问题的,与node或其他平台处理ESM逻辑不符。js// foo module module.exports = 'foo'; // main module import * as foo from 'foo'; // tsx main module const foo = require('foo');针对以上例子,
ts转译就会发生异常现象。require函数可以返回任何JavaScript值,包括 字符串,但namespace import语法总是生成 对象,并且不能生成 字符串。此时通过TS转译后的CommonJS语法就会与原ESM语法规范不一致问题。问题二:Default Import 处理异常
CommonJS并不像ESM拥有所谓的default import。任何导出的变量在CommonJS看来都是module.exports这个对象上的属性。js// CommonJS module.exports = {}; const foo = require('foo'); // ESM export default {}; export const foo = 'foo'; import defaultImport, { foo } from 'foo'; console.log(defaultImport, foo);可以发现
TypeScript将ESM转译为CommonJS时,会通过Object.defineProperty(exports, "__esModule", { value: true });来标记原模块为ESM。ESM的default export会被转译为exports.default属性值,同时ESM的named default会被转译为exports对象上的属性。
TypeScript 提供了 esModuleInterop 配置项来解决上述问题,通过生成额外的 helper code 来兼容 ESM 转译为 CommonJS 时语意不一致问题。该选项默认未启用,因为对现有 TypeScript 项目来说这将是一个破坏性变更。但 Microsoft 强烈建议议对新旧项目都应用此配置(然后更新代码),以便更好地与生态系统其他部分兼容。
helper code(协助函数) 做了什么
tsconfig.json 配置项:
{
"include": ["src/main.ts"],
"compilerOptions": {
"lib": ["ES2023", "DOM"],
"outDir": "ts-dist",
"esModuleInterop": true
}
}执行程序:
// main.ts:
import foo from './foo.js';
import * as namespaceImport from './foo.js';
foo();
console.log(namespaceImport);
// foo.js
module.exports = 'foo';TSX 转译后:
'use strict';
var __createBinding =
(this && this.__createBinding) ||
(Object.create
? function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (
!desc ||
('get' in desc ? !m.__esModule : desc.writable || desc.configurable)
) {
desc = {
enumerable: true,
get: function () {
return m[k];
}
};
}
Object.defineProperty(o, k2, desc);
}
: function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
});
var __setModuleDefault =
(this && this.__setModuleDefault) ||
(Object.create
? function (o, v) {
Object.defineProperty(o, 'default', { enumerable: true, value: v });
}
: function (o, v) {
o['default'] = v;
});
var __importStar =
(this && this.__importStar) ||
function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null)
for (var k in mod)
if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k))
__createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, '__esModule', { value: true });
var foo_js_1 = __importDefault(require('./foo.js'));
var namespaceImport = __importStar(require('./foo.js'));
(0, foo_js_1.default)();
console.log(namespaceImport);从上述转译结果可以发现 esModuleInterop 使用了 __importDefault 协助函数来处理 default import,同时使用 __importStar 协助函数来处理 namespace import。
__importDefault 协助函数本质上就是给原 CommonJS 模块做了一层封装,以 { "default": mod } 形式导出,调用方执行 mod.default。
__importStar 协助函数做的事情就是创建一个新对象,新对象中包含原 CommonJS 导出的非 原型链 上的非 default 属性,同时 default 属性赋值为原 CommonJS 的导出(module.exports)。确保通过 namespace import 获取到的引用必定为对象,兼容 ESM。
注意
__importDefault 和 __importStar 两个协助函数对于 原ESM 模块(通过 __esModule 为真做判断)不做任何处理,直接返回。
# esm 导入 esm
两边默认情况下都会被转为 **CommonJS**
严格按照 **esm** 的标准写,一般不会出现问题
# esm 导入 cjs
历史原因导致的兼容问题
# cjs 导入 esm
一般不会这样使用
# cjs 导入 cjs
不会被编译器处理
严格按照 **cjs** 的标准写,不会出现问题总结
由于历史遗留问题,TypeScript 默认会将模块转译为 CommonJS。若 ESM 模块依赖了 CommonJS 模块,那么转译后的 default import 和 namespace import 就会存在问题。TypeScript 默认情况下并没有遵循 ESM 规范,esModuleInterop 配置项目的就是为了解决这个问题,通过额外注入 helper 的协助函数来兼容 ESM。
一句话:TypeScript 默认情况下不遵循 ESM 规范,esModuleInterop 配置项就是为了兼容 ESM 而提出的。
注意
babel 默认的转译规则和 TS 开启 esModuleInterop 的情况差不多,也是通过两个 helper 函数来处理的。
vite、Rollup、Webpack 等构件工具并没有借助 TS 来做 ESM 和 CommonJS 之间的 interop,他们均有自己的一套处理机制来处理这个兼容问题。
对于 Vite 项目来说,Vite 本身已经很好地处理了与 CommonJS 模块的交互性,因此通常不需要在 TypeScript 中配置 esModuleInterop 来进行额外的转译。
Vite 使用的是现代的打包工具(Rollup 和 Esbuild),这些工具本身就支持 ES 模块和 CommonJS 模块的互操作性。需要注意的是,Vite 处理 CommonJS 和 ESM 之间的 interop 与其他两个构建工具处理 interop 的最终产物效果是一致的。小细节会存在一些不一致,Esbuild 处理 CommonJS 和 ESM 之间的 interop 会先将导入到 ESM 模块的 CommonJS 模块转译为类似于 namespace import 的导入,再取其指定值。
// demo.cjs
module.exports = {
a: 12134,
__esModule: true,
default: {
b: 214214
}
};// main.mjs
import * as namespaceImport from './demo.cjs';
import defaultImport, { a } from './demo.cjs';
console.log(a, defaultImport, namespaceImport);执行 esbuild main.mjs --bundle --outfile=out.mjs 后获得 ESM 产物如下:
// out.mjs
var w = __toESM(require_demo());
var namespaceImport = __toESM(require_demo());
var import_demo = __toESM(require_demo());
console.log(import_demo.a, import_demo.default, namespaceImport);可以看出此时在 ESM 模块中使用 helper 函数(__toESM)来做 interop。__toESM 实现核心就是尊重 __esModule(m && m.__esModule ? m : { ...m, default: m }),将加载的 CommonJS 模块转译为 Namespace Import 的值,然后再取其具体的值(默认导入取 default 值、具名导入取具体的属性值)。这一点和 Vite 不一样,Esbuild 会将 CommonJS 包转译为 ESM 模块,其中 CommonJS 的 module.exports 值等价于 ESM 的 export default 值。Vite 加载 Esbuild 构建过的 ESM 模块后再做 interop,如下:
// 先加载 Esbuild 编译后的 ESM 模块
import __vite__cjsImport0_demo from '/node_modules/.vite/deps/demo.js?v=c8ba3f3d';
// 再做 ESM 加载 CommonJS 的 interop
const foo = ((m) =>
m?.__esModule
? m
: {
...((typeof m === 'object' && !Array.isArray(m)) ||
typeof m === 'function'
? m
: {}),
default: m
})(__vite__cjsImport0_demo);即使 Vite 和 Esbuild 中间实现 interop 的小细节存在不一致,但最终获取的产物均尊重 __esModule,其保持一致。
还有一点需要注意的是 Esbuild 会将 CommonJS 的 module.exports 的值转译为 ESM 的 export default,可参考。这里就需要注意一点 Esbuild 的转译
// main.js
export const a = 132;
export default {
b: 456
}
// output { format: 'cjs' }
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var stdin_exports = {};
__export(stdin_exports, {
a: () => a,
default: () => stdin_default
});
// { a: 132, default: { b: 456 }}
module.exports = __toCommonJS(stdin_exports);
const a = 132;
var stdin_default = {
b: 456
};
// cjs => esm
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var require_stdin = __commonJS({
"<stdin>"(exports, module) {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames2(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var stdin_exports = {};
__export(stdin_exports, {
a: () => a,
default: () => stdin_default
});
module.exports = __toCommonJS(stdin_exports);
const a = 132;
var stdin_default = {
b: 456
};
}
});
// export default { a: 132, default: { b: 456 }, __esModule: true }
export default require_stdin();可以看到 Esbuild 转译 ESM 为 CommonJS 会将 ESM 中的 export default 转译为 module.exports 的 default 属性。但是反之将原 ESM 的 CommonJS 模块转译为 ESM 模块则完全不一样,直接将原 ESM 的 CommonJS 的 module.exports 赋值为转译后的 ESM 模块的 export default。也就是说若导入由 Esbuild 编译的 ESM 模块需要做一层 interop,这也就是 Vite 现阶段在预构建期间所要做的事情。
当然对于 Vite 来说,也并不需要 Esbuild 将额外进行处理,Vite 内部会进一步对预构建产物做一层 interop,此时 Vite 会尊重 __esModule 属性,做到 ESM 模块加载 CommonJS 模块的兼容性。
所以 TypeScript 中的 esModuleInterop 配置项对于构建工具来说并不是很重要。因此,除非你的项目有特别的需求(如某些特定情况下的类型检查问题),在 Vite 项目中一般不需要启用 esModuleInterop。Vite 会通过它的构建流程自动处理这些兼容性问题,使得你可以专注于使用现代 ECMAScript 模块系统。
