JavaScript设计模式(三十五):大心脏-异步模块模式

简介: 大心脏-异步模块模式

异步模块模式——AMD(Asynchronous Module Definition)

模块化:将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展,提高模块的复用率

请求发出后,继续其他业务逻辑,直到模块加载完成执行后续的逻辑,实现模块开发中对模块加载完成后的引用

异步加载文件中的模块

image.png

  • index.js

      /**
       * 同步模块模式-SMD
       */
    
      // 向闭包中传入模块管理器对象F(~屏蔽压缩文件时,前面漏写;报错)
      ~(function (F) {
         
         
          /**
           * 定义模块管理器
           * @param {string} str 模块路由
           * @param {Function} fn 模块方法
           * @returns F模块管理器对象
           */
          F.define = function (str, fn) {
         
         
              // 过滤掉第一个元素为F的字符串数组
              let keys = str.replace(/^F\./, '').split('.');
              // 屏蔽对define与module模块方法的重写
              if (keys[0] === 'define' || keys[0] === 'module') return this;
              // 遍历字符串数组,赋值到当前对象内
              let obj = this;
              for (let i = 0; i < keys.length; i++) {
         
         
                  // 防止后续模块覆盖以前定义的模块
                  if (typeof obj[keys[i]] === 'undefined') {
         
         
                      obj[keys[i]] = {
         
         };
                      if (i === keys.length - 1) {
         
         
                          obj[keys[i]] = fn && fn();
                      }
                  }
                  obj = obj[keys[i]];
              }
              // 当前对象
              return this;
          };
    
          /**
           * 模块调用方法
           * @param  {...any} args 需要最后的参数为function
           */
          F.module = function (...args) {
         
         
              // 依赖模块列表
              let modules = [];
              // 判断最后一个参数是否为函数
              let fn = typeof args[args.length - 1] === 'function' ? args.pop() : null;
              // 将参数转换为数组
              let arr = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
              // 遍历参数
              let i = 0;
              while (i < arr.length) {
         
         
                  // 判断参数是否是字符串(通过define创建的)
                  if (typeof arr[i] === 'string') {
         
         
                      let keys = arr[i].replace(/^F\./, '').split('.');
                      // 获取当前F对象
                      let mod = this;
                      for (let j = 0; j < keys.length; j++) {
         
         
                          // 替换当前的mod,查询到最里层对象
                          mod = mod[keys[j]];
                      }
                      modules.push(mod);
                  } else {
         
         
                      modules.push(arr[i])
                  }
                  i++;
              }
              fn && fn.apply(this, modules);
          };
      }((function () {
         
         
          return window.F = {
         
         };
      }())));
    
  • demo.js
      F.define('demo.say', function () {
         
         
          return function (name) {
         
         
              console.log(`Hello ${
           
           name}`);
          }
      });
    
  • index.html

      <script src="./index.js"></script>
      <script>
          /**
           * 加载脚本文件
           * @param {string} src 脚本路径
           */
          function loadScript(src) {
          
          
              let _script = document.createElement('script');                 // 创建脚本元素
              _script.type = 'text/javascript';                               // 设置类型
              _script.charset = 'UTF-8';                                      // 确认编码
              _script.async = true;                                           // 异步加载
              _script.src = src;                                              // 设置加载路径
              document.getElementsByTagName('head')[0].appendChild(_script);  // 将元素插入到页面中
          }
          // 加载demo.js脚本
          loadScript('./demo.js');
    
          // 调用demo.say模块方法 (Error ---> Uncaught TypeError: Cannot read properties of undefined (reading 'say'))
          F.module('demo.say', function (say) {
          
          
              say('Lee');
          });
    
          setTimeout(() => {
          
          
              // 调用demo.say模块方法 (Success ---> Hello Lee)
              F.module('demo.say', function (say) {
          
          
                  say('Lee');
              });
          }, 2000);
      </script>
    

报错原因:

由于浏览器中的文件是异步加载的,虽然现在开始加载demo.js文件,不过在文件没有加载完之前你可以继续做其他的事情,并且你写的方法,对于文件什么时候加载完成,你是无法获知的。

同步模块模式会立即引用该模块,此时文件加载尚未加载完成,因此你是引用不到该模块的。

当延迟了2s后,demo.js文件已经加载完毕,所以能得到demo.say模块,所以访问无问题。

异步模块(使用require.js

image.png

https://requirejs.org/docs/release/2.3.6/comments/require.js

<script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script>
<script>
    /**
     * 异步模块(使用require.js)
     */
    require(['./lib/a', './lib/b'], function (a, b) {
    
    
        console.log(a, b);
        a('Lee'); // Hello Lee
        b.demo.getName(); // demo
    });
</script>
// a.js
define([], function () {
   
   
    return function (name) {
   
   
        console.log(`Hello ${
     
     name}`);
    }
});

// b.js 依赖于demo.js
define(['./demo'], function (demo) {
   
   
    return {
   
   
        name: 'b',
        demo
    };
});

// demo.js
define([], function () {
   
   
    return {
   
   
        name: 'demo',
        getName(){
   
   
            console.log(this.name);
        }
    }
});

仿require.js的实现

核心思想:每开始进行加载一个js脚本,计数器加1;每加载完成一个脚本,计数器减1;当全部脚本加载完成,计数器为0,此时执行回调函数,告诉控制台脚本已经加载完成

image.png

image.png

// index.js

/**
 * 异步模块 - 创建与调度模块
 */

// 向闭包中传入模块管理器对象R(~屏蔽压缩文件时,前面漏写;报错)
~(function (R) {
   
   

    // 缓存所有加载的模块
    const cache = {
   
   };

    // 最终的回调
    let cb = function () {
   
    };

    // 脚本加载数量(每添加一个js脚本累加1;加载完成累减1;全部完成变为0)
    let depCount = 0;

    /**
     * 获取脚本路径
     * @param {string} name 路径
     * @returns 路径
     */
    function getScriptSrc(name) {
   
   
        // 拼接完整的文件路径字符串,如'lib/ajax' => 'lib/ajax.js'
        return String(name).replace(/\.js$/g, '') + '.js';
    }

    /**
     * 加载脚本文件
     * @param {string} src 路径
     */
    function loadScript(src) {
   
   
        let _script = document.createElement('script');
        _script.type = 'text/javascript';                               // 文件类型
        _script.charset = 'UTF-8';                                      // 确认编码
        _script.async = true;                                           // 异步加载
        _script.src = src;                                              // 文件路径
        document.getElementsByTagName('head')[0].appendChild(_script);  // 插入页面中
    }

    /**
     * 定义模块(当引入的js加载完成,就会自动执行此方法)
     * @param {string} name 模块名称
     * @param {Array} deps 依赖项 ([{name: 模块名称, path: 模块路径}, ...])
     * @param {Function} callback 回调
     */
    R.define = function (name, deps, callback) {
   
   

        depCount--;

        // 添加依赖脚本
        deps.forEach(dep => {
   
   
            depCount++;
            loadScript(getScriptSrc(dep.path));
        });

        // 是否存在此模块,避免重复赋值
        if (!cache[name]) {
   
   
            cache[name] = {
   
   
                depNames: deps.length ? deps.map(dep => dep.name) : [], // 脚本的依赖脚本模块名称(作为缓存对象的key,用于最终整理)
                hasDep: !!deps.length,                                  // 是否存在脚本的依赖脚本
                export: deps.length ? callback : callback(),            // 最终导出的数据(存在依赖脚本则返回函数,用于后续判断调用;不存在依赖脚本,返回当前函数的执行)
            };
        }

        // 全部加载完成
        if (depCount === 0) {
   
   
            Object.values(cache).forEach(item => {
   
   
                if (item.hasDep) {
   
   
                    let params = item.depNames.map(key => cache[key]['export']);
                    item.export = item.export.apply(null, params);
                }
            });

            let obj = {
   
   };
            Object.keys(cache).forEach(key => {
   
   
                obj[key] = cache[key].export;
            });

            cb.call(null, obj);
        }

    };

    /**
     * 执行各个脚本js模块
     * @param {Array} deps 当前页面的依赖项,js脚本路径集合
     * @param {Function} callback 回调
     */
    R.module = function (deps, callback) {
   
   

        // 保存当前的回调,用于脚本完全加载完成后调用
        cb = callback;

        // 添加依赖脚本
        deps.forEach(dep => {
   
   
            depCount++;
            loadScript(getScriptSrc(dep));
        });

    };

}((function () {
   
   
    return window.R = {
   
   };
}())));
  • html.html

      <script src="./index.js"></script>
      <script>
          console.log(R);
          /**
           * 异步模块
           */
          R.module(['./lib/a', './lib/b'], function (mod) {
          
          
              /**
               * {
               *      a: ƒ (name),
               *      b: {
               *          name: 'b', 
               *          demo1: {name: 'demo1', getName: ƒ}, 
               *          demo2: {name: 'demo2', getName: ƒ},
               *      },
               *      demo1: {name: 'demo1', getName: ƒ},
               *      demo2: {name: 'demo2', getName: ƒ},
               * }
               */
              console.log(mod);
    
              mod.a('Lee'); // Hello Lee
    
              mod.b.demo1.getName(); // demo1
              mod.b.demo2.getName(); // demo2
    
              mod.demo1.getName(); // demo1
              mod.demo2.getName(); // demo2
          });
      </script>
    
  • lib/a.js
      // a.js
      R.define('a', [], function () {
         
         
          return function (name) {
         
         
              console.log(`Hello ${
           
           name}`);
          }
      });
    
  • lib/b.js
      // b.js 依赖于 demo1.js demo2.js
      R.define('b', [{
         
          name: 'demo1', path: './lib/demo1' }, {
         
          name: 'demo2', path: './lib/demo2' }], function (demo1, demo2) {
         
         
          console.log(demo1, demo2);
          return {
         
         
              name: 'b',
              demo1, 
              demo2
          };
      });
    
  • lib/demo1.js
      // demo1.js
      R.define('demo1', [], function () {
         
         
          return {
         
         
              name: 'demo1',
              getName(){
         
         
                  console.log(this.name);
              }
          }
      });
    
  • lib/demo2.js
      // demo2.js
      R.define('demo2', [], function () {
         
         
          return {
         
         
              name: 'demo2',
              getName(){
         
         
                  console.log(this.name);
              }
          }
      });
    

特点

模块化开发不仅解决了系统的复杂性问题,而且减少了多人开发中变量、方法名被覆盖的问题。通过其强大的命名空间管理,使模块的结构更合理。

通过对模块的引用,提高了模块代码复用率。异步模块模式在此基础上增加了模块依赖,使开发者不必担心某些方法尚未加载或未加载完全造成的无法使用问题。

异步加载部分功能也可将更多首屏不必要的功能剥离出去,减少首屏加载成本。

目录
相关文章
|
4月前
|
JavaScript 前端开发
在Node.js中,如何合理使用模块来避免全局变量的问题?
在Node.js中,如何合理使用模块来避免全局变量的问题?
188 71
|
7月前
|
Web App开发 JavaScript 前端开发
如何在JavaScript中确定异步操作之间的依赖关系?
如何在JavaScript中确定异步操作之间的依赖关系?
175 58
|
7月前
|
前端开发 JavaScript
有没有方法可以保证在JavaScript中多个异步操作的执行顺序?
有没有方法可以保证在JavaScript中多个异步操作的执行顺序?
277 58
|
7月前
|
JavaScript 前端开发 Docker
如何通过pm2以cluster模式多进程部署next.js(包括docker下的部署)
通过这些步骤,可以确保您的Next.js应用在多核服务器上高效运行,并且在Docker环境中实现高效的容器化管理。
804 44
|
6月前
|
JavaScript 前端开发 API
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)
array.map()可以用来数据转换、创建派生数组、应用函数、链式调用、异步数据流处理、复杂API请求梳理、提供DOM操作、用来搜索和过滤等,比for好用太多了,主要是写法简单,并且非常直观,并且能提升代码的可读性,也就提升了Long Term代码的可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
设计模式 JavaScript 算法
浅谈几种js设计模式
设计模式是软件开发中的宝贵工具,能够提高代码的可维护性和扩展性。通过单例模式、工厂模式、观察者模式和策略模式,我们可以解决不同场景下的实际问题,编写更加优雅和高效的代码。
159 8
|
6月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
10月前
|
JSON 前端开发 JavaScript
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。
306 0
Next.js 实战 (三):优雅的实现暗黑主题模式
|
10月前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
128 2