操刀 requirejs,自己动手写一个
前沿 写在文章的最前面
这篇文章讲的是,我怎么去写一个 requirejs 。
requirejs,众所周知,是一个非常出名的js模块化工具,可以让你使用模块化的方式组织代码,并异步加载你所需要的部分。balabala 等等好处不计其数。
之所以写这篇文章,是做一个总结。目前打算动一动,换一份工作。感谢 一线码农 大大帮忙推了携程,得到了面试的机会。
面试的时候,聊着聊着感觉问题都问在了自己的“点”上,应答都挺顺利,于是就慢慢膨胀了。在说到模块化的时候,我脑子一抽,凭着感觉说了一下requirejs实现的大概步骤,充满了表现欲望,废话一堆。侥幸不可能当场让我写一遍,算是过了,事后尝试了一下,在这里跟大家分享一下我的实现。
结构划分
上面是我划分的项目结构:
- tool,
工具模块
,存放便捷方法,很多地方需要用到。 - async,异步处理模块,主要实现了
promise
和deferred
。逻辑上的异步。 - requirejs ->
loader
,amd加载器,处理模块的依赖和异步加载。物理上的异步。
因为对于异步流程控制方面,研究过一段时间,所以这里第一时间想到的就是 promise ,如果用这个来做,所有的模块放入字典,路径做key,promise做value,所有依赖都结束之后,才进行下一步操作。 不用管复杂的依赖关系,把逻辑尽量简单化:
- 首先有一个字典,存放所有的模块。key放地址,value放promise,promise在模块加载完毕的时候resolve。
- 如果依赖某个模块,先根据路径从字典找key,存在就用该promise,不存在就去加载该模块并放入字典,并使用该模块的promise。
- 所有的模块,我只用它的 promise ,在它的回调中写我的后续操作。它的resolve应该单独抽离出来,属于异步加载方面。
大致思路有了,当然实际写的时候肯定困难重重,不过没关系,遇到问题再去解决。
考虑到代码的简易性,以及我的个人习惯。我打算用类似于 jquery 的 $.Deferred() 和它的promise
,与es6的promise有一定的出入。这样代码书写更简易,并且逻辑上更清晰,es6的promise用起来确实稍显麻烦。我需要的是一个 pub/sub
模式,一个地方触发,多个回调执行的并行方式,es6的promise,需要在then中一次次返回,并且resolve起来也不方便,最最主要的是需要 polyfill 一下,而我想自己写,写我熟悉且喜欢的代码 。
callbacks模块
回调模块 callbacks
,熟悉jquery的朋友接下来可能会觉得使用方式很熟悉,没错,我受jq的影响算是比较深的。以前在学习jq源码的时候,就觉得这个很好用,你可以从我的代码里面看到jq的影子 :
import _ from '../tool/tool'; /**
* 基础回调模块
*
* @export
* @returns callbacks
*/
export default function () {
let list = [],
_args = (arguments[0] || '').split(' '), // 参数数组
fireState = 0, // 触发状态 0-未触发过 1-触发中 2-触发完毕
stopOnFalse = ~_args.indexOf('stopOnFalse'), // stopOnFalse - 如果返回false就停止
once = ~_args.indexOf('once'), // once - 只执行一次,即执行完毕就清空
memory = ~_args.indexOf('memory') ? [] : null, // memory - 保持状态
fireArgs = []; // fire 参数 /**
* 添加回调函数
*
* @param {any} cb
* @returns callbacks
*/
function add(cb) {
if (memory && fireState == 2) { // 如果是memory模式,并且已经触发过
cb.apply(null, fireArgs);
} if (disabled()) return this; // 如果被disabled list.push(cb);
return this;
} /**
* 触发
*
* @param {any} 任意参数
* @returns callbacks
*/
function fire() {
if (disabled()) return this; // 如果被禁用 fireArgs = _.makeArray(arguments); // 保存 fire 参数 fireState = 1; // 触发中 _.each(list, (index, cb) => { // 依次触发回调
if (cb.apply(null, fireArgs) === false && stopOnFalse) { // stopOnFalse 模式下,遇到false会停止触发
return false;
}
}); fireState = 2; // 触发结束 if (once) disable(); // 一次性列表 return this;
} function disable() { // 禁止
list = undefined;
return this;
} function disabled() { // 获取是否被禁止
return !list;
} return {
add: add,
fire: fire,
disable: disable,
disabled: disabled
};
}
这是一个工厂方法,每次所需的对象由该方法生成,用闭包来隐藏局部变量,私有方法。而最后暴露(发布)出来的对象,用 pub/sub 模式,提供了 订阅
, 触发
,禁用
,查看禁用
4个方法。 这里要说的是 ,提供了3个参数:stopOnFalse
、once
、memory
。触发的时候,按照订阅顺序依次触发,如果是 stopOnFalse
模式,当某个订阅的函数,返回是 false 的时候,停止整个触发过程。 如果是 once
,表示每个函数只能执行一次,在执行过后,会被移除队列。而 memory
状态下,在 callback 触发后,会被保持状态,之后添加的方法,添加后会直接执行。
这三种模式,传参的时候直接传入字符串,可以随意组合,用空格分开,比如:callbacks('once memory')
该模块用于整个项目中,处理所有的回调。使用方式类似于jquery的:$.Callbacks(...)
deferred 模块
deferred ,是对promise的父级模块,主要提供了 触发 和 订阅 2个方法。 promise 是对 deferred 的一个再封装,仅仅暴露出其中的 订阅 方法。
从概念上来说,很像 C# 中的委托和事件。
import _ from '../tool/tool';
import callbacks from './callbacks'; /**
* deferred 模块
*
* @export
* @returns deferred
*/
export default function () {
let tuples = [ // 用于存放一系列回调的 tuple 结构
// 方法名 - 接口名称 - 回调列表 - 最终状态
['resolve', 'then', callbacks('once memory'), 'resolved'],
['reject', 'catch', callbacks('once memory'), 'rejected']
]; let _state = 'pending'; // 当前状态 let dfd = { // 返回的延迟对象
state: function () {
return _state;
}, // 状态
promise: function () { // promise - 仅提供接口用于注册/订阅
let self = this;
let pro = {
state: self.state
};
_.each(tuples, (i, tuple) => { // 订阅接口
pro[tuple[1]] = self[tuple[1]];
});
return pro;
}
}; _.each(tuples, (i, tuple) => {
dfd[tuple[0]] = function () { // 触发
if (_state != "pending") return this;
tuple[2].fire.apply(tuple[2], _.makeArray(arguments));
_state = tuple[3];
return this;
};
dfd[tuple[1]] = function (cb) { // 绑定
tuple[2].add(cb);
return this;
};
}); return dfd;
}
deferred
使用了 callbacks
模块来处理其中所有的回调函数。是一个工厂方法,deferred()
返回的是一个deferred对象(发布),包含了3种状态:pending
,resolved
,rejected
;提供了 then
和 catch
去订阅;通过 resolve
和 reject
去 改变(触发) 状态。
deferred 对象,提供了一个 promise() 方法去返回一个promise对象,区别就是promise对象屏蔽了触发的方法。就像委托和事件,前者可以订阅和触发,而后者只能订阅。之所以如此,是想只提供订阅的接口,而如何触发,何时触发,由我自己控制,是我逻辑内部的事情,而其他部分,只需要知道也只能去订阅。
Tuple ,是一种约定的、按照某个规则进行存储的数据结构(类?), c# ,typescript 中都有这个东西,之前在学习jq的时候,看到了它的内部也这么用,于是学到了。其实在我看来,使用tuple,就是节约代码,笑。不必要去定义某个类,或者其他的东西,只需要在定义和使用的时候,遵循某个约定好的规则,那么就可以省去一大堆的代码,让逻辑部分也清晰不少。
all 模块
import deferred from './deferred';
import _ from '../tool/tool'; export default function (promises) {
promises = _.makeArray(promises);
let len = promises.length, // promise 个数
resNum = 0, // resolve 的数量
argsArr = new Array(len), // 每个reject的参数
dfd = deferred(), // 用于当前task控制的deferred
pro = dfd.promise(); // 用于当前返回的promise if (len === 0) { // 如果是个空数组,直接就返回了
dfd.resolve();
return pro;
} function addThen() { // 检测是否全部完成
resNum++;
let args = _.makeArray(arguments);
let index = args.shift(); // 当前参数在promises中的索引 if (args.length <= 1) { // 保存到数组,用户回调
argsArr[index] = args[0];
} else {
argsArr[index] = args;
} if (resNum >= len) { // 如果所有promise都resolve完毕
dfd.resolve(argsArr);
}
} function addCatch() { // 如果某个promise发生了reject
var args = _.makeArray(arguments);
dfd.reject(...args);
} _.each(promises, (index, promise) => {
promise.then(function () {
addThen(index, ...arguments);
}).catch(addCatch);
}); return pro;
}
all,其实就是es6中, Promise.all
或者 $.when
的一种实现。参数是一系列的promise,本身返回一个promise对象,在所有参数中的promise对象都处于 resolved状态
时,本身也会被resolve掉,由此来执行通过then订阅的方法。
all本身,是通过一个触发器来实现在最后一个promise完成时回调。内部用一个int值来存储resolved的参数的个数,给每个参数通过 then 添加一个回调来执行这个触发器,当 完成数量 >= 参数个数
的时候,就表示所有promise已经完成,可以进行后续的操作。 用 >= 来代替 == 是个好习惯 :D
模块分析 模块定义、模块获取
到此为止,async 部分已经完成,准备工作已经做好。我们开始 amd 模块部分的分析。
amd 模块在我看来,主要分为两个部分:模块定义
、模块获取
。先说模块获取:
模块获取
模块的获取,并不复杂。先从字典中根据路径(key)去找该模块,如果有该模块,就去加载。如果不存在,就去加载该js,根据onload来确定该模块的名称(如果是匿名模块);然后根据该模块的返回值==》 一个promise,给该promise添加一个回调,去管理 getModule 的返回值状态==》另一个promise。在使用一个模块的时候,从本质上来讲,是给该模块的promise的then接口添加回调函数,一层层往下处理。
模块定义
这里的重点是 加载模块,大家都知道,amd的每个模块,对应一个js文件,加载模块就是去加载这个js。
再看看模块的定义,有 3种重载:
- define(sender)
- define(deps,sender)
- define(name,deps,sender)
sender 是一个函数,或者某个对象。deps 是一个数组,表示该模块依赖的其他模块。name 是表示当前模块是一个命名模块,强制使用该名称,一般是打包工具生成这种模块,不建议自己直接这么写。
从上面我们可以看到,模块是通过执行一个函数,用传参的方式把所要用到的模块加载到某个地方保存起来。那么看到这个你们有没有想到什么呢?我首先想到的就是 jsonp ,动态执行一个函数,把数据放进去,对得上,完美。从这个思路,我实验了一下,在这里直接说结论: script标签在动态加载到页面后,首先去服务器拿对应地址的数据,然后在文件下载完全后,执行该js文件中的内容,执行完毕后,会触发该script标签的load事件。
也就是说,通过给load事件注册方法,我们可以知道最后一个加载的模块(js文件),来自哪里,什么时候执行完全。这样就确定了,并行加载多个js文件时,匿名模块所属来源。这里不讨论兼容的问题,低版本ie对应的是其他事件:onreadystatechange,我没用过。
在模块加载后,我们用一个函数来将模块填充到字典中,类似于一个 触发器
,每次加载一个模块,模块中包含这个函数并执行,处理依赖关系,并将最后的结果保存。
在模块的加载中,因为可能会同时加载多个模块(js文件),并不能确定到底是哪一个先加载完全。但是我们知道,js是单线程,在js文件下载完全后,会先把js文件中的内容执行完毕,然后再触发load事件,这个顺序是可以保证的,所以就可以使用一个变量来保存最近加载的模块,来知道匿名模块的所属路径。
不论是匿名模块,还是命名模块,都可能依赖其他的模块,所以并不能确定在模块加载完之后,就可以立即使用,要等待所有的依赖项都加载完毕,所以一个模块的最终返回值我使用的一个promise来保存。这样就可以方便的在状态变更后才添加下一步的处理操作,从逻辑上简化整个流程控制。
模块入口 require
/**
* 程序入口, require
*
* @export
* @param {any} deps 依赖项
* @param {any} callback 程序入口
*/
export function requireModule(deps, callback) {
setTimeout(function () { // 避免阻塞同文件中,使用名称定义的模块
deps = deps.map(url => getModule(_.resolvePath(core.rootUrl, url)));
all(deps).then(function (args) {
callback(...args);
});
}, 0);
}
这里的代码比较简单,唯一要注意的就是这个 setTimeout(action,0)
。因为js是单线程,从上往下依次执行。模块可能会被打包工具合并成一个文件,那么在一个文件中就含有了模块入口、命名模块。如果模块入口在最上方,,,在依赖某个命名模块的时候,就会试图去加载这个名称的js文件,而这注定是会失败的。所以使用一个setTimeout,把模块入口的逻辑,放入事件队列中,让js逻辑线程优先去执行文件后面的代码,就避免了这个问题。
loader 模块代码
import core from './core';
import deferred from './async/deferred';
import all from './async/all';
import _ from './tool/tool'; let lastNameDfd = null; // 最后一个加载的module的name的 deferred /**
* 程序入口, require
*
* @export
* @param {any} deps 依赖项
* @param {any} callback 程序入口
*/
export function requireModule(deps, callback) {
setTimeout(function () { // 避免阻塞同文件中,使用名称定义的模块
deps = deps.map(url => getModule(_.resolvePath(core.rootUrl, url)));
all(deps).then(function (args) {
callback(...args);
});
}, 0);
} /**
* 模块定义,url,deps,sender
*
* @export
*/
export function defineModule() {
let args = _.makeArray(arguments);
let name = "", // 模块名称
proArr, // 模块依赖
sender; // 模块的主体 let argsLen = args.length; // 参数的个数,用来重载 if (argsLen == 1) { // 重载一下 sender
proArr = [];
sender = args[0];
}
else if (argsLen == 2) { // deps,sender
proArr = args[0];
sender = args[1];
}
else if (argsLen == 3) { // name,deps,sender
name = args[0];
proArr = args[1];
sender = args[2];
}
else {
throw Error('参数个数异常');
} let dfdThen = (_name, lastModule) => {
_name = _.normalizePath(_name); // 名称,路径 proArr = proArr.map(url => { // 各个依赖项
url = _.resolvePath(_name, url); // 以当前路径为基准,合并路径
return getModule(url);
}); all(proArr).then(function (_args) { // 在依赖项加载完毕后,进行模块处理
_args = _args || [];
let result; // 最终结果
let _type = _.type(sender); // 回调模块类型 if (_type == "function") {
result = sender(..._args);
}
else if (_type == "object") {
result = sender;
}
else {
throw Error("参数类型错误");
} lastModule.resolve(result); });
}; if (argsLen < 3) { // 如果是匿名模块,使用 onload 来判断js的名称/路径
lastNameDfd = deferred(); // 先获取当前模块名称 lastNameDfd.then(dfdThen);
}
else { // 如果是自定义模块名,直接触发,命名模块直接添加
let lastModule = deferred();
let dictName = _.resolvePath(core.rootUrl, name);
core.dict[dictName] = lastModule; let namedDfd = deferred().then(dfdThen); setTimeout(function () { // 避免同文件中,多个命名模块注册阻塞,先把名字注册了,具体内容等待一下 event loop
namedDfd.resolve(dictName, lastModule);
}, 0);
} } /**
* 根据 路径/名称 ,加载/获取模块的promise
*
* @param {any} name
* @returns promise
*/
function getModule(name) {
let dict = core.dict;
if (dict[name]) {
return dict[name];
} let script = addScript(name); let dfd = deferred();
dict[name] = dfd; script.onload = function () { // 模块加载完毕,立马会触发 load 事件,由此来确定模块所属
let lastModule = deferred();
lastNameDfd.resolve(name, lastModule); // 绑定当前模块的名称 lastModule.then(result => { // 在模块加载完毕之后,触发该模块的 resolve
dfd.resolve(result);
});
}; return dfd.promise();
} /**
* 添加 script 标签
*
* @export
* @param {any} name
* @returns
*/
export function addScript(name) {
let script = document.createElement('script');
script.type = "text/javascript";
script.async = true;
script.charset = "utf-8";
script.src = name + ".js";
document.head.appendChild(script);
return script;
}
core 模块
/**
* 默认核心载体
*/
export default {
/**
* 版本
*/
ver: "0.0.1",
/**
* 模块定义名称
*/
defineName: "define",
/**
* 程序入口函数
*/
requireName: "require",
/**
* 暴露的全局名称,可用于配置
*/
coreName: "requirejs",
/**
* 根目录,入口文件目录
*/
rootUrl: "",
/**
* 依赖模块存储字典
*/
dict: { // 模块字典 {key:string,value:promise} }
};
core,主要存的是一些配置信息,和模块的字典,比较简单。
总结、Github
写到这里,就已经结束了。本文讲了对于requirejs,我的实现思路,列举了可能遇到的问题,及我的解决方式。希望能给大家的学习提供点帮助。
上面是github的地址,求star啊,作为一个虚荣的人,我对这个很看重的,哈哈,也就这点追求了。再次感激 一线码农 大哥的推荐,还有 linkFly 的经验指导。
操刀 requirejs,自己动手写一个的更多相关文章
- 动手写一个简单版的谷歌TPU-矩阵乘法和卷积
谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU.计划实现到行为 ...
- 死磕 java同步系列之自己动手写一个锁Lock
问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...
- 动手写一个简单版的谷歌TPU-指令集
系列目录 谷歌TPU概述和简化 基本单元-矩阵乘法阵列 基本单元-归一化和池化(待发布) TPU中的指令集 SimpleTPU实例: (计划中) 拓展 TPU的边界(规划中) 重新审视深度神经网络中的 ...
- 死磕 java线程系列之自己动手写一个线程池
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...
- 自己动手写一个服务网关-java
自己动手写一个服务网关 原文链接:https://www.cnblogs.com/bigben0123/p/9252444.html 引言 什么是网关?为什么需要使用网关? 如图所示,在不使用网关的情 ...
- 动手写一个简单的Web框架(模板渲染)
动手写一个简单的Web框架(模板渲染) 在百度上搜索jinja2,显示的大部分内容都是jinja2的渲染语法,这个不是Web框架需要做的事,最终,居然在Werkzeug的官方文档里找到模板渲染的代码. ...
- 动手写一个简单的Web框架(Werkzeug路由问题)
动手写一个简单的Web框架(Werkzeug路由问题) 继承上一篇博客,实现了HelloWorld,但是这并不是一个Web框架,只是自己手写的一个程序,别人是无法通过自己定义路由和返回文本,来使用的, ...
- 动手写一个简单的Web框架(HelloWorld的实现)
动手写一个简单的Web框架(HelloWorld的实现) 关于python的wsgi问题可以看这篇博客 我就不具体阐述了,简单来说,wsgi标准需要我们提供一个可以被调用的python程序,可以实函数 ...
- 死磕 java线程系列之自己动手写一个线程池(续)
(手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的 ...
随机推荐
- PHP中如何连接数据库基本语句
只是后端修改页面,不需要在前端显示的可以删除原有代码只输入<?php 开始编写语言即可,后面的?>也可以省略 //造一个连接$connect = @mysql_connect(" ...
- mp3 切割
开源的东东很不错,摘了一段好文: 常在听mp3或其他格式音乐的朋友,有时会有特别喜欢的片段,例如副歌的部份会想拿来做手机的铃声.这时候就需要一些处理音效的软体,例如之前提过的 Audacity.其实还 ...
- c++ ip地址相关
#include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <sys/typ ...
- 将通过find命令找到的文件拷贝到一个新的目录中
将通过find命令找到的文件拷贝到一个新的目录中 有这样的一个需求,需要将一部分符合条件的文件从一个目录拷贝到另一个目录中,我通过find命令从源目录查找到符合条件的文件然后使用cp命令拷贝到目标目录 ...
- 第一个 bat 文件
要写一个批处理命令 转换场景数据 包括从文件里读入 每一行信息是一个要转换的场景名字 可以拼出路径 到指定路径 执行命令 http://blog.csdn.net/mfx1986/article/de ...
- jquery 常用组件的小代码
获得所有复选框的值 function getAllValue() { var str=""; $("input[name='checkbox']:checkbox&quo ...
- C++中static的全部作用
要理解static,就必须要先理解另一个与之相对的关键字,很多人可能都还不知道有这个关键字,那就是auto,其实我们通常声明的不用static修饰的变量,都是auto的,因为它是默认的,就象short ...
- easyui 页签
昨天开始搭后台框架,到晚上的时候遇到了一个现在觉得挺可笑但是当时一直很纠结很纠结的问题,这个问题刚刚解决出来,把它拿出来说说,让自己长点儿记性,希望大家不要犯我这个错误啊 在backstage.jsp ...
- HDU 4825 Xor Sum(二进制的字典树,数组模拟)
题目 //居然可以用字典树...//用cin,cout等输入输出会超时 //这是从别处复制来的 #include<cstdio> #include<algorithm> #in ...
- WPF、Windows Forms和Silverlight间的联系和区别(转)
WPF.Windows Forms和Silverlight间的联系和区别http://blog.csdn.net/bitfan/article/details/6128391 .NET Windows ...