likes
comments
collection
share

【js 知识】Commonjs 和 Es Module 的对比

作者站长头像
站长
· 阅读数 38

前言

今天简单学习下模块化的知识,准备跳槽面试的同学可以看起来了,基础打牢,啥都不怕了

模块化

  • 模块化的意义是将程序划分成一个一个小的模块结构,每个模块有属于自己小世界,不会影响到其他的结构
  • 没有模块化可能造成全局污染,引入多个 js 文件,可能出现同样命名的方法
  • 没有模块化,依赖管理也是一个难以处理的问题

commonjs

  • 模块同步加载并执行模块文件,拷贝一份值出来

common 变量

  • module 当前模块信息
  • require 引入模块的方法
  • exports 导出模块的属性

require

模块导入规则
  • ./ 和 ../ 相对路径的文件模块, / 绝对路径的文件模块,会被当作文件模块处理,require() 方法会将路径转换成真实路径

  • 没有路径开头则视为导入一个包

    • fs、http、path 等标识符,为 nodejs 的核心模块,将被优先处理
    • 不是核心模块 会从当前文件的同级目录的 node_modules 寻找 -> package.json 下 main 属性指向的文件 -> index.js ,index.json ,index.node
    • 如果再没有,在父级目录的 node_modules 查找,一直递归到根目录下的 node_modules,直到找不到为止结束
缓存
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存

  • 每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性

  • 代码分析

function require(id) {
  const cachedModule = Module._cache[id];
  // 模块是否被加载过,加载过则取出来走缓存
  if (cachedModule) {
    return cachedModule.exports;
  }
  const module = { exports: {} ,loaded: false , ...}
  // 对模块进行缓存
  Module._cache[id] = module
  // 这一步执行文件内容
  // 为 true 则加载完成,返回对象
  module.loaded = true
  return module.exports
}
循环引用
// a 文件
exports.a = 1;
const b = require('./b');
console.log(b);
exports.a = 2;
// b 文件
exports.b = 3;
const a = require('./a');
console.log(a);
exports.b = 4;
// app 文件
const a = require('./a');
console.log(a);
  • 代码流程分析
    1. 执行 app.js 代码,获取 require('./a') -> 无缓存 ->加入缓存 -> 执行模块
    2. 执行 require('./b') -> 无缓存 -> 加入缓存执行模块,进行 b -> require('./a') -> 有缓存,直接读取 -> 回到 b.js
    3. 打印 { a: 1 } -> 执行 exports.b = 4,修改 b 等于 4
    4. 回到 a.js -> b 获取完毕,打印 { b: 4 } -> 执行 exports.a = 2,修改 a 为 2
    5. 回到 app.js -> 获取 a 完毕,输出 { a: 2 }

module.exports 和 exports

exports
  • 正确写法
// a 模块
exports.name = 'breeze';
exports.age = '18';
exports.fun = function () {
  console.log('bao');
};

// app.js 文件引用
const a = require('./a');
console.log(a); // { name: 'breeze', age: '18', fun: [Function] }
  • 错误写法:不能直接赋值
// 上文 a.js 修改成如下写法
exports = {
  name: 'breeze',
  age: '18',
  fun: function () {
    console.log('bao');
  },
};

// app.js 文件引用
const a = require('./a');
console.log(a); // { }
  • 错误原因:exports、module、require 都是通过形参的方式传入到模块中,如果直接 exports 新赋值一个对象的话,改变了引用类型的地址
  • 模拟一下函数的流程
// js 代码中写的内容
const script = `
  const a = require('./a');
  module.exports = function say() {
    return {
      a: a,
    };
  };`;

// 通过参数的形式传入函数,所以不能改变引用对象的地址,否则只能输出为空
function wrapper(script) {
  return '(function (exports, require, module, __filename, __dirname) {' + script + '\n})';
}

// 执行包装函数
const modulefunction = wrapper(script);
module.exports
  • 和 exports 是同一个引用地址

  • 支持的写法,可以放在一个对象里

module.exports = {
  name: 'breeze',
  age: '18',
  fun: function () {
    console.log('bao');
  },
};
  • 不同的是 module.exports 可以直接赋值,不导出对象,以下情况生效
module.exports = '1';
module.exports = [1, 2, 3];
module.exports = function () {}; //导出方法

Es Module

  • 提前加载并执行模块文件
  • 在预处理阶段分析依赖关系,在执行阶段执行模块(深度优先遍历,执行顺序先子后父)
  • ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块

import、export

  • 引入和导出都是静态的,import 自动提升至顶层

  • import、export 不能放在块级作用域或条件语句中

  • 不可以在导入的文件中修改,可以在导出的文件中操作,可以理解为无论是否基本类型都是进行的引用传递

// a 文件
export let count = 1export addCount = () => {
  count++
}

// app 文件
import { count, addCount } from './a'
count = 2 // 报错,不可以修改
addCount() // 可以正常修改

import()

  • import() 返回 Promise 对象
  • 可以进行动态使用
// a 文件
export name = 'breeze';
export default function fun () {
  console.log('bao')
}

// app 文件
setTimeout(() => {
  import('./a').then(res=>{
    console.log(res) // { name: 'breeze', default: f fun() }
  })
  res
}, 0);
  • 经常使用于路由加载:react 路由懒加载
import React, { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const dynamicImport = (dirName: string) => {
  const Module = lazy(() => import(`../pages/${dirName}`)); // 动态加载
  return <Module />;
};

export const Router = () => {
  return (
    <Routes>
      <Route path="/todo" element={dynamicImport('todo')} />
    </Routes>
  );
};

export default () => {
  return (
    <Suspense fallback={loading}>
      <Router />
    </Suspense>
  );
};

tree shaking

  • tree shaking 原理就是利用的 es6 的 import、export,用来尽可能的删除没有被使用过的代码,不进行打包
// a 文件
export count = 1;
export const addCount = () => {
  count++
}
export const reduceCount = () => {
  count--
}

// app 文件
import { addCount } from './a'
addCount()
  • 以上引用 reduceCount 将不被打包,因为并没有使用到

两者区别

  • CommonJs 的导出是拷贝一份变量出来,ES6 Module 导出的是变量是引用类型的,不能随意更改
  • CommonJs 是可以写在判断里的动态语法,ES6 Module 需要先进行静态分析
  • CommonJs 只能导出单个值,ES6 Module 导出的值数量不限制
转载自:https://juejin.cn/post/7139669780184694821
评论
请登录