javascript基础修炼(12)——手把手教你造一个简易的require.js

目录

  • 一. 概述
  • 二. require.js
    • 2.1 基本用法
    • 2.2 细说API设计
  • 三. 造轮子
    • 3.1 模块加载执行的步骤
    • 3.2 代码框架
    • 3.3 关键函数的代码实现

示例代码托管在我的代码仓:http://www.github.com/dashnowords/blogs

博客园地址:《大史住在大前端》原创博文目录

华为云社区地址:【你要的前端打怪升级指南】

一. 概述

许多前端工程师沉浸在使用脚手架工具的快感中,认为require.js这种前端模块化的库已经过气了,的确如果只从使用场景来看,在以webpack为首的自动化打包趋势下,大部分的新代码都已经使用CommonJsES Harmony规范实现前端模块化,require.js的确看起来没什么用武之地。但是前端模块化的基本原理却基本都是一致的,无论是实现了模块化加载的第三方库源码,还是打包工具生成的代码中,你都可以看到类似的模块管理和加载框架,所以研究require.js的原理对于前端工程师来说几乎是不可避免的,即使你绕过了require.js,也会在后续学习webpack的打包结果时学习类似的代码。研究模块化加载逻辑对于开发者理解javascript回调的运行机制非常有帮助,同时也可以提高抽象编程能力。

二. require.js

2.1 基本用法

require.js是一个实现了AMD(不清楚AMD规范的同学请戳这里【AMD模块化规范】)模块管理规范的库(require.js同时也能够识别CMD规范的写法),基本的使用方法也非常简单:

  1. 类库引入,在主页index.html中引入require.js:

    <script src="require.js" data-main="main.js"></script>

    data-main自定义属性指定了require.js完成初始化后应该加载执行的第一个文件。

  2. main.js中调用require.config传入配置参数,并通过require方法传入主启动函数:
    //main.js
    require.config((
        baseUrl:'.',
        paths:{
           jQuery:'lib/jQuery.min',
           business1:'scripts/business1',
           business2:'scripts/business2',
           business3:'scripts/business3'
        }
    ))
    
    require(['business1','business2'],function(bus1,bus2){
         console.log('主函数执行');
         bus2.welcome();
    });  
  3. 模块定义通过define函数定义
    define(id?:string, deps?:Array<string>, factory:function):any
  4. 访问index.html后的模块加载顺序:

    访问的顺序从require方法执行开始打乱,main.js中的require方法调用声明了对business1business2两个模块的依赖,那么最后一个参数(主方法)不会立即解析,而是等待依赖模块加载,当下载到定义business1模块的文件scripts/business1.js后,写在该文件中的define方法会被执行,此时又发现当前模块依赖business3模块,程序又会延迟生成business1模块的工厂方法(也就是scripts/business1.js中传入define方法的最后一个函数参数),转而先去加载business3这个模块,如果define方法没有声明依赖,或者声明的依赖都已经加载,就会执行传入的工厂方法生成指定模块,不难理解模块的解析是从叶节点开始最终在根节点也就是主工厂函数结束的。

    所以模块文件加载顺序和工厂方法执行顺序基本是相反的,最先加载的模块文件中的工厂方法可能最后才被运行(也可能是乱序,但符合依赖关系),因为需要等待它依赖的模块先加载完成,运行顺序可参考下图(运行结果来自第三节中的demo):

2.2 细说API设计

require.js在设计上贯彻了多态原则,API非常精简。

模块定义的方法只有一个define,但是包含了非常多情况:

  • 1个参数

    • function类型

      将参数判定为匿名模块的工厂方法,仅起到作用域隔离的作用。

    • object类型

      将模块识别为数据模块,可被其他模块引用。

  • 2个参数
    • string+function | object

      第一参数作为模块名,第二参数作为模块的工厂方法或数据集。

    • array<string>+function | object

      第一参数作为依赖列表,第二参数作为匿名模块工厂方法或数据集。

  • 3个参数

    第一个参数作为模块名,第二个参数作为依赖列表,第三个参数作为工厂方法或数据集。

  • deps : array<string>依赖列表中成员的解析
    • 包含/./../

      判定为依赖资源的地址

    • 不包含上述字符

      判定为依赖模块名

模块加载方法require也是诸多方法的集合:

  • 1个参数

    • string类型

      按照模块名或地址来加载模块。

    • array类型

      当做一组模块名或地址来加载,无加载后回调。

  • 2个参数

    第一个参数作为依赖数组,第二个参数作为工厂方法。

在这样的设计中,不同参数类型对应的函数重载在require.js内部进行判定分发,使得由用户编写的调用逻辑显得更加简洁一致。

三. 造轮子

作为前端工程师,只学会使用方法是远远不够的,本节中我们使用“造轮子”的方法造一个简易的require.js,以便探究其中的原理。本节使用的示例中,先加载require.js,入口文件为main.js,主逻辑中前置依赖为business1business2两个模块,business1依赖于business3模块,business2依赖于jQuery。如下所示:

3.1 模块加载执行的步骤

上一节在分析require.js执行步骤时我们已经看到,当一个模块依赖于其他模块时,它的工厂方法(requiredefine的最后一个参数)是需要先缓存起来的,程序需要等待依赖模块都加载完成后才会执行这个工厂方法。需要注意的是,工厂方法的执行顺序只能从依赖树的叶节点开始,也就是说我们需要一个栈结构来限制它的执行顺序,每次先检测栈顶模块的依赖是否全部下载解析完毕,如果是,则执行出栈操作并执行这个工厂方法,然后再检测新的栈顶元素是否满足条件,以此类推。

define方法的逻辑是非常类似的,现在moduleCache中登记一个新模块,如果没有依赖项,则直接执行工厂函数,如果有依赖项,则将工厂函数推入unResolvedStack待解析栈,然后依次对声明的依赖项调用require方法进行加载。

我们会在每一个依赖的文件解析完毕触发onload事件时将对应模块的缓存信息中的load属性设置为true,然后执行检测方法,来检测unResolvedStack的栈顶元素的依赖项是否都已经都已经完成解析(解析完毕的依赖项在moduleCache中记录的对应模块的load属性为true),如果是则执行出栈操作并执行这个工厂方法,然后再次运行检测方法,直到栈顶元素当前无法解析或栈为空。

3.2 代码框架

我们使用基本的闭包自执行函数的代码结构来编写requireX.js(示例中只实现基本功能):

;(function(window, undefined){
    //模块路径记录
    let modulePaths = {
        main:document.scripts[0].dataset.main.slice(0,-3) //data-main传入的路径作为跟模块
    };
    //模块加载缓存记录
    let moduleCache = {};
    //待解析的工厂函数
    let unResolvedStack = [];
    //匿名模块自增id
    let anonymousIndex = 0;
    //空函数
    let NullFunc =()=>{};

    /*moduleCache中记录的模块信息定义*/
    class Module {
        constructor(name, path, deps=[],factory){
            this.name = name;//模块名
            this.deps = deps;//模块依赖
            this.path = path;//模块路径
            this.load = false;//是否已加载
            this.exports = {};//工厂函数返回内容
            this.factory = factory || NullFunc;//工厂函数
        }
    }

    //模块加载方法
    function _require(...rest){
        //...
    }

    //模块定义方法
    function _define(...rest){

    }

    //初始化配置方法
    _require.config = function(conf = {}){

    }

    /**
    *一些其他的内部使用的方法
    */

    //全局挂载
    window.require = _require;
    window.define = _define;

    //从data-main指向开始解析
    _require('main');

})(window);

3.3 关键函数的代码实现

下面注释覆盖率超过90%了,不需要再多说什么。

  1. 加载方法_require(省略了许多条件判断,只保留了核心逻辑)
    function _require(...rest){
        let paramsNum = rest.length;
        switch (paramsNum){
            case 1://如果只有一个字符串参数,则按模块名对待,如果只有一个函数模块,则直接执行
                if (typeof rest[0] === 'string') {
                    return _checkModulePath(rest[0]);
                }
            break;
            case 2:
                if (Object.prototype.toString.call(rest[0]).slice(8,13) === 'Array' && typeof rest[1] === 'function'){
                    //如果依赖为空,则直接运行工厂函数,并传入默认参数
                    return _define('anonymous' + anonymousIndex++, rest[0], rest[1]);
                }else{
                    throw new Error('参数类型不正确,require函数签名为(deps:Array<string>, factory:Function):void');
                }
            break;
        }
    }

如果传入一个字符,则将其作为模块名传入_checkModulePath方法检测是否有注册路径,如果有路径则去获取定义这个模块的文件,如果传入两个参数,则运行_define方法将其作为匿名模块的依赖和工厂函数处理。

  1. 模块定义方法_define
    function _define(id, deps, factory){
        let modulePath = modulePaths[id];//获取模块路径,可能是undefined
        let module = new Module(id, modulePath, deps, factory);//注册一个未加载的新模块
        moduleCache[id] = module;//模块实例挂载至缓存列表
        _setUnResolved(id, deps, factory);//处理模块工厂方法延迟执行逻辑
    }
  1. 延迟执行工厂方法的函数_setUnResolved
    function _setUnResolved(id, deps, factory) {
        //压栈操作缓存要延迟执行的工厂函数
        unResolvedStack.unshift({id, deps,factory});
        //遍历依赖项数组对每个依赖执行检测路径操作,检测路径存在后对应的是js文件获取逻辑
        deps.map(dep=>_checkModulePath(dep));
    }
  1. 模块加载逻辑_loadModule
    function _loadModule(name, path) {
        //如果存在模块的缓存,表示已经登记,不需要再次获取,在其onload回调中修改标记后即可被使用
        if(name !== 'root' && moduleCache[name]) return;
        //如果没有缓存则使用jsonp的方式进行首次加载
        let script = document.createElement('script');
            script.src = path + '.js';
            script.defer = true;
            //初始化待加载模块缓存
            moduleCache[name] = new Module(name,path);
            //加载完毕后回调函数
            script.onload = function(){
                //修改已登记模块的加载解析标记
                moduleCache[name].load = true;
                //检查待解析模块栈顶元素是否可解析
                _checkunResolvedStack();
            }
            console.log(`开始加载${name}模块的定义文件,地址为${path}.js`);
            //开始执行脚本获取
            document.body.appendChild(script);
    }
  1. 检测待解析工厂函数的方法_checkunResolvedStack
    function _checkunResolvedStack(){
        //如果没有待解析模块,则直接返回
        if (!unResolvedStack.length)return;
        //否则查看栈顶元素的依赖是否已经全部加载
        let module = unResolvedStack[0];
        //获取声明的依赖数量
        let depsNum = module.deps.length;
        //获取已加载的依赖数量
        let loadedDepsNum = module.deps.filter(item=>moduleCache[item].load).length;
        //如果依赖已经全部解析完毕
        if (loadedDepsNum === depsNum) {
            //获取所有依赖的exports输出
            let params = module.deps.map(dep=>moduleCache[dep].exports);
            //运行待解析模块的工厂函数并挂载至解析模块的exports输出
            moduleCache[module.id].exports = module.factory.apply(null,params);
            //待解析模块出栈
            unResolvedStack.shift();
            //递归检查
            return _checkunResolvedStack();
        }
    }

示例的效果是页面中提示语缓慢显示出来。的完整的示例代码可从篇头的github仓库中获取,欢迎点星星。

原文地址:https://www.cnblogs.com/dashnowords/p/10816039.html

时间: 2024-10-02 20:41:53

javascript基础修炼(12)——手把手教你造一个简易的require.js的相关文章

javascript基础修炼——一道十面埋伏的原型链面试题

javascript基础修炼--一道十面埋伏的原型链面试题 在基础面前,一切技巧都是浮云. 题目是这样的 要求写出控制台的输出. function Parent() { this.a = 1; this.b = [1, 2, this.a]; this.c = { demo: 5 }; this.show = function () { console.log(this.a , this.b , this.c.demo ); } } function Child() { this.a = 2;

javascript基础修炼(2)——What‘s this(上)

javascript基础修炼(2)--What's this(上) 开发者的javascript造诣取决于对[动态]和[异步]这两个词的理解水平. [TOC] 一.this是什么 this是javascript关键字之一,是javascript能够实现面向对象编程的核心概念.用得好能让代码优雅高端,风骚飘逸,用不好也绝对是坑人坑己利器.我们常常会在一些资料中看到对this的描述是: this是一个特殊的与Execution Contexts相关的对象,用于指明当前代码执行时的Execution

javascript基础修炼(7)——Promise,异步,可靠性

开发者的javascript造诣取决于对[动态]和[异步]这两个词的理解水平. 一. 别人是开发者,你也是 Promise技术是[javascript异步编程]这个话题中非常重要的,它一度让我感到熟悉又陌生,我熟悉其所有的API并能够在编程中相对熟练地运用,却对其中原理和软件设计思想感到陌生,即便我读了很多源码分析和教程也一度很难理解为什么Promise这样一个普通的类能够实现异步,也曾尝试着去按照Promise/A+规范来编写Promise,但很快便陷入了一种更大的混乱之中.直到我接触到一些软

javascript基础修炼(8)——指向FP世界的箭头函数

一. 箭头函数 箭头函数是ES6语法中加入的新特性,而它也是许多开发者对ES6仅有的了解,每当面试里被问到关于"ES6里添加了哪些新特性?"这种问题的时候,几乎总是会拿箭头函数来应付.箭头函数,=>,没有自己的this , arguments , super , new.target ,"书写简便,没有this"在很长一段时间内涵盖了大多数开发者对于箭头函数的全部认知(当然也包括我自己),如果只是为了简化书写,把=>按照function关键字来解析就好了

javascript基础修炼(10)——VirtualDOM和基本DFS

1. Virtual-DOM是什么 Virtual-DOM,即虚拟DOM树.浏览器在解析文件时,会将html文档转换为document对象,在浏览器环境中运行的脚本文件都可以获取到它,通过操作document对象暴露的接口可以直接操作页面上的DOM节点.但是DOM读写是非常耗性能的,很容易触发不必要的重绘和重排,为了更好地处理DOM操作,Virtual-DOM技术就诞生了.Virtual-DOM就是在javascript中模拟真实DOM的结构,通过数据追踪和状态对比来减少对于真实DOM的操作,以

javascript基础修炼(1)——一道十面埋伏的原型链面试题

在基础面前,一切技巧都是浮云. 题目是这样的 要求写出控制台的输出. function Parent() { this.a = 1; this.b = [1, 2, this.a]; this.c = { demo: 5 }; this.show = function () { console.log(this.a , this.b , this.c.demo ); } } function Child() { this.a = 2; this.change = function () { th

javascript基础修炼(5)—Event Loop

开发者的javascript造诣取决于对[动态]和[异步]这两个词的理解水平. 一. 一道考察异步知识的面试题 题目是这样的,要求写出下面代码的输出: setTimeout(() => { console.log(1) }, 0) new Promise((resolve, reject) => { console.log(2) for (let i = 0; i < 10000; i++) { i === 9999 && resolve() } console.log(

javascript基础修炼(6)——前端路由的基本原理

[造轮子]是笔者学习和理解一些较复杂的代码结构时的常用方法,它很慢,但是效果却胜过你读十几篇相关的文章.为已知的API方法自行编写实现,遇到自己无法复现的部分再有针对性地去查资料,最后当你再去学习官方代码的时候,就会明白这样做的价值,总有一天,你也将有能力写出大师级的代码. 一. 前端路由 现代前端开发中最流行的页面模型,莫过于SPA单页应用架构.单页面应用指的是应用只有一个主页面,通过动态替换DOM内容并同步修改url地址,来模拟多页应用的效果,切换页面的功能直接由前台脚本来完成,而不是由后端

Python爬虫基础教程,手把手教你爬取拉勾网!

一.思路分析: 在之前写拉勾网的爬虫的时候,总是得到下面这个结果(真是头疼),当你看到下面这个结果的时候,也就意味着被反爬了,因为一些网站会有相应的反爬虫措施,例如很多网站会检测某一段时间某个IP的访问次数,如果访问频率太快以至于看起来不像正常访客,它可能就会禁止这个IP的访问: 对于拉勾网,我们要找到职位信息的ajax接口倒是不难(如下图),问题是怎么不得到上面的结果. 要想我们的爬虫不被检测出来,我们可以使用代理IP,而网上有很多提供免费代理的网站,比如西刺代理.快代理.89免费代理等等,我