import 和 require 加载机制详解

486 阅读3分钟

一、 import 和 require加载情况对比

  1. import的加载
// 1-1.js

export let counter = 3; 

export function add() { 

    counter++; 

}



// main.js 

import { counter, add} from './1-1.js';

console.log(counter);    // 3

//counter++;

add();

console.log(counter);    //4



//babel-node main.js的执行结果:

 //3

//4

结论:模块加载命令import生成的是一个只读引用。

注意: 上面运行需要babel的支持。

  1. require的加载

下面这个模块文件lib.js的例子。

// main.js

var mod = require('./1-2');

console.log(mod.counter);   // 3

mod.add();

console.log(mod.counter);   // 3



// 1-2.js

var counter = 3;

function add() {

  counter++;

}

module.exports = {

  counter: counter,

  add: add,

};





//node main.js的执行结果:

 //3

//3

结论: CommonJS模块输出的是值的浅拷贝。也就是说,一旦输出module.exports,模块内部的变化就影响不到这个值。

以取值器函数的形式获取

// main.js

var mod = require('./1-3');

console.log(mod.counter);   // 3

mod.add();

console.log(mod.counter);   // 4



// 1-3.js

var counter = 3;

function add() {

  counter++;

}

module.exports = {

  get counter() {   //取值器函数

    return counter

  },

  add: add,

};



//node main.js的执行结果:

 //3

//4
  1. ES6模块与CommonJS模块的差异
  • CommonJS模块输出的是值的浅拷贝 ( 是module.exports浅拷贝了脚本相关变量 ),ES6模块输出的是值的只读引用。
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口。
  • CommonJS模块 require初次加载一个模块时候必然是阻塞的,初次加载之后会被缓存,所以加载之后再require同一个模块不会被执行,从缓存里面取值。
  • CommonJS输出的是一个对象(即module.exports),该对象只有在脚本运行结束时才会生成。而ES6模块输出的不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

ES module 原理详情见 : www.jianshu.com/p/ba0faf79c…

二、循环加载

  1. 概念

“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

  1. ES6模块的循环加载
// a.js 

import {bar} from './b.js'; 

console.log('a.js'); 

console.log(bar); 

export let foo = 'foo'; 



// b.js 

import {foo} from './a.js'; 

console.log('b.js'); 

console.log(foo); 

export let bar = 'bar'; 



//babel-node a.js的运行结果: 

//b.js 

//undefined 

//a.js

//bar 

分析:

  1. 上面的代码中,由于a.js的第一行是加载b.js,所以先执行的是b.js。而b.js的第一行又是加载a.js,这时由于a.js已经开始执行,所以不会重复执行,而是继续执行b.js,因此第一行输出的是b.js。
  2. 接着,b.js要打印变量foo,这时a.js还没有执行完,取不到foo的值,因此打印出来的是undefined。b.js执行完便会开始执行a.js,这时便会一切正常。

结论: 上面的代码中,a.js之所以能够执行,原因就在于ES6加载的变量都是动态引用其所在模块的。只要引用存在,代码就能执行。

  1. CommonJS模块的循环加载
//a.js

exports.done = false;

var b = require('./b.js'); 

console.log('在 a.js 之中,b.done = %j', b.done);   //3

exports.done = true; 

console.log('a.js 执行完毕');   //4



//b.js

exports.done = false; 

var a = require('./a.js'); 

console.log('在 b.js 之中,a.done = %j', a.done);   //1

exports.done = true; 

console.log('b.js 执行完毕');    //2



//ab.js

var a = require('./a.js'); 

var b = require('./b.js');   //从缓存中取

console.log('在 ab.js 之中, a.done=%j, b.done=%j', a.done, b.done);   //5



//执行node ab.js的执行结果:

//在 b.js 之中,a.done = false 

//b.js 执行完毕 

//在 a.js 之中,b.done = true 

//a.js 执行完毕 

//在 ab.js 之中, a.done=true, b.done=true

执行流程产生的结果:

第一,在b.js之中,a.js没有执行完毕,只执行了第一行。

第二,ab.js执行到第二行时不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

( 原因在于 require初次加载模块时候必然是阻塞的,初次加载之后会被缓存,所以加载之后再require不会被执行,从缓存里面取值。)

三、require源码分析

下载nodejs源码,require源码见文件 \node-v14.17.5\lib\internal\modules\cjs\loader.js 。

分析过程参考该篇文章 : blog.csdn.net/a460550542/…

调试node源码: cloud.tencent.com/developer/n…

最简单的方法chrome inspect: node --inspect-brk=9229 1-2.js