美文网首页
模块化fis.js源码分析

模块化fis.js源码分析

作者: 天外来人帅 | 来源:发表于2017-11-01 09:41 被阅读63次

    前言

    • 问题一:前端为什么需要模块化开发
      主要解决两个问题:一个是依赖问题,二是命名冲突问题。
    • 问题二:AMD 和 CMD 是什么?
      (1) AMD推崇依赖前置,在定义模块的时候就声明依赖的模块
      (2) CMD推崇就近依赖,只有在用到某个模块的时候再去require
    • 问题三:AMD 和 CMD 区别?
      AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载时机。AMD依赖前置,js可以方便知道依赖的模块,立即加载,而CMD就近依赖,通过把模块变为字符串,解析一遍知道依赖哪些模块。所以两者都是异步加载模块。AMD在加载模块完成后就会执行该模块,所有模块加载执行完后才会执行require的回调函数,这样就会造成依赖模块的执行顺序和书写顺序不一定一致。而CMD是加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序完全一致

    正文

    基于AMD和CMD等相关知识,简要分析下fis.js模块。先简单分析下fis.js结构,再分析下页面结构,最后分析下fis.js执行逻辑, 依照这样的三部曲,开始下面的旅程。

    简单分析下fis.js结构

    下面是fis.js源码的主体结构示意图,分为三部分:F对象、Module对象、工具函数。

    (function () {
        //定义全局对象F
        var F = {
            'version': '2.0.0',
            'debug': false
        }
        //定义一些工具函数
        
        //定义模块类
        function Module (path, name) {
        }
        Module.prototype = {};
        
        //暴露接口module: 声明一个模块
        F.module = function (name, fn, deps) {};
      
        //暴露接口use: 使用一个模块或多个模块
        F.use = function(names, fn) {};
    
        window.F = F;
    })();
    

    简单分析下页面结构

    以下是以贴吧智能版为例,页面结构简要图:

    <html>
        <head></head>
        <body>
            <script src="//zhangshuai.static.tieba.otp.baidu.com/??tb/mobile/sglobal/lib/lib_1e478fb.js,tb/mobile/sglobal/lib/extend_3034d8c.js"></script>
            <script src="//zhangshuai.static.tieba.otp.baidu.com/tb/mobile/sglobal/lib/common_feae225.js"></script>
            <script src="//zhangshuai.static.tieba.otp.baidu.com/??/tb/mobile/app_starter_eec059c.js,/tb/mobile/aa_dff854c.js,/tb/mobile/app_open_885d443.js,/tb/mobile/ua_device_11a1c37.js,/tb/mobile/slider_6d37d9a.js,/tb/mobile/pic_free_mode_adapter_39a74e5.js,/tb/mobile/app_starter_conf_9958175.js,/tb/mobile/clouda_push_6bbb96d.js,/tb/mobile/slide_image_8c6ae49.js,/tb/mobile/slider_main_470314e.js,/tb/mobile/slider_event_aaaa509.js,/tb/mobile/slider_header_cada614.js,/tb/mobile/slider_abstract_447695c.js"></script>
            <div>
                ....
            </div>
            <script>
                F.use(['xxx'], function(obj){
                    ...
                });
            </script>
            <script>
                F.use(['yyy'], function(obj){
                    ...
                });
            </script>
            ...
        </body>
    </html>
    

    第三个script 中js形如:

      F.module('alert', function() {...},[]);
      F.module('confirm', function(){...},[]);
      F.module('dialog', function(){...},[]);
      ...
    
    执行fis.js:

    加载执行fis.js, 将F对象暴露给全局。

    window.F = F
    
    image.png
    暴露出的API:

    F.use: 指定一个或多个模块名,待模块加载完成后执行回调函数,并将模块对象依次传递给函数做参数
    F.module(name,function(require, exports){},[]): 声明一个模块。模块定义函数,有两个参数,分别为require, exports。require是一个函数,用来引用其他模块;exports是一个对象,模块函数最终将模块的api挂载到exports对象上,作为模块对外输出唯一对象。

    fis.js 运行流程

    接下来分析下fis.js 运行流程。以贴吧智能版为例:分为两部分,第一部分是:模块的定义阶段(F.module),第二部分是:模块的调用阶段(F.use)。

    模块标记对象:
    Module.lazyLoadPaths = {name: true}; //F.module 时标记,在lazyLoad() 后逐一删除掉。
    Module.loadedPaths = {path: true}; //已经下载完成的js文件
    Module.loadingPaths = {path: true}; //正在下载的   由true -> false
    Module.initingPaths = {path: true}; //初始化进行中 由true -> false
    Module.requiredPaths = {name: true}; //存放依赖模块 
    
    模块定义阶段:
      // 模块类:
      function Module(path, name){
          //模块名,在define时指定
          this.name = name;
          //模块js文件全路径
          this.path = path;
          //模块函数体
          this.fn = null;
          //模块对象
          this.exports = {};
          this._loaded = false;
          //完成后需要触发的函数
          this._requiredStack = [];
          this._readyStack = [];
          //保存实例,用于单实例判断
          Module.cache[this.name] = this;
      }
      Module.prototype = {
        ...
      };
    
      //根据名称和路径获取模块实例
      function get(name, path) {
          if (Module.cache[name]) {
              return Module.cache[name];
          }
          return new Module(path, name);
      }
    
      // F对象
      F = {};
    
      //模块定义接口
      F.module = function(name, fn, deps){
          var mod = get(name); //获取模块对象,若不存,则返回new Module(name, path);
          mod.fn = fn;
          mod.deps = deps || [];
          if (Module.requiredPaths[name]) {
              mod.define();
          } else {
              Module.lazyLoadPaths[name] = true;
          }  
    }
    

    这一阶段做的工作:

    • 1、 通过get 函数,new 一个模块实例,并保存在Module.cache 中。
    • 2 、在实例对象中,保存一些属性。

    执行完之后:
    Module.cache(存放模块实例) 保存的内容 形如:

    image.png

    Module.lazyLoadPaths 保存的内容 形如:

    image.png
    模块调用阶段:

    分析两种情况: 一种是模块预加载了,另一种是模块未被预加载。

      // 指定一个或多个模块名,待模块加载完成后执行回调函数,并将模块对象依次传递给函数作为参数。
      F.use = function(names, fn) {
          if(typeof names === 'string') {
              name = [names]; //names 数组化,统一管理。name 作为Module 实例对象的唯一id。
          }
          forEach(names, function(name, i){
              var mod = get(name);  //get 函数,从Module.cache中获取相应模块,如果没有 返回一个新的new Module();
              mod.ready(function(){  //第一次执行ready
                  args[i] = mod.exports;
                  if(fn){
                      fn.apply(null,args); //编译阶段,执行F.use 中的回调函数。
                  }
             });
             mod.lazyLoad();  
         }
      }
      function Module(path, name) {
            // 模块名,在define时指定
            this.name = name;
            // 模块js文件全路径
            this.path = path;
            // 模块函数
            this.fn = null;
            // 模块对象
            this.exports = {};
            // 包括依赖是否都下载完成
            this._loaded = false;
            // 完成后需要触发的函数
            this._requiredStack = [];
            this._readyStack = [];
            // 保存实例,用于单实例判断
            Module.cache[this.name] = this;
      }
      Module.prototype = {
          // 在此函数中,会调用mod中存储的fn, 也就是调用模块主体函数,抛出exports 保存在mod.exports
          // 若遇到require,会执行require函数,再次调用此函数
          init: function() {
              if (!this._inited) {
                  this._inited = true;
                  if (!this.fn) {
                      throw new Error('Module "' + this.name + '" not found!');
                  }
                  var result;
                  Module.initingPaths[this.name] = true;
                  if (result = this.fn.call(null, require, this.exports)) {
                      this.exports = result;
                  }
                  Module.initingPaths[this.name] = false;
              }
          },
          // 根据传入参数不同,控制流程。
          ready: function(fn, isRequired) {
              var stack = isRequired ? this._requireStack : this._readyStack;
              if (fn) {
                  stack.push(fn);
              }else {
                  this._loaded = true;
                  Module.loadedPaths[this.path] = true;
                  delete Module.loadingPaths[this.path];
                  this.triggerStack();
              }
          },
          //根据模块存在的状态,执行不同的函数。
          lazyLoad: function() {
              var name = this.name,
                  path = this.path;
              if (Module.lazyLoadPaths[name]) {
                  this.define();
                  delete Module.lazyLoadPaths[name];
              } else {
                  if (Module.loadedPaths[path]) {
                      this.triggerStack();
                  } else if (!Module.loadingPaths[path]) {
                      Module.requiredPaths[this.name] = true;
                      this.load();
                  }
              }
          },
          //调用分析依赖函数,若存在依赖,则执行mod.ready(fn, true), 否则执行mod.ready();
          define: function() {
              var _this = this,
                  deps = this.deps,
                  depPaths = [];
              deps = removeCyclicDeps(_this.path, this.deps);
              if (deps.length) {
                  Module.loadingPaths[this.path] = true;
                  forEach(deps, function(d) {
                      var mod = get(d);
                      depPaths.push(mod.path);
                  });
                  forEach(deps, function(d) {
                      var mod = get(d);
                      mod.ready(function() {
                          if (isPathsLoaded(depPaths)) {
                              _this.ready();
                          }
                      }, true);
                      mod.lazyLoad();
                  });
              } else {
                  this.ready();
              }
          },
          // 在此函数中,统一处理回调函数。先调用mod.init() 
          // 再调用mod.ready 传过来的回调函数,此回调函数中调用F.use中的回调函数。
          triggerStack: function() {
              if (this._readyStack.length > 0) {
                  this.init();
                  forEach(this._readyStack, function(func) {
                      if (!func.doing) {
                          func.doing = true;
                          func();
                      }
                  });
                  this._readyStack = [];
              }
              if(this._requiredStack.length > 0) {
                  forEach(this._readyStack, function(func){
                      if(!func.doing){
                          func.doing = true;
                          func();
                      }
                  });
                  this._requiredStack = [];
              }
          }
        }
    
        //实现模块的require方法
        function require(name) {
            var mod = get(name);
            if (!Module.initingPaths[name]) { //清除循环依赖
                mod.init();
            }
            return mod.exports;
        }
    

    优先把页面所依赖的模块,分析打包到js文件中。

    • 1、names 数组化,统一处理
    • 2、执行mod.ready(function(){ 执行fn()}); 第一次执行mod.ready, 将回调函数保存到了mod._readyStack
    • 3、执行mod.lazyLoad(); 调用mod.define();
    • 4、执行mod.define(); 由于没有依赖,所以再次执行mod.ready();
    • 5、执行mod.ready(); 调用mod.triggerStack();
    • 6、执行mod.triggerStack(); 先执行初始化mod.init(); 再执行mod._readyStack() 中存储的回调函数。
    • 7、执行mod.init(); 执行mod.fn(); 并将 exports 保存到mod.exports中
    • 8、执行mod._readyStack 中存储的函数, 在这个函数中调用F.use的回调函数。

    总结

    循环依赖分析:

    循环依赖说明:
    例如: 假设有以下三个模块A,B,C
    A -> B
    B -> C
    C -> A

    依赖关系如下 a -> [b -> [c -> [a]]]
    处理c模块的依赖关系的时候,调用 removeCyclicDeps(c, [a]) 返回return [];

    依赖分析源代码

        function removeCyclicDeps(uri, deps) {
            return filter(deps, function(dep) {
                return !Module.loadingPaths[dep] || !isCyclicWaiting(Module.cache[dep], uri, []);
            });
        }
    
        function isCyclicWaiting(mod, uri, track) {
            if (!mod || mod._loaded)
                return false;
            track.push(mod.name);
            var deps = mod.deps || [];
            if (deps.length) {
                if (indexOf(deps, uri) > -1) {
                    return true;
                } else {
                    for (var i = 0; i < deps.length; i++) {
                        if (indexOf(track, deps[i]) < 0 && isCyclicWaiting(Module.cache[deps[i]], uri, track)) {
                            return true;
                        }
                    }
                    return false;
                }
            }
            return false;
        }
    

    相关文章

      网友评论

          本文标题:模块化fis.js源码分析

          本文链接:https://www.haomeiwen.com/subject/pyqmpxtx.html