我爱编程

模块--require.js

2017-07-22  本文已影响0人  209bd3bc6844

html文件中的<script>标签中的代码或src引用的js文件中的代码是同步加载和执行的
html文件中的<script>标签中的代码使用document.write()方式引入的js文件是异步执行的
Web动态加载JS外部文件(script标签)
异步加载文件。当页面还有同步代码执行的时候。异步加载从控制台上看到的显示。一直是pending。文件很小的额话其实早就加载回来了。只是js是单线程的。没有功夫去处理显示状态。

面向对象的方式实现require.js

问题:这里都有哪些类型的对象呢?
答案:至少有模块(Module)这一类对象

那模块类对象有哪些数据呢?

Module.id       // 模块id
Module.name     // 模块名字
Module.src      // 模块的真实的uri路径
Module.dep      // 模块的依赖
Module.cb       // 模块的成功回调函数
Module.errorFn  // 模块的失败回调函数
Module.STATUS   // 模块的状态(等待中、正在网络请求、准备执行、执行成功、出现错误……)

又有哪些对应的操作这些数据的方法呢?

Module.prototype.init           // 初始化,用来赋予各种基本值
Module.prototype.fetch          // 通过网络请求获取模块
Module.prototype.analyzeDep     // 分析、处理模块的依赖
Module.prototype.execute        // 运算该模块

先想一下require.js是怎么使用的。
index.html页面引入
<script type="text/javascript" src="./require.js" data-main="main"></script>
data-main 是主文件的入口。main.js里就是主文件,里面有require。然后其他js文件就是定义模块,define()。
假设main.js现在如下

require(['a', 'b'], function (a, b) {
    a.hi();
    b.goodbye();
}, function () {
    console.error('Something wrong with the dependent modules.');
});

这个文件执行的时候。我们先要去加载依赖的a和b模块。我们既然用Module对象来描述每一个模块对象。一般构造函数都如下。有个init

 function Module(name, dep, cb, errorFn) {
        this.init(name, dep, cb, errorFn);
 }

所以我们在解析main.js时候就分别对a和b实例化。只是这时候的实例化的主要目的是执行Module上的加载js函数。
如何异步加载js文件呢。这里用的是动态生成script标签。新的<script>元素加载js文件。此文件当元素添加到页面之后立刻开始下载。等到没有同步代码执行时候。立刻执行加载好的js文件。
当执行加载好的a.js文件时候,就会执行define函数。这里我们队刚刚实例化的a模块进行丰富。

    let module = modules[name];
    module.name = name;
    module.dep = dep;
    module.cb = cb;
    module.errorFn = errorFn;

如果define函数还依赖别的模块。要继续去加载别的模块。
碰到了一个难点:如何分析和处理模块的依赖?
举个例子:main.js必须等a和b模块都加载执行完成后才能执行main的回调。
我想了一个方法:记数法。分两步走。

  1. Module原型新增Module.depCount属性,初始值为该模块依赖模块数组的长度。
  2. 假如depCount===0,说明该模块依赖的模块都已经运算好了,通过setter触发执行该模块。
  3. 某模块执行成功之后,触发下一步。
  4. 下一步为:通过对象mapDepToModuleOrTask,查找到依赖与该模块的所有模块,那么让那些模块都执行depCount--

注:对象mapDepToModuleOrTask的作用是映射被依赖模块到依赖模块之间的关系。
结构如下图所示。举个例子:当模块a准备好之后,我们就遍历mapDepToModule['a']对应的数组,里面的每一项都执行depCount--。
分析依赖模块这个方法

Module.prototype.analyzeDep = function () {
    let depCount = this.dep ? this.dep.length : 0;// 依赖的模块数
    if (depCount === 0) {//如果不依赖别的模块,直接执行回调。
      this.execute();//执行模块回调
      return;
    }
    Object.defineProperty(this, 'depCount', { // 如果依赖别的模块,就增加一个depCount的属性。当依赖加载完一个depCount就--。知道depCount=0。触发回调函数
      get() {
        return depCount;
      },
      set(newDepCount) {
        depCount = newDepCount;
        if (newDepCount === 0) {
          this.execute();
        }
      }
    });
    this.dep.forEach((depModuleName) => { // 遍历该模块的依赖模块,再加载依赖模块
      if (!modules[depModuleName]) { // 映射所有依赖该(depModuleName)模块的模块
        let module = new Module(depModuleName);
        modules[depModuleName] = module;
      }
      if (!mapDepToModuleOrTask[depModuleName]) {
        mapDepToModuleOrTask[depModuleName] = [];
      }
      mapDepToModuleOrTask[depModuleName].push(this); //当前模块的依赖模块都push当前模块
    });
  }

处理依赖循环

我们有时候会定于循环依赖的模块,比如a需要b并且b需要a,会造成死循环(可以在代码中判断是循环依赖的话a需要b,b有需要a。在加载b模块de时候。不再去加载a模块)。这样就不会造成死循环了,但是在这个情况下当b模块调用时他将会从a获得一个undefined值。所以解决办法是模块b的回调函数中,并不能直接引用到a,需要使用require方法包住。
处理办法

// a.js
define(['b'],function (b) {
    var hi = function () {
        console.log('hi');
    };

    b.goodbye();
    return {
        hi: hi
    }
});
// b.js
define(['require', 'a'], function (require) {
    var goodbye = function () {
        console.log('goodbye');
    };
    // 因为在运算b的时候,a还没准备好,所以不能直接拿到a,只能用require再发起一次新的任务
    require(['a'], function (a) {
        a.hi();
    });

    return {
        goodbye: goodbye
    }
});

这样一来原先的require.js就有问题了
原先的设计中, 每一个define是跟一个模块一一对应的, require只能用一次,用于主入口模块(如:main.js)的加载。现在define中还需要解析require,require也需要解析依赖,执行回调。所以require也应当是一个模块。但这个模块不需要fetch。我将它命名为:任务(Task),这是一个有别于Module的新的类。
每一次调用require,相当于新建一个Task(任务)。这个任务的功能是:当任务的所有依赖都准备好之后,执行该任务的成功回调函数。
有没有发现这个Task原型与Module很像?它们都有依赖、回调、状态,都需要分析依赖、执行回调函数等方法。但是又有些不同,比如Task没有网络请求,所以不需要fetch这样的方法。
所以,我让Task继承了Module,然后重写某些方法。
作者的博客--实现require
作者的代码
我仿照写的代码
JavaScript 模块简史

上一篇 下一篇

猜你喜欢

热点阅读