前言

这篇文章是《webpack 实战》相关章节的总结。看完这篇文章,你将了解:

  • commonjs 与 ES6 模块化语法
  • 值的拷贝与动态映射
  • 什么是循环依赖,两种模块化风格对它的处理方式

commonjs 模块化语法

computer.js

module.exports = {
  number: 1,
  add: function(a, b) {return a + b}
}

main.js

const xx = require('./computer.js')
console.log(xx.number)
console.log(xx.add(1, 2))

只需记住:require 函数的返回值就是 module.exports 就可以了

此外 computer.js 里 exports 变量也可以表示导出值。但是注意它不能赋值自身导出,只能赋值自身属性导出。因为 exports 本质是指向 modlue.exports 的内存引用,作用机理可理解为如下代码:

let module = {exports: null}
let exports = module.exports
...
return module.exports

ES6 模块化语法

computer.js

const number = 1
const add = (a, b) => {return a + b}
export {number, add}

或者

export default {
  number: 1,
  add: function(a, b) {return a + b}
}

上面两种导出风格都可以,分别为命名导出默认导出,有两种导出风格,就有两种导入风格

main.js

import {number, add} from './computer.js'
console.log(number)
console.log(add(1, 2))

或者

import xx from './computer.js'
console.log(xx.number)
console.log(xx.add(1, 2))

注意两种写法对号入座

命名导出的限制是主文件导入变量的名称必须与导出模块的变量命名一致,默认导出对象自定义命名则相对自由。但是,相应地,命名导出与命名导入也可以通过 as 重命名

比如

// 可以重命名导出变量
export {number as num}
...
import {num} from './computer.js'

// 也可以重命名导入变量
export {number}
...
import {number as n}

此外,还可以混合导出(即默认导出和命名导出同时存在)

import React, {Component} from 'React'

// 对应
export default React对象
export {Component}
在浏览器运行模块化的 js

一般来说,模块化代码都是面向打包工具的。如果想在浏览器中使用 es6 模块化的话,应该

  • 在 script 标签加上 type="module"
  • 在服务器环境下运行 html
  • 推荐使用 .mjs 后缀

具体参考

值的拷贝与动态映射

同样是模块化

  • commonjs 导入变量是导出变量值的拷贝,是一份无关的副本。b 变 a 不变
  • es6 模块化导入变量是导出变量的相同引用,且导入变量是只读的。b 变 a 也变

例如

computer.js

let number = 0
let plus = () => {number++}
module.exports = {number, plus}

main.js

let number = require('./comuter.js').number
let plus = require('./computer.js').plus
console.log(number)  // 0
plus()
console.log(number)  // 0
number += 1
console.log(number)  // 1

computer.js

let number = 0
let plus = () => {number++}
export {number, plus}

main.js

import {number, plus} from './computer.js'
console.log(number)  // 0
plus()
console.log(number)  // 1
number += 1  // syntax Error, ... read-only
console.log(number)  

循环依赖

什么是循环依赖?foo.js 引用 bar.js,bar.js 引用 foo.js,这就是循环依赖。commonjs 和 ES6 模块化对它们的处理方式不同

// foo.js
const bar = require('./bar.js')
console.log('value of bar: ', bar)
module.exports = 'This is foo.js'

// bar.js
const foo = requier('./foo.js')
console.log('value of foo: ', foo)
module.exports = 'This is bar.js'

// index.js
require('./foo.js')

代码输出结果是:

// index.js 导入 foo.js,开始执行 foo.js 代码
// foo.js 导入 bar.js,执行权交给 bar.js
// bar.js 导入 foo.js,因为之前已经导入过一次 foo.js,所以执行权不会交给 foo.js,而是直接取出导出值 {}
value of foo: {}
// 继续执行 bar.js 到结束,执行权返回 foo.js,直到代码结束
value of bar: This is bar.js

出现这种结果,原因可以从 webpack 打包后的代码察觉

// require 函数
function __webpack_require__(moduleId) {
  // 如果模块哈希已有记录,直接返回记录 exports 属性。第二次,bar.js 导入 foo.js 时执行这里取出 {}
  if(installedModules(moduleId)) {
    return installedModules[moduleId].exports;
  }
  // 如果模块哈希未有记录,添加以下记录。第一次,index.js 导入 foo.js 时执行这里存入 {}
  var module = installedModules[moduleId] = {
    i: moudleId,
    l: false,
    exports: {}
  };
  ...
}

将代码改为 es6 模块化风格

// foo.js
import bar from './bar.js'
console.log('value of bar: ', bar)
export default 'This is foo'

// bar.js
import foo from './foo.js'
console.log('value of foo: ', foo)
export default 'This is bar'

// index.js
import foo from './foo.js'

代码输出结果是:

// index.js 导入 foo.js,执行权交给 foo.js
// foo.js 导入 bar.js,执行权交给 bar.js
// bar.js 再导入 foo.js,因为之前已经导入一次,所以执行权不变,继续代码,foo 被初始化为 undefined
value of foo: undefined
// 继续运行到代码结束
value of bar: This is bar

再看一个例子

// index.js
import foo from './foo.js'
foo('index.js')

// foo.js
import bar from './bar.js'
funtion foo(invoker) {
  console.log(`${invoker} 导入 foo.js`)
  bar('foo.js')
}
export default foo

// bar.js
import foo from './foo.js'
// 下面的变量防止代码出现循环递归
let invoked = false
function bar(invoker) {
    if(!invoked) {
        console.log(`${invoker} 导入 bar.js`)
        foo('bar.js')
    }
}
export default bar

输出结果是

// index.js 导入 foo.js,执行权交给 foo.js
// foo.js 导入 bar.js,执行权交给 bar.js
// bar.js 导入 foo.js,因为是第二次导入,所以执行权不交给 foo.js,此时 foo 值为 undefined
// bar.js 执行完毕,执行权交给 foo.js
// foo.js 执行完毕,foo 值为函数,此时因为动态映射的特性,bar.js 代码里的 foo 值已为函数
// index.js 执行 foo('index.js')
index.js 导入 foo.js
foo.js 导入 bar.js
bar.js 导入 foo.js
// foo.js 执行 bar('index.js'),bar 函数的 invoked 防循环递归,代码结束

可以看到,es6 模块化的动态映射特性,使得只要代码能保证使用导入变量的时候相应的依赖已经执行完毕,就可以解决循环依赖问题