es6 依赖循环_深入了解es6的 module export

在使用es6的export时,我们可能时常遇到这样的疑问:

  1. export default const x = 9; 为啥会报语法错误?
  2. export default function f(){}; export default class C{}; 为啥这又可以呢?
  3. 文件a:export let x = 8; 文件b:import { x } from './a' 两个x是同一个x吗?是一回事儿吗?

问题一:export default const x = 9; 为啥会报语法错误?

ecma-262 6.0(es6)的标准说明书中规定,export default后面只能是这几种形式:

  1. HoistableDeclaration(函数声明,generator声明,未来不仅限于前面这两个)
  2. ClassDeclaration(类声明)
  3. AssignmentExpression(赋值表达式)

所以从标准看,是不支持变量声明定义的语法的,但是我们的疑问是,为什么标准不支持这种写法呢,挺方便的挺常需要的东西。

其实是基于这样的考虑:export default 被设计成模块的默认导出方式,这个默认值只会有一个,但是const可以支持这种形式:const x = 8, y = 10, z = 5; 所以开发人员可能会这样去写export default const x = 8, y = 5, z=99; 这显然是自相矛盾的,不是一个好的语法设计。所以这种形式的语法干脆就被禁止掉了,可以用以下形式替代:

const x = 9;

export default x;

问题二:export default function f(){}; export default class C{}; 为啥这又可以呢?

其实第一个问题已经解释了原因。

funcitonclass不可以一口气定义好几个,一次只能定义一个,这个和export default一个默认值的形式一致的,所以这种语法是行得通的。

问题三:文件a:export let x = 8; 文件b:import { x } from './a' 两个x是同一个x吗?是一回事儿吗?

这个问题有点意思,值得我们好好探究一下。

我们先来看一个例子:

文件a

import { x } from './b';
setTimeout(() => {
    console.log('x', x);
}, 5000);

文件b

export let x = 8; // 会绑定
setTimeout(() => x = 9, 1000);

这个是时候运行的话,x会打印多少呢?

一种可能是,x被导出时,被赋值给新的变量:a的x = b的x; 此时b的x变化不会导致a的x变化,x打印为 8

另一种可能是,a的x和b的x是同一个变量,b的x变化,a的x也会变,x打印为9

用ts-node直接执行或者webpack打包后在浏览器执行,均得到了9

所以两个文件的x实际上是一回事儿,这个就是es6标准说的binding,也就是说这两个x实际上是绑定关系。

那该怎么实现这个es6的标准的呢,有一种方式可以实现这种效果,将b中的x变成o.x,类似于:

function () {
    let o = {};
    o.x = 8;
    setTimeout(() => o.x = 9, 1000);
    return o;
}

这样作为transpiler需要将代码中所有用到x的地方变成o.x。

webpack实现的方式比较巧妙一些,是这么做的:

a文件:

var _web_sub__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./b.js");
setTimeout(() => {
  console.log('x', _web_sub__WEBPACK_IMPORTED_MODULE_0__.x);
}, 5000);

b文件:

 __webpack_require__.d(__webpack_exports__, {
   "x": () => x
});
let x = 8;
setTimeout(() => x = 9, 1000);

重点看b文件,

__webpack_exports__是一个空对象

__webpack_require__.d利用defineObjectProperty方法将将x以getter形式定义到__webpack_exports__

最终__webpack_exports__会被返回给a文件,被赋值给_web_sub__WEBPACK_IMPORTED_MODULE_0__

执行_web_sub__WEBPACK_IMPORTED_MODULE_0__.x时,会触发 () = x 此getter

可以看到,webpack用了闭包的方式,在闭包中直接访问要导出的变量,这样a文件的x和b文件的x实际上是同一个,x并没有被赋值给其他中间变量。

webpack便是用的此方式实现的live binding(动态绑定),挺简单的一套操作。

不过我们再来看另外一个例子:

文件a:

import x from './b';
setTimeout(() => {
    console.log('x', x);
}, 5000);

文件b:

let x = 8;
export default x; // 不会绑定
setTimeout(() => x = 9, 1000);

按照上面动态绑定的说法,x应该打印9

然而却不是,事实打印的是8

这又是为什么呢?难道default就不动态绑定了?es6也太混蛋了,太坑人了。

其实不然,大家可以在回顾一下上面标准里说的export default后面应该接啥,例子里的情况实际上属于AssignmentExpression的情况,也就是说例子里export default后面其实是个可以作为右值的表达式,而表达式可以是各种形式:

export default x;
export default x + 1;
export default 1 + 2;

这种情况下,其实并没有一个实际可绑定的变量,因为他是一个表达式,因此也就无所谓绑定了。

我们可以看一下webpack是如何实现这种场景的:

__webpack_require__.d(__webpack_exports__, {
  "default": () => __WEBPACK_DEFAULT_EXPORT__
});
let x = 8;
const __WEBPACK_DEFAULT_EXPORT__ = (x);
setTimeout(() => x = 9, 1000);

x被作为表达式赋值给了__WEBPACK_DEFAULT_EXPORT__

我们再看另外一个例子:

文件a:

import x from './b';
setTimeout(() => {
    console.log('x', x);
}, 5000);

文件b:

export default function f() {

};

setTimeout(() => f = 9, 1000);

这种情况,x打印了9,而不是空函数

为什么这次export default又可以绑定了呢,其实根据上面对表达式的解释很好推断,

function f() {}是一个很明确的函数定义,有一个明确的名字:f

f可以被绑定,class的情况其实是一样的

在webpack中,被绑定的变量会被放到一个闭包中返回,依赖此变量的模块在访问该变量时触发getter,闭包将绑定变量返回。

其实可以总结一下:只要export后面是一个可以明确获取到绑定变量的情况,该变量都会被绑定。

可以看一下es6标准中给出的用法,有ExportName和LocalName的概念

e04d8fc19fd4f58d34d4ca2a486a0a5c.png

到此,export的常规只是部分已经结束。

然后我们还有一个思考,为什么es6要采用变量绑定的形式呢?

确实发现了一个好处,就是可以解决循环引用的问题。

咱们先说一下为什么es6可以支持循环引用。

大家都知道在深层遍历一个具有循环引用的变量时,如果不去主动检测规避的话,会形成死循环。那es6中为啥不会出现栈溢出或者没完没了的死循环呢?

我们先看标准中怎么说的:

abd9ddde4a089909f8b7b0ab610fcb0b.png

ResolveExport是幂等的、无副作用,一个实现可以选择预先计算或者缓存住Exports的结果

ResolveExport是用来处理imported的,是幂等的、无副作用的。

那我们看看webpack是怎么实现的:

var __webpack_module_cache__ = {};

function __webpack_require__(moduleId) {

  if(__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }
  var module = __webpack_module_cache__[moduleId] = {
    exports: {}
  };

  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  return module.exports;
}

模块导出的东西会存到__webpack_module_cache__中,等下次再重复import时,会直接读取缓存__webpack_module_cache__。这就是ResolveExport的实现。

有了这个之后,某模块第二次被重复引用时,并不会重复的去执行模块的代码,而是直接将上次导出的值返回给了需要的模块。这样当我们循环引用时,某模块第二次被引用,由于并不会重复执行模块代码,因此循环引用被切断,不会没完没了的引用下去(间接循环和直接循环都是如此)

举例如下:

文件util.js:

import { ajax } from './network';

console.log('ajax', ajax);

export function getPermission(id) {
    const url = 'https://x.com/id';
    return ajax(url);
}

export function parseUrl(url) {
}

getPermission(123); // 可成功运行

文件network.js:

import { parseUrl } from './util';

export function ajax(url) {
  console.log('ajax is running');
  parseUrl(url);
}

从两个文件上方的import可以看到是相互依赖,循环引用。

咱们可以结合webapck的__webpack_require__函数简单捋一下执行流程:

先调用__webpack_require__,传入util.js

=> 1. 生成__webpack_module_cache__ ,一个空的exports对象

=> 2. 执行util.js ,将导出变量以getter函数的形式加到exports对象上

=> 3. 由于依赖network.js,所以转为先执行network.js

=> 4. 发现network.js依赖util.js,转为执行__webpack_require__('./util.js')

=> 5. 发现__webpack_module_cache__中有utils.js的导出缓存:__webpack_module_cache__[moduleId].exports,直接将该缓存return,并没有继续去执行util.js(这一步是关键

=> 6. network.js拿到缓存的util.js导出的数据继续往下执行

=> 7. network.js执行完毕后,将导出的数据返回给util.js,util.js继续执行

=> 结束

可以看到步骤5终止了无限循环

上面总体说明了为什么es6中循环引用不会导致死循环,但是另外有一个问题,就是步骤5直接reutrn了util.js的导出缓存,但此时util.js还没有开始执行,又哪里来的导出值呢?

答案是,在第5步的那一刻,util.js确实没有导出值,只有等utils执行完了,才能有真正的具有业务意义的值,但是第5步的导出数据已经返回给了network.js,network.js如果当时立即就访问该值,此时会触发上面说的getter函数,getter函数会直接访问导出的变量,如果该变量是个函数,可以进行提升,则可以正常运行,但是如果是个不能提升的普通变量,则会直接报错,这样的话即使es6允许循环引用,那也失去了意义,因为无法获取依赖的导出数据。

这时该我们的binding(绑定)出场了,由于es6的导出值和被依赖模块中的export值是绑定关系,network.js只需要等util.js执行完,就可以获取到util.js的导出数据了。

这种场景非常适合network中函数对导出数据依赖的情况

举例如下:

util.js:

import { ajax } from './network';

console.log('ajax', ajax);

export function getPermission(id) {
    const url = 'https://x.com/id';
    return ajax(url);
}

export function parseUrl(url) {
}

export const xxxxxx = 8;

getPermission(123); // 可成功运行

会报错的network.js

import { parseUrl, xxxxxx } from './util';

console.log('parseUrl', parseUrl); // 由于parseUrl是函数,有变量提升,可以正常访问
console.log('xxxxxx', xxxxxx); // 此处触发() = xxxxxx,会报错

export function ajax(url) {
  console.log('ajax is running');
  parseUrl(url);
}

正确的network.js

import { parseUrl, xxxxxx } from './util';

console.log('parseUrl', parseUrl); // 由于parseUrl是函数,有变量提升,可以正常访问
// console.log('xxxxxx', xxxxxx); // 此处触发() = xxxxxx,会报xxxxxx未定义

export function ajax(url) {
  console.log('ajax is running');
  console.log('xxxxxx', xxxxxx); // 打印出8,此时util的xxxxxx已经赋值完成
  parseUrl(url);
}

到此结束

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页