资讯专栏INFORMATION COLUMN

谈谈对模块化的理解

silvertheo / 1038人阅读

摘要:重要的模块化规范有几个,模块机制,,。模块化的目的在于营造安全封闭的作用域且具有易于引用接口,按我的理解可分为模块定义模块引入两部分。它的定义如下模块标识符模块对外输出的值调用该模块的模块。在中,有一部分模块由提供,称之为核心模块。

重要的模块化规范有几个:commonjs,ES6模块机制,AMD,CMD。由于业务中一直接触的都是Vue+webpack+babel架构的项目,在封装代码时用的比较的多还是ES6规范,对其他模块化规范不熟悉,因此在这里记录一下学习过的模块化知识。

CommonJS

模块化的目的在于营造安全封闭的作用域、且具有易于引用接口,按我的理解可分为模块定义、模块引入两部分。

在模块中存在着一个module对象,它代表着模块本身,将需要导出的api挂载于其中的exports属性上即可以定义导出的接口;CommonJS规范中存在require()方法,用于接受模块标识,引入某个模块到当前的上下文。

1. 模块定义

要理解模块如何定义,那必须要先理解module对象。在Node中,每一个文件模块都是一个对象,即module对象。它的定义如下:

function Module(id, parent){
    this.id = id    //模块标识符
    this.exports = {}    //模块对外输出的值
    this.parent = parent    //调用该模块的模块。parent为null时意味着模块为入口模块
    if(parent && parent.children){
        parent.children.push(this)
    }    
    this.filename = filename    //文件名
    this.loaded = false    //是否已加载
    this.children = []    //表示该模块调用的其他模块
}

定义模块的目的其实在于定义输出的值。写法非常简单,举个?

function sayHello(){
    console.log("hello")
}
module.exports = sayHello    //或exports.sayHello = sayHello

为了方便导出接口,Node还定义了一个exports变量,但有个容易踩的坑是,exports只是一个引用,本来指向module.exports,假如只是给exports变量赋值则exports变量会失去对module.exports的指向。说到底,必须对module.exports定义接口才能真正导出值。

先说解决方法,常见的写法为:

exports = module.exports = sayHello
//或严格地只给exports变量添加属性
exports.sayHello = sayHello

再举个例子说一下犯错的具体场景:

//a.js
exports.name = "kent"
exports.sayHi = function(){
    console.log("hi")
}

console.log(module)// { exports: { name: "kent", sayHi: function(){ console.log("hi") } } }

//假如给exports重新赋值 =_=
exports = {
    name: "nicolas",
    sayBye: function(){
        console.log("bye")
     }
}

//module中的exports属性不会有任何变化
console.log(module)// { exports: { name: "kent", sayHi: function(){ console.log("hi") } } }
console.log(exports)// { name: "nicolas", sayBye: function(){ console.log("bye") } }
//b.js
//因此require的时候读取的name仍然为kent
var person = require("a.js")
console.log(person.name)//kent

具体的原因也可以从模块机制中看出来

function require(...) {
  var module = { exports: {} };
  ((module, exports) => {
    // Your module code here. In this example, define a function.
    function some_func() {};
    exports = some_func;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = some_func;
    // At this point, the module will now export some_func, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
}
2. 模块引入

模块引入的语法也非常简单。上一节也简单提过。这里再举个?

//book.js
exports.name = "javascript"
exports.logName = function(){
    console.log("javascript")
}
//main.js
var book = require("./book.js")//require的参数即模块标识符
console.log(book.name)//"javascript"
book.logName()//"javascript"

下面详情谈谈模块引入经历哪些步骤。但在此之前需要先了解两个概念:核心模块与文件模块。

在Node中,有一部分模块由Node提供,称之为核心模块。在Node进程启动的时候,核心模块就直接加载至内存中。因此引入核心模块只需要走路径分析一个步骤,其加载速度最快。

另一部分则是运行时动态加载,常见的有用户定义带路径标识符的模块,或自定义模块(如三方提供的包)。这类模块需要完整地走完以下三个步骤:路径分析、文件定位与编译执行。

①路径分析:
路径分析可以理解为模块标识符的分析。模块标识符在Node中主要有:

    ·核心模块,如:http, fs等等;
    ·以"./"或"../"开头的相对路径模块,相对于当前的目录位置;
    ·以"/"开头的绝对路径模块;
    ·非路径形式的文件模块,与核心模块的标识符类似。Node会搜索各级的node_modules目录。

· 核心模块:核心模块经过路径分析之后会直接加载。需要注意的是,自定义的文件模块不能与核心模块标识符相同,要不更换不同的标识符要么使用相对路径或绝对路径标识符。

· 路径形式的文件模块:在分析文件模块的时候,require方法将会把路径转换为真实路径并以此为索引编译模块并存放到缓存中(缓存加载将在下文介绍)。

· 非路径形式的文件模块(自定义模块):自定义模块的路径分析在我们引用三方库的时候经常会碰到。这类非路径形式的文件模块加载时将会以模块路径为线索逐级搜索。举个? :

//在"/Users/zhazheng/Documents/www"下新建一个module_path.js

//module_path.js
console.log(module.paths)

//再执行module_path.js
node module_path

//得出以下log
[ "/Users/zhazheng/Documents/www/node_modules",
  "/Users/zhazheng/Documents/node_modules",
  "/Users/zhazheng/node_modules",
  "/Users/node_modules",
  "/node_modules" ]

可见,这类模块会从当前文件目录往上逐级递归直到根目录下的node_modules目录。因此这类模块的路径分析是最费时的。

②文件定位:文件定位主要包括文件扩展名分析、目录和包的处理。

·文件扩展名分析:分析标识符的过程中出现不包含文件扩展名的情况非常常见。在标识符不包含文件扩展名的情况下,Node会依次尝试以下三种扩展名:.js、.json、.node。由于尝试解析的过程是同步阻塞进行的,因此大量的分析文件扩展名会产生性能问题,这种情况下可以尝试添加扩展名或充分利用缓存加载的优势。

·目录分析与包的处理: 假如分析完扩展名后仍然没有找到对应的文件而只得出一个目录,那么Node会将此目录当做一个包来处理。首先会查找当前目录下是否有package.json文件,假如有则检查是否具有main属性(main属性即指向入口文件)。假如没有package.json文件或package.json中不具备main属性,那么Node则按index为默认的文件名,最后再重复“文件扩展名分析”这个步骤。

3.缓存加载

事实上Node的模块,无论是核心模块还是文件模块,第一次加载之后都会被缓存。require()方法将会对二次加载的模块进行缓存。因此假如有多次加载模块的需求,那么就需要记得先从缓存中删除模块。

缓存均保存在require.cache对象中,需要删除单个模块或全部模块的缓存可以这样写:

//删除单个模块缓存
delete require.cache[moduleName]

//删除全部模块缓存
Object.getOwnPropertyNames(require.cache).forEach(key => {
    delete require.cache[key]
})

当然,一般情况下缓存是可以带来性能优势的。对于路径套得非常深的自定义文件模块来说尤甚。

4.循环加载

循环加载是避免不了的问题。在Node中需要了解一下循环加载的表现。首先要理解的是,require是一个同步加载的过程,读取的接口仅仅是指向exports对象中的属性,举个? :(以下三个模块均在同一目录下)

//a.js
exports.name = "a1"
console.log(`a.js, ${require("./b.js").name}`)
exports.name = "a2"
//b.js
exports.name = "b1"
console.log(`b.js, ${require("./a.js").name}`)
exports.name = "b2"
//main.js
console.log(`main.js, ${require("./a.js").name}`)
console.log(`main.js, ${require("./b.js").name}`)

nvm run node然后.load main.js得出以下的结果

b.js, a1
a.js, b2//这两行结果应该大致可以理解两个模块的require方法发生了什么
main.js a2
main.js b2

再次执行.load main.js会读取缓存结果

main.js a2
main.js b2

循环加载示例代码可到我的github查看

AMD
...未完待续

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91938.html

相关文章

  • 70 个 Spring 最常见面试题,Java 晋升必会

    摘要:容器自动完成装载,默认的方式是这部分重点在常用模块的使用以及的底层实现原理。 对于那些想面试高级 Java 岗位的同学来说,除了算法属于比较「天方夜谭」的题目外,剩下针对实际工作的题目就属于真正的本事了,热门技术的细节和难点成为了主要考察的内容。 这里说「天方夜谭」并不是说算法没用,不切实际,而是想说算法平时其实很少用到,甚至面试官都对自己出的算法题一知半解。 这里总结打磨了 70 道...

    Ashin 评论0 收藏0
  • 金三银四,2019大厂Android高级工程师面试题整理

    摘要:原文地址游客前言金三银四,很多同学心里大概都准备着年后找工作或者跳槽。最近有很多同学都在交流群里求大厂面试题。 最近整理了一波面试题,包括安卓JAVA方面的,目前大厂还是以安卓源码,算法,以及数据结构为主,有一些中小型公司也会问到混合开发的知识,至于我为什么倾向于混合开发,我的一句话就是走上编程之路,将来你要学不仅仅是这些,丰富自己方能与世接轨,做好全栈的装备。 原文地址:游客kutd...

    tracymac7 评论0 收藏0
  • Java项目经验——程序员成长钥匙

    摘要:当你真正到公司里面从事了几年开发之后,你就会同意我的说法利用找工作,需要的就是项目经验,项目经验就是理解项目开发的基本过程,理解项目的分析方法,理解项目的设计思 Java就是用来做项目的!Java的主要应用领域就是企业级的项目开发!要想从事企业级的项目开发,你必须掌握如下要点: 1、掌握项目开发的基本步骤 2、具备极强的面向对象的分析与设计技巧 3、掌握用例驱动、以架构为核心的主流开发...

    zhangfaliang 评论0 收藏0
  • 谈谈JavaScript中function多重理解

    摘要:中的有多重意义。它可能是一个构造器,承担起对象模板的作用可能是对象的方法,负责向对象发送消息。语义匿名函数处理某些特殊效果如处理一些数据又不想暴露过多的变量判断版本的方式最终只要一个结果,匿名函数内部用到了一些局部变量全部可以隔离开。 JavaScript 中的 function 有多重意义。它可能是一个构造器(constructor),承担起对象模板的作用; 可能是对象的方法(met...

    muzhuyu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<