5.2.4 源码分析

在源码分析之前,确保我们已经记住了弹窗Modal的基本元素,这样在阅读JavaScript代码时才能对照元素进行理解。示例如下:

  1. <!-- 触发元素 -->
  2. <button data-toggle="modal" data-target="#myModal" class="btn btn-primary">
  3. 触发元素</button>
  4. <!-- 弹窗内容 -->
  5. <div class="modal fade" id="myModal">
  6. <div class="modal-dialog">
  7. <div class="modal-content">
  8. <div class="modal-header">...</div>
  9. <div class="modal-body">...</div>
  10. <div class="modal-footer">...</div>
  11. </div>
  12. </div>
  13. </div>

步骤1 立即调用的函数。回顾2.4节,JavaScript插件架构里的步骤,第一步是最通用的步骤,也就是使用立即调用的函数,防止插件内代码外泄,从而形成一个闭环,并且只能从jQuery的fn里进行扩展。

  1. // 1.定义立即调用的函数
  2. +function ($) {
  3. "use strict"; // 使用严格模式 ES5支持
  4. // 后续步骤
  5. // 2.Modal插件类及原型方法的定义
  6. // 3.在jQuery上定义Modal插件,并重设插件构造器
  7. // 可以通过该属性获取插件的真实类函数
  8. // 4. 防冲突处理
  9. // 5. 绑定触发事件
  10. }(window.jQuery);

步骤2 插件核心代码。主要是Modal核心类函数的定义、默认参数的定义和8个原型方法的定义,这8个原型方法主要是处理弹窗的反转、打开、关闭和弹窗背景设置、取消等操作。核心代码如下:

  1. var Modal = function (element, options) {
  2. // element表示modal弹出框容器及内部元素,options是设置选项
  3. this.options = options // 传进来的各种参数
  4. this.$element = $(element)
  5. this.$backdrop = // modal下面的背景对象
  6. this.isShown = null // 默认情况下,不设置是否已经显示弹窗
  7.  
  8. // 如果设置了remote,就加载remote指定url的内容到modal-content样式的元素内,并触发
  9. // loaded.bs.modal事件
  10. if (this.options.remote) {
  11. this.$element
  12. .find('.modal-content')
  13. .load(this.options.remote, $.proxy(function () {
  14. this.$element.trigger('loaded.bs.modal') // 触发loaded.bs.modal事件
  15. }, this))
  16. }
  17. }
  18.  
  19. Modal.DEFAULTS = {{ // 默认设置
  20. backdrop: true, // 默认单击弹窗以外的地方时自动关闭弹窗
  21. keyboard: true, // 默认设置,按Esc键关闭弹窗
  22. show: true // 默认设置,单击触发元素时打开弹窗
  23. }
  24. // 反转弹窗状态
  25. Modal.prototype.toggle = function () { };
  26. // 打开弹窗
  27. Modal.prototype.show = function () { };
  28. // 关闭弹窗
  29. Modal.prototype.hide = function (e) { };
  30. // 确保当前打开的弹窗处于焦点状态
  31. Modal.prototype.enforceFocus = function () { };
  32. // 按Esc键是否退出的处理
  33. Modal.prototype.escape = function () { };
  34. // 关闭弹窗
  35. Modal.prototype.hideModal = function () { };
  36. // 删除背景,关闭弹窗时触发
  37. Modal.prototype.removeBackdrop = function () { };
  38. // 添加背景,打开弹窗时触发
  39. Modal.prototype.backdrop = function (callback) { };

注意

上面的this.$element.load(this.options.remote)语句表示将内容加载至modal-content样式的元素内,而不是modal-body样式元素内,这和2.x版完全不一样了。

下面的代码是上述8个原型方法的详细注释,阅读的时候主要是要注意元素焦点、动画、回调函数等主要代码的执行原理。

  1. // 反转弹窗状态
  2. Modal.prototype.toggle = function (_relatedTarget) {
  3. return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
  4. // 如果是关闭状态,则打开弹窗,否则就关闭
  5. }
  6. // 打开弹窗
  7. Modal.prototype.show = function (_relatedTarget) {
  8. var that = this // 当前modal对象赋值为that,防止作用域冲突
  9. var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
  10. // 定义弹窗前的触发事件
  11.  
  12. this.$element.trigger(e) // 打开弹窗前,触发事件
  13.  
  14. // 如果已经打开了(或者曾经被阻止过),则退出执行,后续代码不做处理
  15. if (this.isShown || e.isDefaultPrevented()) return
  16.  
  17. this.isShown = true // 设置状态为打开
  18.  
  19. this.escape() // 处理键盘事件,主要是设置按Esc键的时候是否关闭弹窗
  20.  
  21. this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]',
  22. (this.hide, this))
  23. // 如果单击了元素内的子元素(带有[data-dismiss="modal"]属性),则关闭弹窗
  24.  
  25. this.backdrop(function () { // 绘制弹窗背景以后,处理以下代码
  26. var transition = $.support.transition && that.$element.hasClass('fade')
  27. // 判断浏览器是否支持动画,并且弹窗是否设置了动画过渡效果(是否有fade样式)
  28.  
  29. if (!that.$element.parent().length) {
  30. that.$element.appendTo(document.body)
  31. // 如果modal弹窗没有父容器,则将它附加到body上
  32. }
  33.  
  34. that.$elementshow().scrollTop(0) // 显示modal弹窗
  35.  
  36. if (transition) {
  37. that.$element[0].offsetWidth // 如果支持动画,强制刷新UI现场,重绘弹窗
  38. }
  39.  
  40. that.$element
  41. .addClass('in') // 给modal弹窗添加in样式,和modal样式一起
  42. .attr('aria-hidden', false) // 设置aria-hidden为假(告诉阅读器该元素是非
  43. // 隐藏状态)
  44.  
  45. that.enforceFocus() // 强制给弹窗设定焦点
  46. var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
  47.  
  48. // 打开弹窗显示后的触发事件
  49. transition ?
  50. that.$element.find('.modal-dialog') // 找到弹窗元素
  51. .one($.support.transition.end, function () {
  52. // 如果支持动画,则动画结束以后给弹窗内的元素设置焦点,并触发shown事件
  53. that.$element.focus().trigger(e)
  54. })
  55. .emulateTransitionEnd(300) :
  56. that.$element.focus().trigger(e) // 否则直接设置焦点,并触发shown事件
  57. })
  58. }
  59. // 关闭弹窗
  60. Modal.prototype.hide = function (e) {
  61. if (e) e.preventDefault() // 先阻止冒泡行为
  62. e = $.Event('hide.bs.modal') // 关闭弹窗前的触发事件
  63. this.$element.trigger(e) // 关闭弹窗前触发事件
  64.  
  65. // 如果已经关闭了(或者曾经被阻止过),则退出执行后续代码不做处理
  66. if (!this.isShown || e.isDefaultPrevented()) return
  67.  
  68. this.isShown = false // 设置状态为关闭
  69. this.escape() // 处理键盘事件,主要是设置按Esc键的时候是否关闭弹窗
  70. $(document).off('focusin.bs.modal') // 取消所有的focusin.bs.modal事件
  71.  
  72. this.$element
  73. .removeClass('in') // 删除in样式
  74. .attr('aria-hidden', true) // 设定aria-hidden为true(告诉阅读器该元素是隐藏状态)
  75. .off('click.dismiss.bs.modal') // 取消dismiss的单击事件
  76.  
  77. // 如果支持动画,则动画结束以后再关闭,否则直接关闭
  78. $.support.transition && this.$element.hasClass('fade') ?
  79. this.$element
  80. .one($.support.transition.end, $.proxy(this.hideModal, this))
  81. .emulateTransitionEnd(300) :
  82. this.hideModal()
  83. }
  84. // 确保当前打开的弹窗处于焦点状态
  85. Modal.prototype.enforceFocus = function () {
  86. $(document)
  87. .off('focusin.bs.modal') // 禁用所有的focusin事件,防止无限循环
  88. .on('focusin.bs.modal', $.proxy(function (e) {
  89. if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
  90. // 如果处于焦点的元素不是当前元素(或不包含当前元素),则强制给当前元素设置焦点
  91. this.$element.focus()
  92. }
  93. }, this))
  94. }
  95. // 按Esc键是否退出的处理
  96. Modal.prototype.escape = function () {
  97. if (this.isShown && this.options.keyboard) {
  98. // 如果弹窗是打开状态,并且keyboard选项不为false,则说明允许按Esc键关闭弹窗
  99. this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
  100. // 检测键盘事件,如果是Esc(keycode=27)键,则关闭
  101. e.which == 27 && this.hide()
  102. }, this))
  103. } else if (!this.isShown) { // 否则,取消键盘事件检测
  104. this.$element.off('keyup.dismiss.bs.modal')
  105. }
  106. }
  107. // 关闭弹窗
  108. Modal.prototype.hideModal = function () {
  109. var that = this
  110. this.$element.hide() // 关闭弹窗
  111. this.backdrop(function () {
  112. that.removeBackdrop() // 清除背景
  113. that.$element.trigger('hidden.bs.modal')
  114. // 关闭以后,触发hidden事件(hide事件是在关闭前执行的)
  115. })
  116. }
  117. // 删除背景,关闭弹窗时触发
  118. Modal.prototype.removeBackdrop = function () {
  119. this.$backdrop && this.$backdrop.remove() // 删除背景元素
  120. this.$backdrop = null // 设置背景对象为null
  121. }
  122. // 添加背景,打开弹窗时触发
  123. Modal.prototype.backdrop = function (callback) {
  124. var animate = this.$element.hasClass('fade') ? 'fade' : ''
  125. // 是否设置了动画过渡效果,如果是则设置为fade
  126. if (this.isShown && this.options.backdrop) {
  127. // 如果是打开状态,并且设置了backdrop参数
  128. var doAnimate = $.support.transition && animate // 定义动画标识
  129.  
  130. // 在body上定义背景div元素,并附加fade标识以支持动画
  131. this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
  132. .appendTo(document.body)
  133.  
  134.  
  135. // 背景被单击时进行判断:如果backdrop参数为static,则强制将弹窗设置为焦点;否则,关闭弹窗
  136. this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
  137. if (e.target !== e.currentTarget) return
  138. this.options.backdrop == 'static'
  139. ? this.$element[0].focus.call(this.$element[0])
  140. : this.hide.call(this)
  141. }, this))
  142.  
  143. if (doAnimate) this.$backdrop[0].offsetWidth
  144. // 如果支持动画,则强制刷新UI现场,重绘弹窗
  145. this.$backdrop.addClass('in') // 添加in样式
  146. if (!callback) return // 如果没有回调,则直接返回
  147.  
  148. // 如果支持动画,则动画结束后执行回调函数;否则,直接执行回调函数
  149. doAnimate ?
  150. this.$backdrop
  151. .one($.support.transition.end, callback)
  152. .emulateTransitionEnd(150) :
  153. callback()
  154.  
  155. } else if (!this.isShown && this.$backdrop) {
  156. // 如果是关闭状态,但背景对象依然还存在
  157. this.$backdrop.removeClass('in') // 去除in样式
  158.  
  159. // 如果支持动画,则动画结束后执行回调函数;否则,直接执行回调函数
  160. $.support.transition && this.$element.hasClass('fade') ?
  161. this.$backdrop
  162. .one($.support.transition.end, callback)
  163. .emulateTransitionEnd(150) :
  164. callback()
  165.  
  166. } else if (callback) { // 如果不是以上两种情况,但回调函数却存在
  167. callback() // 直接执行回调函数
  168. }
  169. }

步骤3 jQuery插件定义。在jQuery上定义$.fn.modal插件,有点特殊的代码是options参数的收集和合并,主要收集了3部分:插件的默认参数Defaults、modal元素上的data-属性、执行插件时候传入的option对象,三部分的优先级依次升高。源码如下:

  1. var old = $.fn.modal
  2. // 保留其他库的$.fn.modal代码(如果定义的话),以便在noConflict之后可以继续使用该老代码
  3. $.fn.modal = function (option, _relatedTarget) {
  4. return this.each(function () { // 根据选择器,遍历所有符合规则的元素
  5. var $this = $(this)
  6. var data = $this.data('bs.modal') // 获取自定义属性bs.modal的值
  7.  
  8. // 将默认参数、选择器所在元素的自定义属性(data-开头)和option参数,这三种值合并在
  9. 一起,作为options参数
  10. // 优先级:后面的参数优先级高于前面的参数
  11. var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option ==
  12. 'object' && option)
  13.  
  14. // 如果值不存在,则将Modal实例设置为bs.modal的值
  15. if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
  16.  
  17. // 如果option传递了string,则表示要执行某个方法
  18. // 比如传入了show,则要执行Modal实例的show方法,data["show"]相当于data.show()
  19. if (typeof option == 'string') data[option](_relatedTarget)
  20. else if (options.show) data.show(_relatedTarget)
  21. })
  22. }
  23. $.fn.modal.Constructor = Modal // 重设插件构造器,可以通过该属性获取插件的真实类函数

步骤4 防冲突处理。源码如下:

  1. // 防冲突处理
  2. $.fn.modal.noConflict = function () {
  3. $.fn.modal = old // 恢复以前的旧代码
  4. return this // 将$.fn.modal.noConflict()设置为Bootstrap的Modal插件
  5. }

步骤5 绑定触发事件。源码如下:

  1. // 绑定触发事件
  2. $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
  3. // 监测所有拥有自定义属性data-toggle="modal"的元素上的单击事件
  4.  
  5. var $this = $(this)
  6. var href = $this.attr('href') // 获取href属性值
  7.  
  8. // 获取data-target属性值,如果没有,则获取href值,该值是所弹出元素的id
  9. var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')))
  10.  
  11. // 如果弹窗元素上已经有该弹窗实例(即弹出过一次了),则设置option值为字符串toggle
  12. // 否则将remote值(如果有的话)、弹窗元素上的自定义属性值集合、触发元素上的自定义属性值集合,
  13. 合并为option对象
  14. var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.
  15. test(href) && href }, $target.data(), $this.data())
  16.  
  17. if ($this.is('a')) e.preventDefault()() // 如果是a链接元素,则还要阻止默认行为
  18.  
  19. $target
  20. .modal(option, this) // 给弹窗元素绑定Modal插件(也就是实例化Modal),并传入option参数
  21. .one('hide', function () {
  22. $this.is(':visible') && $this.focus() // 定义一次hide事件,给所单击元素加上焦点
  23. })
  24. })
  25.  
  26. $(document)
  27. // 给所有的弹窗元素绑定shown事件,一旦该弹窗打开以后,就在body上添加modal-open样式
  28. .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
  29. // 同理,给所有的弹窗元素绑定hidden事件,一旦该弹窗关闭以后,就删除body上的modal-open样式
  30. .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })

在绑定触发事件上,最后在body元素上添加、删除modal-open样式的操作时需要注意,因为该样式控制了整个body样式(超出边界是自动隐藏),然后给其内部的modal元素(或backdrop背景)做样式设置的时候提供了参考,但切勿通过此样式影响其他的内容布局等,所以要确保该样式不被其他功能或者插件所引用。

  1. // 源码5093行
  2. .modal-open {
  3. overflow: hidden;
  4. }

至此,模态窗体的适应方法和源码分析就结束了。通过分析我们可以看出它的设计精妙的部分,尤其是几个元素的z-index设置,以及在触发事件时候的各种样式的操作。最后还有可扩展的事件,比如在弹出前触发的show,弹出后触发的shown,以及关闭前触发的hide,和关闭后触发的hidden,都为我们做自定义插件提供了良好的参考。