庖丁解牛layui源码第一弹:layui.use源码

动态 精帖 已结 6 1306
江天 VIP1 2018年10月12日 08:48:40
悬赏:5积分
<p>为了更好的应用layui来开发项目,读一遍源码比看文档记忆更深刻且知其然知其所以然。这是我的习惯。</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>n.prototype.use = function(e, n, l) { function s(e, t) { // different n{<img class="larryms-face" alt="[神马]" title="[神马]" src="http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/horse2_thumb.gif">}igator platform var n = "PLaySTATION 3" === n{<img class="larryms-face" alt="[神马]" title="[神马]" src="http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/horse2_thumb.gif">}igator.platform ? /^complete$/ : /^(complete|loaded)$/; ("load" === e.type || n.test((e.currentTarget || e.srcElement).readyState)) && (o.modules[f] = t, d.removeChild(v), function r() { return ++m > 1e3 * o.timeout / 4 ? i(f + " is not a valid module") : void(o.status[f] ? c() : setTimeout(r, 4)) }()) } function c() { l.push(layui[f]), e.length > 1 ? y.use(e.slice(1), n, l) : "function" == typeof n && n.apply(layui, l) } var y = this, p = o.dir = o.dir ? o.dir : r, d = t.getElementsByTagName("head")[0]; // if string, change it to array, check jQuery exists e = "string" == typeof e ? [e] : e, window.jQuery && jQuery.fn.on && (y.each(e, function(t, o) { // if add jquery module, delete it, because it has been loaded "jquery" === o && e.splice(t, 1) }), layui.jquery = layui.$ = jQuery); // the first module var f = e[0], m = 0; if (l = l || [], o.host = o.host || (p.match(/\/\/([\s\S]+?)\//) || ["//" + location.host + "/"])[0], 0 === e.length || layui["layui.all"] && u[f] || !layui["layui.all"] && layui["layui.mobile"] && u[f]) return c(), y; if (o.modules[f])! function g() { return ++m > 1e3 * o.timeout / 4 ? i(f + " is not a valid module") : void("string" == typeof o.modules[f] && o.status[f] ? c() : setTimeout(g, 4)) }(); else { var v = t.createElement("script"), h = (u[f] ? p + "lay/" : /^\{\/\}/.test(y.modules[f]) ? "" : o.base || "") + (y.modules[f] || f) + ".js"; h = h.replace(/^\{\/\}/, ""), v.async = !0, v.charset = "utf-8", v.src = h + function() { var e = o.version === !0 ? o.v || (new Date).getTime() : o.version || ""; return e ? "?v=" + e : "" }(), d.appendChild(v), !v.attachEvent || v.attachEvent.toString && v.attachEvent.toString().indexOf("[native code") < 0 || a ? v.addEventListener("load", function(e) { s(e, h) }, !1) : v.attachEvent("onreadystatechange", function(e) { s(e, h) }), o.modules[f] = h } return y }</li></ol></code></pre><p> </p><p>use函数有三个参数,参数一模块数组,参数二回调,参数三为数组,动态存储use进的layui module,由字符串变成layui module对象。</p><p> </p><p>前两个函数暂且略过,按函数s,c执行顺序来解析。</p><p> </p><span style="font-weight:bold;">一. </span>变量赋值:<p> </p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>var y = this, p = o.dir = o.dir ? o.dir : r, d = t.getElementsByTagName("head")[0];</li></ol></code></pre><p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">A. </span>将layui对象赋值给y;</p><p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">B. </span>内部临时变量o赋值给p,存储了modules、status、timeout、event、callback、dir、host、base、version、debug、v等内部默认配置及其他用户通过layui.config扩展进来的配置项,如果o.dir用户未设置,则使用r的值(一个自执行函数的结果,如下:)</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>r = function() { var e = t.currentScript ? t.currentScript.src : function() { for (var e, o = t.scripts, n = o.length - 1, r = n; r > 0; r--) if ("interactive" === o[r].readyState) { e = o[r].src; break } return e || o[n].src }(); return e.substring(0, e.lastIndexOf("/") + 1) }(),</li></ol></code></pre><p><span style="font-weight:bold;">PS:浏览器中,动态插入script标签与初次加载页面dom时的script加载方式不同:初次加载页面,浏览器会从上到下顺序解析dom,碰到script标签时,下载脚本并阻塞dom解析,等到该脚本下载、执行完毕后再继续解析之后的dom(现代浏览器做了preload优化,会预先下载好多个脚本,但执行顺序与它们在dom中顺序一致,执行时阻塞其他dom解析)动态插入script,var a = document.createElement('script'); a.src='xxx'; document.body.appendChild(a);浏览器会在该脚本下载完成后执行,过程是异步的。下载完成后,如何在解析它时知道它的url呢?有两种方法,一种是用srcipt.onload获取this对象的src属性;一种是采用document.currentScript.src。后者用来检测当前正在执行的脚本。</span></p><p><span style="font-weight:bold;"> </span></p><p>r自执行函数中,返回当前脚本除脚本名外的路径,比如layui.js。通过检测是否有currentScript来获取当前执行脚本路径,如果无该对象,则执行自执行匿名函数,循环所有已经加载到dom中的script标签集合,ps:</p><blockquote>“uninitialized” 初始状态,“loading” 开始下载,“loaded” 下载完成,“interactive” 数据完成下载但尚不可用,“complete” 所有数据已准备就绪。 </blockquote><p> </p><p>如果当前js文档处于已加载状态可交互状态,返回它的路径,否则返回dom中第一个脚本。</p><p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">C. </span>d获取到head标签元素。</p><p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">D.</span> 对use进的模块,如果是字符串装箱成数组[xx],此处<span style="font-weight:bold;">注意下,和=的优先级,后者高于前者。</span>紧接着检测widow上JQuery对象及原型上on方法是否存在,然后使用layui对象上的each函数循环传入的模块数组,闭包参数t是数组索引,o是数组值即传入的模块字符串。这里有必要看一下each函数的实现,如下:</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>n.prototype.each = function(e, t) { var o, n = this; if ("function" != typeof t) return n; if (e = e || [], e.constructor === Object) { for (o in e) if (t.call(e[o], o, e[o])) break } else for (o = 0; o < e.length && !t.call(e[o], o, e[o]); o++); return n }</li></ol></code></pre><p> </p><p>第一个参数是待处理数组,第二个为闭包回调。如果t非函数,直接返回layui对象。如果传入的e的构造函数是一个Object,即e是个对象,循环该对象,并对每个对象成员应用该闭包,闭包如果返回为真则停止。如果是数组,使用了无for循环体的处理方法,也是返回值为真则停止循环。简单来说,layui的each方法扩展了原生js的forEach方法,不仅支持数组,也支持对象的循环处理。此处检测传入的模块字符串是否有jquery,如果有,删除之。<span style="font-weight:bold;">splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。注释:该方法会改变原始数组,跟slice不同,后者不改变原始数组</span>。同时each方法会将layui对象返回到上下文,可用于后续链式执行。</p><p> </p><p>然后将JQuery对象赋值给layui的jquery和$属性。</p><p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">E.</span> f取值传入模块的第一个。m赋值为0备用。</p><p> </p>二. 执行<p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">F. </span>判断传入的第三个参数是否存在,不存在赋空数组。</p><p> </p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>if (l = l || [], o.host = o.host || (p.match(/\/\/([\s\S]+?)\//) || ["//" + location.host + "/"])[0], 0 === e.length || layui["layui.all"] && u[f] || !layui["layui.all"] && layui["layui.mobile"] && u[f]) return c(), y;</li></ol></code></pre><p><span style="font-weight:bold; color:rgb(255, 87, 34);">G. </span>判断用户是否自定义host,无,则从用户配置的dir匹配url中//../这部分即//baidu.com/,不要http及后面的脚本路径部分,如果也无则从当前加载脚本或第一个脚本中获取,再无,则从location对象中获取,即location.host。</p><p>if条件的后半部分要理解,必须先解析layui.define函数。if判断中,先检测传入的模块数组是否为0,或者layui对象上layui.all是否存在,f=e[0], 即传入的模块数组e第一个,u[f]在以下变量中有值,如果用户未在use的时候传入模块数组,即为空,那符合e.length===0,u[f]</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>u = { layer: "modules/layer", laydate: "modules/laydate", laypage: "modules/laypage", laytpl: "modules/laytpl", layim: "modules/layim", layedit: "modules/layedit", form: "modules/form", upload: "modules/upload", tree: "modules/tree", table: "modules/table", element: "modules/element", rate: "modules/rate", colorpicker: "modules/colorpicker", slider: "modules/slider", carousel: "modules/carousel", flow: "modules/flow", util: "modules/util", code: "modules/code", jquery: "modules/jquery", mobile: "modules/mobile", "layui.all": "../layui.all" };</li></ol></code></pre><p>这个if条件后半部分不太好理解。我们先看define做了些什么。</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>n.prototype.cache = o, n.prototype.define = function(e, t) { var n = this, r = "function" == typeof e, i = function() { var e = function(e, t) { layui[e] = t, o.status[e] = !0 }; return "function" == typeof t && t(function(n, r) { e(n, r), o.callback[n] = function() { t(e) } }), this }; return r && (t = e, e = []), layui["layui.all"] || !layui["layui.all"] && layui["layui.mobile"] ? i.call(n) : (n.use(e, i), n) }</li></ol></code></pre><p>layui的define方法的别名叫cache。this赋值给n,判断传入的e参数是否为函数,通常e可能是依赖的参数数组或字符串。e如果为函数,则不传入依赖模块。i是一个函数,最终返回值是layui对象,在返回前执行,判断define第二个参数如果是闭包函数,则执行它,再传入一个闭包,这闭包即实际应用中的export函数,它接受两个参数,一个是对外暴露的接口名,一个是接口实体。当执行export以后,执行e函数,e函数第一个参数是接口名,第二个参数是接口实体,将接口加载到layui对象上,并赋值它到实体,如此使用layui.xxx即可调用,并在内部对象o的属性status上记录该接口状态为真。在export内部给layui内部o对象的callback对象的该接口属性赋值一个闭包,闭包内执行define传入的第二个参数即闭包函数,参数为第一个数组,即依赖模块,此时该闭包的参数是一个数组,而非export函数,因此只执行define中的程序,而无接口对外暴露的操作。</p><p> </p><p>最后返回结果,如果define第一个参数为闭包,那将该闭包赋值给t,而e置空,紧接着判断layui上layui.all是否存在,或者它不存在但layui.mobile存在,则在layui上调用函数i,此时对上面定义的i函数进行了调用。否则的化,使用下面定义的user函数递归加载模块,并把闭包i也给use传了进去。并最终都将this即layui对象返回到执行上下文。在i.call(n)执行的时候,发生了什么呢?首先判断执行上下文中的t及return前半部分做赋值操作后的t,即保证无论define传入的第一个是闭包还是第二个是闭包,此时t都是function,紧接着t被调用,即通常在define中传入的闭包被调用,而这个闭包的参数是export函数,即return中给t传入的闭包函数,参数n是export的对外接口名,比如laypage,而r则是接口实体。当在define尾部export被调用的时候,e(n, r)这个函数将接口名即n,接口实体即r传入了e函数,e函数将实体t加载到layui对象上去,并且将layui内部对象o的status属性对应的值对象打个标记,即该接口状态为真。并且内部对象o的callback属性上,对应的该接口名上也赋值一个闭包,内部是define函数第二个参数即传入的闭包函数的调用。</p><p> </p><p>疑问1:该处layui['layui.all']等做啥用?相当于layui['layui.all'] || layui['layui.mobile'] ,模块名layui.all,代表所有模块。模块名layui.mobile,代表手机版的所有模块。找到layui.all.js第231-236行(格式化以后)</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>layui.define(function (a) { var i = layui.cache; layui.config({ dir: i.dir.replace(/lay\/dest\/$/, "") }), a("layui.all", layui.v) });</li></ol></code></pre><p>很显然在这里调用define函数,跟layui define函数一样,a相当于export函数,而这个函数内部调用了以上提到的i函数,将所有接口实体,此处相当于layui.v,即当前layui版本号,赋值给了接口layui.all,即设置了layui.layui.all=2.4.0这样的值。因此这类检测目的在于检测是模块调用还是经典场景(即直接引入js方式)使用。</p><p> </p><p>上面define函数画成四维导图如下(无法传图,贴文字吧!):</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>define function e,t params n layui object r bool,e function or not i function e function, used for loaded export to layui object at the same time,status and callback been recorded return t===function call user defined closure and excute export function return r===true t = e,classic use call i, otherwise recursive call use,layui object</li></ol></code></pre><p> </p><p> </p><p>疑问2: i函数执行后返回前t函数执行内部,给内部对象o赋值了callback,这个代码的用处:</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>n.prototype.factory = function(e) { if (layui[e]) return "function" == typeof o.callback[e] ? o.callback[e] : null },</li></ol></code></pre><p>也就是工厂函数factory调用的时候,会返回这个闭包作为工厂函数。参数e即模块名。此时执行仅仅执行define传入的用户闭包,并不执行export,因此在define中需要判断export是否为函数然后才能执行,否则遇到这种调用肯定报错。</p><p> </p><p><span style="font-weight:bold; color:rgb(255, 87, 34);">H. </span>条件成立,调用c函数,并返回layui对象。</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>function c() { l.push(layui[f]), e.length > 1 ? y.use(e.slice(1), n, l) : "function" == typeof n && n.apply(layui, l) }</li></ol></code></pre><p>该函数将layui对象上的模块推入数组l,即use第三个参数。判断参数一即模块数组个数是否大于1,如果是,则递归调用,传入删掉模块数组第一个参数后的其他模块数组,以及回调和模块对象数组l,否则判断n是否为函数,在layui上执行闭包。参数是l,即模块对象数组,这些对象已经被加载到layui对象上了,在use中调用的时候使用layui.xx进行调用。通常官方的写法是赋值给一个短变量后使用,即var laypage = layui.laypage。</p><p> </p><p>PS: 0 === e.length || layui["layui.all"] && u[f] || !layui["layui.all"] && layui["layui.mobile"] && u[f],结果解析:</p><p>如果0===e.length为真,后面结果即便为假,整个等式为真。如果为假,检测layui['layui.all']是否为真,为真,则还必须检测u[f]是否为真。如果二者都为真,后面有||,可不再检测。如果仍旧为假,也就是说是mobile端。检测为真。整个表达式为真。</p><p> </p><p>I. 最后一个条件判断:</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>if (o.modules[f])! function g() { return ++m > 1e3 * o.timeout / 4 ? i(f + " is not a valid module") : void("string" == typeof o.modules[f] && o.status[f] ? c() : setTimeout(g, 4)) }(); else { var v = t.createElement("script"), h = (u[f] ? p + "lay/" : /^\{\/\}/.test(y.modules[f]) ? "" : o.base || "") + (y.modules[f] || f) + ".js"; h = h.replace(/^\{\/\}/, ""), v.async = !0, v.charset = "utf-8", v.src = h + function() { var e = o.version === !0 ? o.v || (new Date).getTime() : o.version || ""; return e ? "?v=" + e : "" }(), d.appendChild(v), !v.attachEvent || v.attachEvent.toString && v.attachEvent.toString().indexOf("[native code") < 0 || a ? v.addEventListener("load", function(e) { s(e, h) }, !1) : v.attachEvent("onreadystatechange", function(e) { // 每当以下状态发生改变时都会调用该函数 // 0: 请求未初始化 // 1: 服务器连接已建立 // 2: 请求已接收 // 3: 请求处理中 // 4: 请求已完成,且响应已就绪 s(e, h) }), o.modules[f] = h }</li></ol></code></pre><p> </p><p>如果内部对象的modules上,f为e[0],即读取的依赖参数数组第一个,如果存在这个module,执行自执行函数。该函数会在预计的次数m内循环检测某一module是否已经加载到layui对象上去。如果o对象上没有该modules,ps:o上的modules对象记录了某模块是否已经被动态写入到dom树中。如果modules上没有,创建script标签,如果依赖数组第一个存在,即系统默认,直接使用lay路径,如果检测到{/}开头,替换为空,否则为o.base或者空,再加上其文件名,如果在modules中,读取它,不在直接传入的依赖名。下面做replacce,<span style="color:rgb(255, 87, 34);">疑问:将{/}替换,干啥用的暂时不知道,base干啥用的也不知道?</span>然后加给scirpt元素加async标签,即异步,字符编码和处理资源路径,已处理完的路径前缀+版本号。再在head中追加该标签,检测attachEvent未定义,或者有定义但是非函数,这又是一种检测函数的方法,或者检测到是opera,使用addEventListener执行load函数绑定,并且闭包中执行s函数,其中addEventListener为阻止冒泡, 如果有attachEvent,则使用它进行事件绑定。最后给o对象modules上对应依赖赋值module路径。</p><p> </p><p>其中s函数:</p><pre><code class="larry-codes layui-box layui-code-view layui-code-notepad"><h3 class="layui-code-h3">code</h3><ol class="layui-code-ol"><li>function s(e, t) { var n = "PLaySTATION 3" === n{<img class="larryms-face" alt="[神马]" title="[神马]" src="http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/horse2_thumb.gif">}igator.platform ? /^complete$/ : /^(complete|loaded)$/; ("load" === e.type || n.test((e.currentTarget || e.srcElement).readyState)) && (o.modules[f] = t, d.removeChild(v), function r() { return ++m > 1e3 * o.timeout / 4 ? i(f + " is not a valid module") : void(o.status[f] ? c() : setTimeout(r, 4)) }()) }</li></ol></code></pre><p>检测平台,决定test的RegExp表达式检测complete还是complete和loaded,如果事件e的type为load,或者检测script元素状态为complete或loaded,则将加载的模块路径赋值到o对象的modules上,删除该script dom标签。关于删除:</p><blockquote>原因其实也就是js的按块执行、非阻塞I/O以及Event loop机制。Ja vaScript代码块被执行,创造出自己的执行环境后,就完全和dom中的script标签脱离了。事件队列中的事件全都执行完毕后执行环境才被销毁。来源:https://segmentfault.com/q/1010000005041807。然后r闭包执行,意思是检测在一定循环次数内,f模块是否已经加载到layui对象上去,否循环检测到指定次数,真则调用c函数,推入加载到layui上的对象到l数组,循环加载依赖模块,或者全部依赖加载完毕,调用use传入的回调。 </blockquote><p> </p><p>本文完。本文会持续更新及纠正。</p>
回帖
  • <p><img src="http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d8/good_thumb.gif" alt="[good]" data-w-e="1">文章非常不错,但是,这个排版,真是。。。。有待改善~~~,当然,这个有可能跟编辑器有一定的关系~~~!<br></p>
    1
  • 江天
    2018-10-11
    @骑猪Ta夕阳 哈哈哈哈。找版主。呼叫版主。。。。<p><br></p>
    0
  • <p>那是因为你看的懂,而我看不懂!</p>
    0
  • 江天
    2018-10-12
    @发呆msg <p>你那儿看不懂,咱们探讨。</p>
    0
  • @江天 <p>探讨估计就给你气死了。不讨了。你告诉我怎么引入外部js就行。单独引用外部js作为layui的一个模块。能有个demo最好了 我邮箱1142275013@qq.com</p>
    0
  • 江天
    2018-10-13
    该回贴已被外星人劫持啦!
    0
提交回复
您的回贴若被采纳将获得悬赏积分;但恶意灌水广告贴将会受到系统惩罚,共同营造良好交流氛围